From 7d4a6be569bc6f6cf3b859f2f7c351164965e9e1 Mon Sep 17 00:00:00 2001
From: Glax <johanna@bernloehr.eu>
Date: Fri, 9 May 2025 06:28:44 +0200
Subject: [PATCH 01/50] MangaConnectors do not have to return an Object with 6
 Parameters. Job-Start Logic readable and optimized More robust Database
 design

---
 .../DownloadAvailableChaptersJobRecord.cs     |   5 +
 .../DownloadAvailableJobsRecord.cs            |   5 -
 API/Controllers/JobController.cs              |  59 +-
 API/Controllers/LibraryConnectorController.cs |  19 +-
 API/Controllers/LocalLibrariesController.cs   |   8 +-
 API/Controllers/MangaConnectorController.cs   |   4 +-
 API/Controllers/MangaController.cs            |  30 +-
 .../NotificationConnectorController.cs        |   5 +-
 API/Controllers/QueryController.cs            |  18 +-
 API/Controllers/SearchController.cs           | 167 ++--
 API/Controllers/SettingsController.cs         |   9 +-
 API/Migrations/20250316150158_dev-160325-2.cs |  42 -
 .../20250401001439_dev-010425-1.Designer.cs   | 827 ------------------
 ...401162026_dev-010425-2-Longer_Var_Chars.cs |  98 ---
 .../20250402001438_dev-010425-4.Designer.cs   | 762 ----------------
 API/Migrations/20250402001438_dev-010425-4.cs | 123 ---
 ....cs => 20250509033915_Initial.Designer.cs} | 351 ++++----
 ...5-Initial.cs => 20250509033915_Initial.cs} | 142 +--
 ...s => 20250509034207_Initial-2.Designer.cs} | 351 ++++----
 ...nership.cs => 20250509034207_Initial-2.cs} |   2 +-
 ...s => 20250509035413_Initial-3.Designer.cs} | 365 ++++----
 API/Migrations/20250509035413_Initial-3.cs    | 130 +++
 ...s => 20250509035606_Initial-4.Designer.cs} | 377 ++++----
 ...10425-1.cs => 20250509035606_Initial-4.cs} |  22 +-
 .../20250509035754_Initial-5.Designer.cs      | 783 +++++++++++++++++
 API/Migrations/20250509035754_Initial-5.cs    |  40 +
 API/Migrations/PgsqlContextModelSnapshot.cs   | 393 +++++----
 API/Program.cs                                |  14 +-
 API/Schema/Chapter.cs                         | 126 +--
 .../Jobs/DownloadAvailableChaptersJob.cs      |  28 +-
 API/Schema/Jobs/DownloadMangaCoverJob.cs      |  41 +-
 API/Schema/Jobs/DownloadSingleChapterJob.cs   |  64 +-
 API/Schema/Jobs/Job.cs                        |  75 +-
 API/Schema/Jobs/JobState.cs                   |   3 +-
 API/Schema/Jobs/MoveFileOrFolderJob.cs        |  24 +-
 API/Schema/Jobs/MoveMangaLibraryJob.cs        |  52 +-
 API/Schema/Jobs/RetrieveChaptersJob.cs        |  49 +-
 API/Schema/Jobs/UpdateFilesDownloadedJob.cs   |  39 +-
 API/Schema/Jobs/UpdateMetadataJob.cs          |  27 -
 API/Schema/LibraryConnectors/Kavita.cs        |   6 +-
 API/Schema/Manga.cs                           | 254 +++---
 API/Schema/MangaAltTitle.cs                   |   1 -
 API/Schema/MangaConnectors/AsuraToon.cs       | 191 ----
 API/Schema/MangaConnectors/Bato.cs            | 203 -----
 API/Schema/MangaConnectors/Global.cs          |  22 +-
 API/Schema/MangaConnectors/MangaConnector.cs  |  61 +-
 API/Schema/MangaConnectors/MangaDex.cs        | 528 +++++------
 API/Schema/MangaConnectors/MangaHere.cs       | 183 ----
 API/Schema/MangaConnectors/MangaKatana.cs     | 233 -----
 API/Schema/MangaConnectors/Manganato.cs       | 219 -----
 API/Schema/MangaConnectors/Mangaworld.cs      | 223 -----
 API/Schema/MangaConnectors/ManhuaPlus.cs      | 179 ----
 API/Schema/MangaConnectors/Webtoons.cs        | 259 ------
 API/Schema/MangaConnectors/WeebCentral.cs     | 175 ----
 API/Schema/PgsqlContext.cs                    | 191 ++--
 API/Tranga.cs                                 | 172 ++--
 56 files changed, 2924 insertions(+), 5855 deletions(-)
 create mode 100644 API/APIEndpointRecords/DownloadAvailableChaptersJobRecord.cs
 delete mode 100644 API/APIEndpointRecords/DownloadAvailableJobsRecord.cs
 delete mode 100644 API/Migrations/20250316150158_dev-160325-2.cs
 delete mode 100644 API/Migrations/20250401001439_dev-010425-1.Designer.cs
 delete mode 100644 API/Migrations/20250401162026_dev-010425-2-Longer_Var_Chars.cs
 delete mode 100644 API/Migrations/20250402001438_dev-010425-4.Designer.cs
 delete mode 100644 API/Migrations/20250402001438_dev-010425-4.cs
 rename API/Migrations/{20250401162026_dev-010425-2-Longer_Var_Chars.Designer.cs => 20250509033915_Initial.Designer.cs} (78%)
 rename API/Migrations/{20250316143014_dev-160325-Initial.cs => 20250509033915_Initial.cs} (83%)
 rename API/Migrations/{20250401234456_dev-010425-3-ParentJobOwnership.Designer.cs => 20250509034207_Initial-2.Designer.cs} (78%)
 rename API/Migrations/{20250401234456_dev-010425-3-ParentJobOwnership.cs => 20250509034207_Initial-2.cs} (85%)
 rename API/Migrations/{20250316150158_dev-160325-2.Designer.cs => 20250509035413_Initial-3.Designer.cs} (78%)
 create mode 100644 API/Migrations/20250509035413_Initial-3.cs
 rename API/Migrations/{20250316143014_dev-160325-Initial.Designer.cs => 20250509035606_Initial-4.Designer.cs} (77%)
 rename API/Migrations/{20250401001439_dev-010425-1.cs => 20250509035606_Initial-4.cs} (62%)
 create mode 100644 API/Migrations/20250509035754_Initial-5.Designer.cs
 create mode 100644 API/Migrations/20250509035754_Initial-5.cs
 delete mode 100644 API/Schema/Jobs/UpdateMetadataJob.cs
 delete mode 100644 API/Schema/MangaConnectors/AsuraToon.cs
 delete mode 100644 API/Schema/MangaConnectors/Bato.cs
 delete mode 100644 API/Schema/MangaConnectors/MangaHere.cs
 delete mode 100644 API/Schema/MangaConnectors/MangaKatana.cs
 delete mode 100644 API/Schema/MangaConnectors/Manganato.cs
 delete mode 100644 API/Schema/MangaConnectors/Mangaworld.cs
 delete mode 100644 API/Schema/MangaConnectors/ManhuaPlus.cs
 delete mode 100644 API/Schema/MangaConnectors/Webtoons.cs
 delete mode 100644 API/Schema/MangaConnectors/WeebCentral.cs

diff --git a/API/APIEndpointRecords/DownloadAvailableChaptersJobRecord.cs b/API/APIEndpointRecords/DownloadAvailableChaptersJobRecord.cs
new file mode 100644
index 0000000..39ca18a
--- /dev/null
+++ b/API/APIEndpointRecords/DownloadAvailableChaptersJobRecord.cs
@@ -0,0 +1,5 @@
+using System.ComponentModel.DataAnnotations;
+
+namespace API.APIEndpointRecords;
+
+public record DownloadAvailableChaptersJobRecord([Required]string language, [Required]ulong recurrenceTimeMs, [Required]string localLibraryId);
\ No newline at end of file
diff --git a/API/APIEndpointRecords/DownloadAvailableJobsRecord.cs b/API/APIEndpointRecords/DownloadAvailableJobsRecord.cs
deleted file mode 100644
index a272df3..0000000
--- a/API/APIEndpointRecords/DownloadAvailableJobsRecord.cs
+++ /dev/null
@@ -1,5 +0,0 @@
-using System.ComponentModel.DataAnnotations;
-
-namespace API.APIEndpointRecords;
-
-public record DownloadAvailableJobsRecord([Required]ulong recurrenceTimeMs, [Required]string localLibraryId);
\ No newline at end of file
diff --git a/API/Controllers/JobController.cs b/API/Controllers/JobController.cs
index 1f4309a..97714b0 100644
--- a/API/Controllers/JobController.cs
+++ b/API/Controllers/JobController.cs
@@ -2,15 +2,17 @@
 using API.Schema;
 using API.Schema.Jobs;
 using Asp.Versioning;
+using log4net;
 using Microsoft.AspNetCore.Mvc;
 using static Microsoft.AspNetCore.Http.StatusCodes;
+// ReSharper disable InconsistentNaming
 
 namespace API.Controllers;
 
 [ApiVersion(2)]
 [ApiController]
 [Route("v{version:apiVersion}/[controller]")]
-public class JobController(PgsqlContext context) : Controller
+public class JobController(PgsqlContext context, ILog Log) : Controller
 {
     /// <summary>
     /// Returns all Jobs
@@ -102,7 +104,7 @@ public class JobController(PgsqlContext context) : Controller
     /// <param name="MangaId">ID of Manga</param>
     /// <param name="record">Job-Configuration</param>
     /// <response code="201">Job-IDs</response>
-    /// <response code="400">Could not find Library with ID</response>
+    /// <response code="400">Could not find ToLibrary with ID</response>
     /// <response code="404">Could not find Manga with ID</response>
     /// <response code="500">Error during Database Operation</response>
     [HttpPut("DownloadAvailableChaptersJob/{MangaId}")]
@@ -110,7 +112,7 @@ public class JobController(PgsqlContext context) : Controller
     [ProducesResponseType(Status400BadRequest)]
     [ProducesResponseType(Status404NotFound)]
     [ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
-    public IActionResult CreateDownloadAvailableChaptersJob(string MangaId, [FromBody]DownloadAvailableJobsRecord record)
+    public IActionResult CreateDownloadAvailableChaptersJob(string MangaId, [FromBody]DownloadAvailableChaptersJobRecord record)
     {
         if (context.Mangas.Find(MangaId) is not { } m)
             return NotFound();
@@ -126,13 +128,13 @@ public class JobController(PgsqlContext context) : Controller
             }
             catch (Exception e)
             {
+                Log.Error(e);
                 return StatusCode(500, e.Message);
             }
         }
-        Job job = new DownloadAvailableChaptersJob(record.recurrenceTimeMs, MangaId);
-        Job dep = new RetrieveChaptersJob(record.recurrenceTimeMs, MangaId, job.JobId);
-        job.DependsOnJobsIds?.Add(dep.JobId);
-        return AddJobs([dep, job]);
+        Job retrieveChapters = new RetrieveChaptersJob(m, record.language, record.recurrenceTimeMs);
+        Job downloadChapters = new DownloadAvailableChaptersJob(m, record.recurrenceTimeMs, dependsOnJobs: [retrieveChapters]);
+        return AddJobs([retrieveChapters, downloadChapters]);
     }
 
     /// <summary>
@@ -148,9 +150,9 @@ public class JobController(PgsqlContext context) : Controller
     [ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
     public IActionResult CreateNewDownloadChapterJob(string ChapterId)
     {
-        if(context.Chapters.Find(ChapterId) is null)
+        if(context.Chapters.Find(ChapterId) is not { } c)
             return NotFound();
-        Job job = new DownloadSingleChapterJob(ChapterId);
+        Job job = new DownloadSingleChapterJob(c);
         return AddJobs([job]);
     }
 
@@ -167,9 +169,9 @@ public class JobController(PgsqlContext context) : Controller
     [ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
     public IActionResult CreateUpdateFilesDownloadedJob(string MangaId)
     {
-        if(context.Mangas.Find(MangaId) is null)
+        if(context.Mangas.Find(MangaId) is not { } m)
             return NotFound();
-        Job job = new UpdateFilesDownloadedJob(0, MangaId);
+        Job job = new UpdateFilesDownloadedJob(m, 0);
         return AddJobs([job]);
     }
 
@@ -183,8 +185,7 @@ public class JobController(PgsqlContext context) : Controller
     [ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
     public IActionResult CreateUpdateAllFilesDownloadedJob()
     {
-        List<string> ids = context.Mangas.Select(m => m.MangaId).ToList();
-        List<UpdateFilesDownloadedJob> jobs =  ids.Select(id => new UpdateFilesDownloadedJob(0, id)).ToList();
+        List<UpdateFilesDownloadedJob> jobs = context.Mangas.Select(m => new UpdateFilesDownloadedJob(m, 0, null, null)).ToList();
         try
         {
             context.Jobs.AddRange(jobs);
@@ -193,12 +194,13 @@ public class JobController(PgsqlContext context) : Controller
         }
         catch (Exception e)
         {
+            Log.Error(e);
             return StatusCode(500, e.Message);
         }
     }
 
     /// <summary>
-    /// Create a new UpdateMetadataJob
+    /// Not Implemented: Create a new UpdateMetadataJob
     /// </summary>
     /// <param name="MangaId">ID of the Manga</param>
     /// <response code="201">Job-IDs</response>
@@ -210,14 +212,11 @@ public class JobController(PgsqlContext context) : Controller
     [ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
     public IActionResult CreateUpdateMetadataJob(string MangaId)
     {
-        if(context.Mangas.Find(MangaId) is null)
-            return NotFound();
-        Job job = new UpdateMetadataJob(0, MangaId);
-        return AddJobs([job]);
+        return StatusCode(Status501NotImplemented);
     }
 
     /// <summary>
-    /// Create a new UpdateMetadataJob for all Manga
+    /// Not Implemented: Create a new UpdateMetadataJob for all Manga
     /// </summary>
     /// <response code="201">Job-IDs</response>
     /// <response code="500">Error during Database Operation</response>
@@ -226,18 +225,7 @@ public class JobController(PgsqlContext context) : Controller
     [ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
     public IActionResult CreateUpdateAllMetadataJob()
     {
-        List<string> ids = context.Mangas.Select(m => m.MangaId).ToList();
-        List<UpdateMetadataJob> jobs =  ids.Select(id => new UpdateMetadataJob(0, id)).ToList();
-        try
-        {
-            context.Jobs.AddRange(jobs);
-            context.SaveChanges();
-            return Created();
-        }
-        catch (Exception e)
-        {
-            return StatusCode(500, e.Message);
-        }
+        return StatusCode(Status501NotImplemented);
     }
     
     private IActionResult AddJobs(Job[] jobs)
@@ -250,6 +238,7 @@ public class JobController(PgsqlContext context) : Controller
         }
         catch (Exception e)
         {
+            Log.Error(e);
             return StatusCode(500, e.Message);
         }
     }
@@ -269,8 +258,7 @@ public class JobController(PgsqlContext context) : Controller
     {
         try
         {
-            Job? ret = context.Jobs.Find(JobId);
-            if(ret is null)
+            if(context.Jobs.Find(JobId) is not { } ret)
                 return NotFound();
             
             context.Remove(ret);
@@ -279,6 +267,7 @@ public class JobController(PgsqlContext context) : Controller
         }
         catch (Exception e)
         {
+            Log.Error(e);
             return StatusCode(500, e.Message);
         }
     }
@@ -322,6 +311,7 @@ public class JobController(PgsqlContext context) : Controller
         }
         catch (Exception e)
         {
+            Log.Error(e);
             return StatusCode(500, e.Message);
         }
     }
@@ -354,6 +344,7 @@ public class JobController(PgsqlContext context) : Controller
         }
         catch (Exception e)
         {
+            Log.Error(e);
             return StatusCode(500, e.Message);
         }
     }
@@ -367,6 +358,6 @@ public class JobController(PgsqlContext context) : Controller
     [ProducesResponseType(Status501NotImplemented)]
     public IActionResult StopJob(string JobId)
     {
-        return StatusCode(501);
+        return StatusCode(Status501NotImplemented);
     }
 }
\ No newline at end of file
diff --git a/API/Controllers/LibraryConnectorController.cs b/API/Controllers/LibraryConnectorController.cs
index 4a7ee66..343462c 100644
--- a/API/Controllers/LibraryConnectorController.cs
+++ b/API/Controllers/LibraryConnectorController.cs
@@ -1,6 +1,7 @@
 using API.Schema;
 using API.Schema.LibraryConnectors;
 using Asp.Versioning;
+using log4net;
 using Microsoft.AspNetCore.Mvc;
 using static Microsoft.AspNetCore.Http.StatusCodes;
 
@@ -9,10 +10,10 @@ namespace API.Controllers;
 [ApiVersion(2)]
 [ApiController]
 [Route("v{v:apiVersion}/[controller]")]
-public class LibraryConnectorController(PgsqlContext context) : Controller
+public class LibraryConnectorController(PgsqlContext context, ILog Log) : Controller
 {
     /// <summary>
-    /// Gets all configured Library-Connectors
+    /// Gets all configured ToLibrary-Connectors
     /// </summary>
     /// <response code="200"></response>
     [HttpGet]
@@ -24,9 +25,9 @@ public class LibraryConnectorController(PgsqlContext context) : Controller
     }
     
     /// <summary>
-    /// Returns Library-Connector with requested ID
+    /// Returns ToLibrary-Connector with requested ID
     /// </summary>
-    /// <param name="LibraryControllerId">Library-Connector-ID</param>
+    /// <param name="LibraryControllerId">ToLibrary-Connector-ID</param>
     /// <response code="200"></response>
     /// <response code="404">Connector with ID not found.</response>
     [HttpGet("{LibraryControllerId}")]
@@ -43,9 +44,9 @@ public class LibraryConnectorController(PgsqlContext context) : Controller
     }
     
     /// <summary>
-    /// Creates a new Library-Connector
+    /// Creates a new ToLibrary-Connector
     /// </summary>
-    /// <param name="libraryConnector">Library-Connector</param>
+    /// <param name="libraryConnector">ToLibrary-Connector</param>
     /// <response code="201"></response>
     /// <response code="500">Error during Database Operation</response>
     [HttpPut]
@@ -61,14 +62,15 @@ public class LibraryConnectorController(PgsqlContext context) : Controller
         }
         catch (Exception e)
         {
+            Log.Error(e);
             return StatusCode(500, e.Message);
         }
     }
     
     /// <summary>
-    /// Deletes the Library-Connector with the requested ID
+    /// Deletes the ToLibrary-Connector with the requested ID
     /// </summary>
-    /// <param name="LibraryControllerId">Library-Connector-ID</param>
+    /// <param name="LibraryControllerId">ToLibrary-Connector-ID</param>
     /// <response code="200"></response>
     /// <response code="404">Connector with ID not found.</response>
     /// <response code="500">Error during Database Operation</response>
@@ -90,6 +92,7 @@ public class LibraryConnectorController(PgsqlContext context) : Controller
         }
         catch (Exception e)
         {
+            Log.Error(e);
             return StatusCode(500, e.Message);
         }
     }
diff --git a/API/Controllers/LocalLibrariesController.cs b/API/Controllers/LocalLibrariesController.cs
index 6cb3a1a..004fa6d 100644
--- a/API/Controllers/LocalLibrariesController.cs
+++ b/API/Controllers/LocalLibrariesController.cs
@@ -1,6 +1,7 @@
 using API.APIEndpointRecords;
 using API.Schema;
 using Asp.Versioning;
+using log4net;
 using Microsoft.AspNetCore.Mvc;
 using static Microsoft.AspNetCore.Http.StatusCodes;
 
@@ -9,7 +10,7 @@ namespace API.Controllers;
 [ApiVersion(2)]
 [ApiController]
 [Route("v{v:apiVersion}/[controller]")]
-public class LocalLibrariesController(PgsqlContext context) : Controller
+public class LocalLibrariesController(PgsqlContext context, ILog Log) : Controller
 {
     [HttpGet]
     [ProducesResponseType<LocalLibrary[]>(Status200OK, "application/json")]
@@ -52,6 +53,7 @@ public class LocalLibrariesController(PgsqlContext context) : Controller
         }
         catch (Exception e)
         {
+            Log.Error(e);
             return StatusCode(500, e.Message);
         }
     }
@@ -79,6 +81,7 @@ public class LocalLibrariesController(PgsqlContext context) : Controller
         }
         catch (Exception e)
         {
+            Log.Error(e);
             return StatusCode(500, e.Message);
         }
     }
@@ -106,6 +109,7 @@ public class LocalLibrariesController(PgsqlContext context) : Controller
         }
         catch (Exception e)
         {
+            Log.Error(e);
             return StatusCode(500, e.Message);
         }
     }
@@ -128,6 +132,7 @@ public class LocalLibrariesController(PgsqlContext context) : Controller
         }
         catch (Exception e)
         {
+            Log.Error(e);
             return StatusCode(500, e.Message);
         }
     }
@@ -151,6 +156,7 @@ public class LocalLibrariesController(PgsqlContext context) : Controller
         }
         catch (Exception e)
         {
+            Log.Error(e);
             return StatusCode(500, e.Message);
         }
     }
diff --git a/API/Controllers/MangaConnectorController.cs b/API/Controllers/MangaConnectorController.cs
index 9bfa576..91c7f14 100644
--- a/API/Controllers/MangaConnectorController.cs
+++ b/API/Controllers/MangaConnectorController.cs
@@ -1,6 +1,7 @@
 using API.Schema;
 using API.Schema.MangaConnectors;
 using Asp.Versioning;
+using log4net;
 using Microsoft.AspNetCore.Mvc;
 using static Microsoft.AspNetCore.Http.StatusCodes;
 
@@ -9,7 +10,7 @@ namespace API.Controllers;
 [ApiVersion(2)]
 [ApiController]
 [Route("v{v:apiVersion}/[controller]")]
-public class MangaConnectorController(PgsqlContext context) : Controller
+public class MangaConnectorController(PgsqlContext context, ILog Log) : Controller
 {
     /// <summary>
     /// Get all available Connectors (Scanlation-Sites)
@@ -74,6 +75,7 @@ public class MangaConnectorController(PgsqlContext context) : Controller
         }
         catch (Exception e)
         {
+            Log.Error(e);
             return StatusCode(500, e.Message);
         }
     }
diff --git a/API/Controllers/MangaController.cs b/API/Controllers/MangaController.cs
index 006e58b..07a4065 100644
--- a/API/Controllers/MangaController.cs
+++ b/API/Controllers/MangaController.cs
@@ -1,19 +1,21 @@
 using API.Schema;
 using API.Schema.Jobs;
 using Asp.Versioning;
+using log4net;
 using Microsoft.AspNetCore.Mvc;
 using SixLabors.ImageSharp;
 using SixLabors.ImageSharp.Formats.Jpeg;
 using SixLabors.ImageSharp.Processing;
 using SixLabors.ImageSharp.Processing.Processors.Transforms;
 using static Microsoft.AspNetCore.Http.StatusCodes;
+// ReSharper disable InconsistentNaming
 
 namespace API.Controllers;
 
 [ApiVersion(2)]
 [ApiController]
 [Route("v{v:apiVersion}/[controller]")]
-public class MangaController(PgsqlContext context) : Controller
+public class MangaController(PgsqlContext context, ILog Log) : Controller
 {
     /// <summary>
     /// Returns all cached Manga
@@ -82,6 +84,7 @@ public class MangaController(PgsqlContext context) : Controller
         }
         catch (Exception e)
         {
+            Log.Error(e);
             return StatusCode(500, e.Message);
         }
     }
@@ -287,11 +290,12 @@ public class MangaController(PgsqlContext context) : Controller
         
         return Ok(max);
     }
-    
+
     /// <summary>
     /// Configure the cut-off for Manga
     /// </summary>
     /// <param name="MangaId">Manga-ID</param>
+    /// <param name="chapterThreshold">Threshold (Chapter Number)</param>
     /// <response code="200"></response>
     /// <response code="404">Manga with ID not found.</response>
     /// <response code="500">Error during Database Operation</response>
@@ -307,21 +311,22 @@ public class MangaController(PgsqlContext context) : Controller
         
         try
         {
-            m.IgnoreChapterBefore = chapterThreshold;
+            m.IgnoreChaptersBefore = chapterThreshold;
             context.SaveChanges();
             return Ok();
         }
         catch (Exception e)
         {
+            Log.Error(e);
             return StatusCode(500, e.Message);
         }
     }
 
     /// <summary>
-    /// Move Manga to different Library
+    /// Move Manga to different ToLibrary
     /// </summary>
     /// <param name="MangaId">Manga-ID</param>
-    /// <param name="LibraryId">Library-Id</param>
+    /// <param name="LibraryId">ToLibrary-Id</param>
     /// <response code="202">Folder is going to be moved</response>
     /// <response code="404">MangaId or LibraryId not found</response>
     /// <response code="500">Error during Database Operation</response>
@@ -331,24 +336,23 @@ public class MangaController(PgsqlContext context) : Controller
     [ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
     public IActionResult MoveFolder(string MangaId, string LibraryId)
     {
-        Manga? manga = context.Mangas.Find(MangaId);
-        if (manga is null)
+        if (context.Mangas.Find(MangaId) is not { } manga)
             return NotFound();
-        LocalLibrary? library = context.LocalLibraries.Find(LibraryId);
-        if (library is null)
+        if(context.LocalLibraries.Find(LibraryId) is not { } library)
             return NotFound();
-        
-        MoveMangaLibraryJob dep = new (MangaId, LibraryId);
-        UpdateFilesDownloadedJob up = new (0, manga.MangaId, null, [dep.JobId]);
+
+        MoveMangaLibraryJob moveLibrary = new(manga, library);
+        UpdateFilesDownloadedJob updateDownloadedFiles = new(manga, 0, dependsOnJobs: [moveLibrary]);
         
         try
         {
-            context.Jobs.AddRange([dep, up]);
+            context.Jobs.AddRange(moveLibrary, updateDownloadedFiles);
             context.SaveChanges();
             return Accepted();
         }
         catch (Exception e)
         {
+            Log.Error(e);
             return StatusCode(500, e.Message);
         }
     }
diff --git a/API/Controllers/NotificationConnectorController.cs b/API/Controllers/NotificationConnectorController.cs
index b3a5ed3..66ef992 100644
--- a/API/Controllers/NotificationConnectorController.cs
+++ b/API/Controllers/NotificationConnectorController.cs
@@ -3,6 +3,7 @@ using API.APIEndpointRecords;
 using API.Schema;
 using API.Schema.NotificationConnectors;
 using Asp.Versioning;
+using log4net;
 using Microsoft.AspNetCore.Mvc;
 using static Microsoft.AspNetCore.Http.StatusCodes;
 
@@ -12,7 +13,7 @@ namespace API.Controllers;
 [ApiController]
 [Produces("application/json")]
 [Route("v{v:apiVersion}/[controller]")]
-public class NotificationConnectorController(PgsqlContext context) : Controller
+public class NotificationConnectorController(PgsqlContext context, ILog Log) : Controller
 {
     /// <summary>
     /// Gets all configured Notification-Connectors
@@ -69,6 +70,7 @@ public class NotificationConnectorController(PgsqlContext context) : Controller
         }
         catch (Exception e)
         {
+            Log.Error(e);
             return StatusCode(500, e.Message);
         }
     }
@@ -209,6 +211,7 @@ public class NotificationConnectorController(PgsqlContext context) : Controller
         }
         catch (Exception e)
         {
+            Log.Error(e);
             return StatusCode(500, e.Message);
         }
     }
diff --git a/API/Controllers/QueryController.cs b/API/Controllers/QueryController.cs
index 2254e33..8877a8d 100644
--- a/API/Controllers/QueryController.cs
+++ b/API/Controllers/QueryController.cs
@@ -1,14 +1,16 @@
 using API.Schema;
 using Asp.Versioning;
+using log4net;
 using Microsoft.AspNetCore.Mvc;
 using static Microsoft.AspNetCore.Http.StatusCodes;
+// ReSharper disable InconsistentNaming
 
 namespace API.Controllers;
 
 [ApiVersion(2)]
 [ApiController]
 [Route("v{v:apiVersion}/[controller]")]
-public class QueryController(PgsqlContext context) : Controller
+public class QueryController(PgsqlContext context, ILog Log) : Controller
 {
     /// <summary>
     /// Returns the Author-Information for Author-ID
@@ -32,13 +34,16 @@ public class QueryController(PgsqlContext context) : Controller
     /// </summary>
     /// <param name="AuthorId">Author-ID</param>
     /// <response code="200"></response>
+    /// <response code="404">Author not found</response>
     [HttpGet("Mangas/WithAuthorId/{AuthorId}")]
     [ProducesResponseType<Manga[]>(Status200OK, "application/json")]
     public IActionResult GetMangaWithAuthorIds(string AuthorId)
     {
-        return Ok(context.Mangas.Where(m => m.AuthorIds.Contains(AuthorId)));
+        if(context.Authors.Find(AuthorId) is not { } a)
+            return NotFound();
+        return Ok(context.Mangas.Where(m => m.Authors.Contains(a)));
     }
-    
+    /*
     /// <summary>
     /// Returns Link-Information for Link-Id
     /// </summary>
@@ -71,18 +76,21 @@ public class QueryController(PgsqlContext context) : Controller
         if (ret is null)
             return NotFound();
         return Ok(ret);
-    }
+    }*/
     
     /// <summary>
     /// Returns all Manga with Tag
     /// </summary>
     /// <param name="Tag"></param>
     /// <response code="200"></response>
+    /// <response code="404">Tag not found</response>
     [HttpGet("Mangas/WithTag/{Tag}")]
     [ProducesResponseType<Manga[]>(Status200OK, "application/json")]
     public IActionResult GetMangasWithTag(string Tag)
     {
-        return Ok(context.Mangas.Where(m => m.Tags.Contains(Tag)));
+        if(context.Tags.Find(Tag) is not { } t)
+            return NotFound();
+        return Ok(context.Mangas.Where(m => m.MangaTags.Contains(t)));
     }
     
     /// <summary>
diff --git a/API/Controllers/SearchController.cs b/API/Controllers/SearchController.cs
index cc30c6c..8ba0e45 100644
--- a/API/Controllers/SearchController.cs
+++ b/API/Controllers/SearchController.cs
@@ -2,85 +2,53 @@ using API.Schema;
 using API.Schema.Jobs;
 using API.Schema.MangaConnectors;
 using Asp.Versioning;
+using log4net;
 using Microsoft.AspNetCore.Mvc;
 using Microsoft.EntityFrameworkCore;
 using static Microsoft.AspNetCore.Http.StatusCodes;
+// ReSharper disable InconsistentNaming
 
 namespace API.Controllers;
 
 [ApiVersion(2)]
 [ApiController]
 [Route("v{v:apiVersion}/[controller]")]
-public class SearchController(PgsqlContext context) : Controller
+public class SearchController(PgsqlContext context, ILog Log) : Controller
 {
-    
-    /// <summary>
-    /// Initiate a search for a Manga on all Connectors
-    /// </summary>
-    /// <param name="name">Name/Title of the Manga</param>
-    /// <response code="200"></response>
-    /// <response code="500">Error during Database Operation</response>
-    [HttpPost("Name")]
-    [ProducesResponseType<Manga[]>(Status200OK, "application/json")]
-    [ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
-    public IActionResult SearchMangaGlobal([FromBody]string name)
-    {
-        List<(Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)> allManga = new();
-        foreach (MangaConnector contextMangaConnector in context.MangaConnectors.Where(connector => connector.Enabled))
-            allManga.AddRange(contextMangaConnector.GetManga(name));
-        
-        List<Manga> retMangas = new();
-        foreach ((Manga? manga, List<Author>? authors, List<MangaTag>? tags, List<Link>? links, List<MangaAltTitle>? altTitles) in allManga)
-        {
-            try
-            {
-                Manga? add = AddMangaToContext(manga, authors, tags, links, altTitles);
-                if(add is not null)
-                    retMangas.Add(add);
-            }
-            catch (DbUpdateException e)
-            {
-                return StatusCode(500, e);
-            }
-        }
-        return Ok(retMangas.ToArray());
-    }
-    
     /// <summary>
     /// Initiate a search for a Manga on a specific Connector
     /// </summary>
-    /// <param name="MangaConnectorName">Manga-Connector-ID</param>
-    /// <param name="name">Name/Title of the Manga</param>
+    /// <param name="MangaConnectorName"></param>
+    /// <param name="Query"></param>
     /// <response code="200"></response>
     /// <response code="404">MangaConnector with ID not found</response>
     /// <response code="406">MangaConnector with ID is disabled</response>
     /// <response code="500">Error during Database Operation</response>
-    [HttpPost("{MangaConnectorName}")]
+    [HttpPost("{MangaConnectorName}/{Query}")]
     [ProducesResponseType<Manga[]>(Status200OK, "application/json")]
     [ProducesResponseType(Status404NotFound)]
     [ProducesResponseType(Status406NotAcceptable)]
     [ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
-    public IActionResult SearchManga(string MangaConnectorName, [FromBody]string name)
+    public IActionResult SearchManga(string MangaConnectorName, string Query)
     {
-        MangaConnector? connector = context.MangaConnectors.Find(MangaConnectorName);
-        if (connector is null)
+        if(context.MangaConnectors.Find(MangaConnectorName) is not { } connector)
             return NotFound();
         else if (connector.Enabled is false)
-            return StatusCode(406);
+            return StatusCode(Status406NotAcceptable);
         
-        (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)[] mangas = connector.GetManga(name);
+        Manga[] mangas = connector.SearchManga(Query);
         List<Manga> retMangas = new();
-        foreach ((Manga? manga, List<Author>? authors, List<MangaTag>? tags, List<Link>? links, List<MangaAltTitle>? altTitles) in mangas)
+        foreach (Manga manga in mangas)
         {
             try
             {
-                Manga? add = AddMangaToContext(manga, authors, tags, links, altTitles);
-                if(add is not null)
+                if(AddMangaToContext(manga) is { } add)
                     retMangas.Add(add);
             }
             catch (DbUpdateException e)
             {
-                return StatusCode(500, e.Message);
+                Log.Error(e);
+                return StatusCode(Status500InternalServerError, e.Message);
             }
         }
 
@@ -104,98 +72,77 @@ public class SearchController(PgsqlContext context) : Controller
     [ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
     public IActionResult GetMangaFromUrl([FromBody]string url)
     {
-        List<MangaConnector> connectors = context.MangaConnectors.AsEnumerable().Where(c => c.ValidateUrl(url)).ToList();
+        List<MangaConnector> connectors = context.MangaConnectors.AsEnumerable().Where(c => c.UrlMatchesConnector(url)).ToList();
         if (connectors.Count == 0)
             return NotFound();
         else if (connectors.Count > 1)
             return StatusCode(Status300MultipleChoices);
 
-        (Manga manga, List<Author>? authors, List<MangaTag>? tags, List<Link>? links, List<MangaAltTitle>? altTitles)? x = connectors.First().GetMangaFromUrl(url);
-        if (x is null)
+        if(connectors.First().GetMangaFromUrl(url) is not { } manga)
             return BadRequest();
         try
         {
-            Manga? add = AddMangaToContext(x.Value.manga, x.Value.authors, x.Value.tags, x.Value.links, x.Value.altTitles);
-            if (add is not null)
+            if(AddMangaToContext(manga) is { } add)
                 return Ok(add);
             return StatusCode(500);
         }
         catch (DbUpdateException e)
         {
+            Log.Error(e);
             return StatusCode(500, e.Message);
         }
     }
     
-    private Manga? AddMangaToContext(Manga? manga, List<Author>? authors, List<MangaTag>? tags, List<Link>? links,
-        List<MangaAltTitle>? altTitles)
+    private Manga? AddMangaToContext(Manga manga)
     {
-        if (manga is null)
-            return null;
-
         Manga? existing = context.Mangas.Find(manga.MangaId);
         
-        if (tags is not null)
+        IEnumerable<MangaTag> mergedTags = manga.MangaTags.Select(mt =>
         {
-            IEnumerable<MangaTag> mergedTags = tags.Select(mt =>
-            {
-                MangaTag? inDb = context.Tags.Find(mt.Tag);
-                return inDb ?? mt;
-            });
-            manga.MangaTags = mergedTags.ToList();
-            IEnumerable<MangaTag> newTags = manga.MangaTags
-                .Where(mt => !context.Tags.Select(t => t.Tag).Contains(mt.Tag));
-            context.Tags.AddRange(newTags);
-        }
+            MangaTag? inDb = context.Tags.Find(mt.Tag);
+            return inDb ?? mt;
+        });
+        manga.MangaTags = mergedTags.ToList();
 
-        if (authors is not null)
+        IEnumerable<Author> mergedAuthors = manga.Authors.Select(ma =>
         {
-            IEnumerable<Author> mergedAuthors = authors.Select(ma =>
-            {
-                Author? inDb = context.Authors.Find(ma.AuthorId);
-                return inDb ?? ma;
-            });
-            manga.Authors = mergedAuthors.ToList();
-            IEnumerable<Author> newAuthors = manga.Authors
-                .Where(ma => !context.Authors.Select(a => a.AuthorId).Contains(ma.AuthorId));
-            context.Authors.AddRange(newAuthors);
-        }
+            Author? inDb = context.Authors.Find(ma.AuthorId);
+            return inDb ?? ma;
+        });
+        manga.Authors = mergedAuthors.ToList();
 
-        if (links is not null)
+        /*
+        IEnumerable<Link> mergedLinks = manga.Links.Select(ml =>
         {
-            IEnumerable<Link> mergedLinks = links.Select(ml =>
-            {
-                Link? inDb = context.Links.Find(ml.LinkId);
-                return inDb ?? ml;
-            });
-            manga.Links = mergedLinks.ToList();
-            IEnumerable<Link> newLinks = manga.Links
-                .Where(ml => !context.Links.Select(l => l.LinkId).Contains(ml.LinkId));
-            context.Links.AddRange(newLinks);
-        }
+            Link? inDb = context.Links.Find(ml.LinkId);
+            return inDb ?? ml;
+        });
+        manga.Links = mergedLinks.ToList();
 
-        if (altTitles is not null)
+        IEnumerable<MangaAltTitle> mergedAltTitles = manga.AltTitles.Select(mat =>
         {
-            IEnumerable<MangaAltTitle> mergedAltTitles = altTitles.Select(mat =>
+            MangaAltTitle? inDb = context.AltTitles.Find(mat.AltTitleId);
+            return inDb ?? mat;
+        });
+        manga.AltTitles = mergedAltTitles.ToList();
+*/
+        try
+        {
+
+            if (context.Mangas.Find(manga.MangaId) is { } r)
             {
-                MangaAltTitle? inDb = context.AltTitles.Find(mat.AltTitleId);
-                return inDb ?? mat;
-            });
-            manga.AltTitles = mergedAltTitles.ToList();
-            IEnumerable<MangaAltTitle> newAltTitles = manga.AltTitles
-                .Where(mat => !context.AltTitles.Select(at => at.AltTitleId).Contains(mat.AltTitleId));
-            context.AltTitles.AddRange(newAltTitles);
-        }
-        
-        existing?.UpdateWithInfo(manga);
-        if(existing is not null)
-            context.Mangas.Update(existing);
-        else
+                context.Mangas.Remove(r);
+                context.SaveChanges();
+            }
             context.Mangas.Add(manga);
-
-        context.Jobs.Add(new DownloadMangaCoverJob(manga.MangaId));
-        context.Jobs.Add(new RetrieveChaptersJob(0, manga.MangaId));
-
-        context.SaveChanges();
-        return existing ?? manga;
+            context.Jobs.Add(new DownloadMangaCoverJob(manga));
+            context.SaveChanges();
+        }
+        catch (DbUpdateException e)
+        {
+            Log.Error(e);
+            return null;
+        }
+        return manga;
     }
 }
\ No newline at end of file
diff --git a/API/Controllers/SettingsController.cs b/API/Controllers/SettingsController.cs
index d974eef..04da6a5 100644
--- a/API/Controllers/SettingsController.cs
+++ b/API/Controllers/SettingsController.cs
@@ -2,6 +2,7 @@
 using API.Schema;
 using API.Schema.Jobs;
 using Asp.Versioning;
+using log4net;
 using Microsoft.AspNetCore.Mvc;
 using Newtonsoft.Json.Linq;
 using static Microsoft.AspNetCore.Http.StatusCodes;
@@ -11,7 +12,7 @@ namespace API.Controllers;
 [ApiVersion(2)]
 [ApiController]
 [Route("v{v:apiVersion}/[controller]")]
-public class SettingsController(PgsqlContext context) : Controller
+public class SettingsController(PgsqlContext context, ILog Log) : Controller
 {
     /// <summary>
     /// Get all Settings
@@ -252,14 +253,16 @@ public class SettingsController(PgsqlContext context) : Controller
     {
         try
         {
+            Dictionary<Chapter, string> oldPaths = context.Chapters.ToDictionary(c => c, c => c.FullArchiveFilePath);
             TrangaSettings.UpdateChapterNamingScheme(namingScheme);
-            MoveFileOrFolderJob[] newJobs =
-                context.Chapters.Where(c => c.Downloaded).Select(c => c.UpdateArchiveFileName()).Where(x => x != null).ToArray()!;
+            MoveFileOrFolderJob[] newJobs = oldPaths
+                .Select(kv => new MoveFileOrFolderJob(kv.Value, kv.Key.FullArchiveFilePath)).ToArray();
             context.Jobs.AddRange(newJobs);
             return Ok();
         }
         catch (Exception e)
         {
+            Log.Error(e);
             return StatusCode(500, e);
         }
     }
diff --git a/API/Migrations/20250316150158_dev-160325-2.cs b/API/Migrations/20250316150158_dev-160325-2.cs
deleted file mode 100644
index 6adb255..0000000
--- a/API/Migrations/20250316150158_dev-160325-2.cs
+++ /dev/null
@@ -1,42 +0,0 @@
-using Microsoft.EntityFrameworkCore.Migrations;
-
-#nullable disable
-
-namespace API.Migrations
-{
-    /// <inheritdoc />
-    public partial class dev1603252 : Migration
-    {
-        /// <inheritdoc />
-        protected override void Up(MigrationBuilder migrationBuilder)
-        {
-            migrationBuilder.DropForeignKey(
-                name: "FK_Mangas_LocalLibraries_LibraryLocalLibraryId",
-                table: "Mangas");
-
-            migrationBuilder.AddForeignKey(
-                name: "FK_Mangas_LocalLibraries_LibraryLocalLibraryId",
-                table: "Mangas",
-                column: "LibraryLocalLibraryId",
-                principalTable: "LocalLibraries",
-                principalColumn: "LocalLibraryId",
-                onDelete: ReferentialAction.Restrict);
-        }
-
-        /// <inheritdoc />
-        protected override void Down(MigrationBuilder migrationBuilder)
-        {
-            migrationBuilder.DropForeignKey(
-                name: "FK_Mangas_LocalLibraries_LibraryLocalLibraryId",
-                table: "Mangas");
-
-            migrationBuilder.AddForeignKey(
-                name: "FK_Mangas_LocalLibraries_LibraryLocalLibraryId",
-                table: "Mangas",
-                column: "LibraryLocalLibraryId",
-                principalTable: "LocalLibraries",
-                principalColumn: "LocalLibraryId",
-                onDelete: ReferentialAction.Cascade);
-        }
-    }
-}
diff --git a/API/Migrations/20250401001439_dev-010425-1.Designer.cs b/API/Migrations/20250401001439_dev-010425-1.Designer.cs
deleted file mode 100644
index 442a078..0000000
--- a/API/Migrations/20250401001439_dev-010425-1.Designer.cs
+++ /dev/null
@@ -1,827 +0,0 @@
-// <auto-generated />
-using System;
-using System.Collections.Generic;
-using API.Schema;
-using Microsoft.EntityFrameworkCore;
-using Microsoft.EntityFrameworkCore.Infrastructure;
-using Microsoft.EntityFrameworkCore.Migrations;
-using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
-using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
-
-#nullable disable
-
-namespace API.Migrations
-{
-    [DbContext(typeof(PgsqlContext))]
-    [Migration("20250401001439_dev-010425-1")]
-    partial class dev0104251
-    {
-        /// <inheritdoc />
-        protected override void BuildTargetModel(ModelBuilder modelBuilder)
-        {
-#pragma warning disable 612, 618
-            modelBuilder
-                .HasAnnotation("ProductVersion", "9.0.3")
-                .HasAnnotation("Relational:MaxIdentifierLength", 63);
-
-            NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "hstore");
-            NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
-
-            modelBuilder.Entity("API.Schema.Author", b =>
-                {
-                    b.Property<string>("AuthorId")
-                        .HasMaxLength(64)
-                        .HasColumnType("character varying(64)");
-
-                    b.Property<string>("AuthorName")
-                        .IsRequired()
-                        .HasMaxLength(128)
-                        .HasColumnType("character varying(128)");
-
-                    b.HasKey("AuthorId");
-
-                    b.ToTable("Authors");
-                });
-
-            modelBuilder.Entity("API.Schema.Chapter", b =>
-                {
-                    b.Property<string>("ChapterId")
-                        .HasMaxLength(64)
-                        .HasColumnType("character varying(64)");
-
-                    b.Property<string>("ChapterNumber")
-                        .IsRequired()
-                        .HasMaxLength(10)
-                        .HasColumnType("character varying(10)");
-
-                    b.Property<bool>("Downloaded")
-                        .HasColumnType("boolean");
-
-                    b.Property<string>("FileName")
-                        .IsRequired()
-                        .HasMaxLength(256)
-                        .HasColumnType("character varying(256)");
-
-                    b.Property<string>("ParentMangaId")
-                        .IsRequired()
-                        .HasMaxLength(64)
-                        .HasColumnType("character varying(64)");
-
-                    b.Property<string>("Title")
-                        .HasMaxLength(256)
-                        .HasColumnType("character varying(256)");
-
-                    b.Property<string>("Url")
-                        .IsRequired()
-                        .HasMaxLength(2048)
-                        .HasColumnType("character varying(2048)");
-
-                    b.Property<int?>("VolumeNumber")
-                        .HasColumnType("integer");
-
-                    b.HasKey("ChapterId");
-
-                    b.HasIndex("ParentMangaId");
-
-                    b.ToTable("Chapters");
-                });
-
-            modelBuilder.Entity("API.Schema.Jobs.Job", b =>
-                {
-                    b.Property<string>("JobId")
-                        .HasMaxLength(64)
-                        .HasColumnType("character varying(64)");
-
-                    b.PrimitiveCollection<string[]>("DependsOnJobsIds")
-                        .HasMaxLength(64)
-                        .HasColumnType("text[]");
-
-                    b.Property<bool>("Enabled")
-                        .HasColumnType("boolean");
-
-                    b.Property<byte>("JobType")
-                        .HasColumnType("smallint");
-
-                    b.Property<DateTime>("LastExecution")
-                        .HasColumnType("timestamp with time zone");
-
-                    b.Property<string>("ParentJobId")
-                        .HasMaxLength(64)
-                        .HasColumnType("character varying(64)");
-
-                    b.Property<decimal>("RecurrenceMs")
-                        .HasColumnType("numeric(20,0)");
-
-                    b.Property<byte>("state")
-                        .HasColumnType("smallint");
-
-                    b.HasKey("JobId");
-
-                    b.HasIndex("ParentJobId");
-
-                    b.ToTable("Jobs");
-
-                    b.HasDiscriminator<byte>("JobType");
-
-                    b.UseTphMappingStrategy();
-                });
-
-            modelBuilder.Entity("API.Schema.LibraryConnectors.LibraryConnector", b =>
-                {
-                    b.Property<string>("LibraryConnectorId")
-                        .HasMaxLength(64)
-                        .HasColumnType("character varying(64)");
-
-                    b.Property<string>("Auth")
-                        .IsRequired()
-                        .HasMaxLength(256)
-                        .HasColumnType("character varying(256)");
-
-                    b.Property<string>("BaseUrl")
-                        .IsRequired()
-                        .HasMaxLength(256)
-                        .HasColumnType("character varying(256)");
-
-                    b.Property<byte>("LibraryType")
-                        .HasColumnType("smallint");
-
-                    b.HasKey("LibraryConnectorId");
-
-                    b.ToTable("LibraryConnectors");
-
-                    b.HasDiscriminator<byte>("LibraryType");
-
-                    b.UseTphMappingStrategy();
-                });
-
-            modelBuilder.Entity("API.Schema.Link", b =>
-                {
-                    b.Property<string>("LinkId")
-                        .HasMaxLength(64)
-                        .HasColumnType("character varying(64)");
-
-                    b.Property<string>("LinkProvider")
-                        .IsRequired()
-                        .HasMaxLength(64)
-                        .HasColumnType("character varying(64)");
-
-                    b.Property<string>("LinkUrl")
-                        .IsRequired()
-                        .HasMaxLength(2048)
-                        .HasColumnType("character varying(2048)");
-
-                    b.Property<string>("MangaId")
-                        .HasColumnType("character varying(64)");
-
-                    b.HasKey("LinkId");
-
-                    b.HasIndex("MangaId");
-
-                    b.ToTable("Links");
-                });
-
-            modelBuilder.Entity("API.Schema.LocalLibrary", b =>
-                {
-                    b.Property<string>("LocalLibraryId")
-                        .HasMaxLength(64)
-                        .HasColumnType("character varying(64)");
-
-                    b.Property<string>("BasePath")
-                        .IsRequired()
-                        .HasMaxLength(256)
-                        .HasColumnType("character varying(256)");
-
-                    b.Property<string>("LibraryName")
-                        .IsRequired()
-                        .HasMaxLength(512)
-                        .HasColumnType("character varying(512)");
-
-                    b.HasKey("LocalLibraryId");
-
-                    b.ToTable("LocalLibraries");
-                });
-
-            modelBuilder.Entity("API.Schema.Manga", b =>
-                {
-                    b.Property<string>("MangaId")
-                        .HasMaxLength(64)
-                        .HasColumnType("character varying(64)");
-
-                    b.Property<string>("CoverFileNameInCache")
-                        .HasColumnType("text");
-
-                    b.Property<string>("CoverUrl")
-                        .IsRequired()
-                        .HasColumnType("text");
-
-                    b.Property<string>("Description")
-                        .IsRequired()
-                        .HasColumnType("text");
-
-                    b.Property<string>("DirectoryName")
-                        .IsRequired()
-                        .HasMaxLength(256)
-                        .HasColumnType("character varying(256)");
-
-                    b.Property<string>("IdOnConnectorSite")
-                        .IsRequired()
-                        .HasMaxLength(128)
-                        .HasColumnType("character varying(128)");
-
-                    b.Property<float>("IgnoreChapterBefore")
-                        .HasColumnType("real");
-
-                    b.Property<string>("LibraryLocalLibraryId")
-                        .HasColumnType("character varying(64)");
-
-                    b.Property<string>("MangaConnectorId")
-                        .IsRequired()
-                        .HasMaxLength(64)
-                        .HasColumnType("character varying(64)");
-
-                    b.Property<string>("Name")
-                        .IsRequired()
-                        .HasMaxLength(256)
-                        .HasColumnType("character varying(256)");
-
-                    b.Property<string>("OriginalLanguage")
-                        .HasMaxLength(8)
-                        .HasColumnType("character varying(8)");
-
-                    b.Property<byte>("ReleaseStatus")
-                        .HasColumnType("smallint");
-
-                    b.Property<string>("WebsiteUrl")
-                        .IsRequired()
-                        .HasMaxLength(256)
-                        .HasColumnType("character varying(256)");
-
-                    b.Property<long>("Year")
-                        .HasColumnType("bigint");
-
-                    b.HasKey("MangaId");
-
-                    b.HasIndex("LibraryLocalLibraryId");
-
-                    b.HasIndex("MangaConnectorId");
-
-                    b.ToTable("Mangas");
-                });
-
-            modelBuilder.Entity("API.Schema.MangaAltTitle", b =>
-                {
-                    b.Property<string>("AltTitleId")
-                        .HasMaxLength(64)
-                        .HasColumnType("character varying(64)");
-
-                    b.Property<string>("Language")
-                        .IsRequired()
-                        .HasMaxLength(8)
-                        .HasColumnType("character varying(8)");
-
-                    b.Property<string>("MangaId")
-                        .HasColumnType("character varying(64)");
-
-                    b.Property<string>("Title")
-                        .IsRequired()
-                        .HasMaxLength(256)
-                        .HasColumnType("character varying(256)");
-
-                    b.HasKey("AltTitleId");
-
-                    b.HasIndex("MangaId");
-
-                    b.ToTable("AltTitles");
-                });
-
-            modelBuilder.Entity("API.Schema.MangaConnectors.MangaConnector", b =>
-                {
-                    b.Property<string>("Name")
-                        .HasMaxLength(32)
-                        .HasColumnType("character varying(32)");
-
-                    b.PrimitiveCollection<string[]>("BaseUris")
-                        .IsRequired()
-                        .HasMaxLength(256)
-                        .HasColumnType("text[]");
-
-                    b.Property<bool>("Enabled")
-                        .HasColumnType("boolean");
-
-                    b.Property<string>("IconUrl")
-                        .IsRequired()
-                        .HasMaxLength(2048)
-                        .HasColumnType("character varying(2048)");
-
-                    b.PrimitiveCollection<string[]>("SupportedLanguages")
-                        .IsRequired()
-                        .HasMaxLength(8)
-                        .HasColumnType("text[]");
-
-                    b.HasKey("Name");
-
-                    b.ToTable("MangaConnectors");
-
-                    b.HasDiscriminator<string>("Name").HasValue("MangaConnector");
-
-                    b.UseTphMappingStrategy();
-                });
-
-            modelBuilder.Entity("API.Schema.MangaTag", b =>
-                {
-                    b.Property<string>("Tag")
-                        .HasMaxLength(64)
-                        .HasColumnType("character varying(64)");
-
-                    b.HasKey("Tag");
-
-                    b.ToTable("Tags");
-                });
-
-            modelBuilder.Entity("API.Schema.Notification", b =>
-                {
-                    b.Property<string>("NotificationId")
-                        .HasMaxLength(64)
-                        .HasColumnType("character varying(64)");
-
-                    b.Property<DateTime>("Date")
-                        .HasColumnType("timestamp with time zone");
-
-                    b.Property<string>("Message")
-                        .IsRequired()
-                        .HasMaxLength(512)
-                        .HasColumnType("character varying(512)");
-
-                    b.Property<string>("Title")
-                        .IsRequired()
-                        .HasMaxLength(128)
-                        .HasColumnType("character varying(128)");
-
-                    b.Property<byte>("Urgency")
-                        .HasColumnType("smallint");
-
-                    b.HasKey("NotificationId");
-
-                    b.ToTable("Notifications");
-                });
-
-            modelBuilder.Entity("API.Schema.NotificationConnectors.NotificationConnector", b =>
-                {
-                    b.Property<string>("Name")
-                        .HasMaxLength(64)
-                        .HasColumnType("character varying(64)");
-
-                    b.Property<string>("Body")
-                        .IsRequired()
-                        .HasMaxLength(4096)
-                        .HasColumnType("character varying(4096)");
-
-                    b.Property<Dictionary<string, string>>("Headers")
-                        .IsRequired()
-                        .HasColumnType("hstore");
-
-                    b.Property<string>("HttpMethod")
-                        .IsRequired()
-                        .HasMaxLength(8)
-                        .HasColumnType("character varying(8)");
-
-                    b.Property<string>("Url")
-                        .IsRequired()
-                        .HasMaxLength(2048)
-                        .HasColumnType("character varying(2048)");
-
-                    b.HasKey("Name");
-
-                    b.ToTable("NotificationConnectors");
-                });
-
-            modelBuilder.Entity("AuthorManga", b =>
-                {
-                    b.Property<string>("AuthorsAuthorId")
-                        .HasColumnType("character varying(64)");
-
-                    b.Property<string>("MangaId")
-                        .HasColumnType("character varying(64)");
-
-                    b.HasKey("AuthorsAuthorId", "MangaId");
-
-                    b.HasIndex("MangaId");
-
-                    b.ToTable("AuthorManga");
-                });
-
-            modelBuilder.Entity("JobJob", b =>
-                {
-                    b.Property<string>("DependsOnJobsJobId")
-                        .HasColumnType("character varying(64)");
-
-                    b.Property<string>("JobId")
-                        .HasColumnType("character varying(64)");
-
-                    b.HasKey("DependsOnJobsJobId", "JobId");
-
-                    b.HasIndex("JobId");
-
-                    b.ToTable("JobJob");
-                });
-
-            modelBuilder.Entity("MangaMangaTag", b =>
-                {
-                    b.Property<string>("MangaId")
-                        .HasColumnType("character varying(64)");
-
-                    b.Property<string>("MangaTagsTag")
-                        .HasColumnType("character varying(64)");
-
-                    b.HasKey("MangaId", "MangaTagsTag");
-
-                    b.HasIndex("MangaTagsTag");
-
-                    b.ToTable("MangaMangaTag");
-                });
-
-            modelBuilder.Entity("API.Schema.Jobs.DownloadAvailableChaptersJob", b =>
-                {
-                    b.HasBaseType("API.Schema.Jobs.Job");
-
-                    b.Property<string>("MangaId")
-                        .IsRequired()
-                        .HasMaxLength(64)
-                        .HasColumnType("character varying(64)");
-
-                    b.HasIndex("MangaId");
-
-                    b.ToTable("Jobs", t =>
-                        {
-                            t.Property("MangaId")
-                                .HasColumnName("DownloadAvailableChaptersJob_MangaId");
-                        });
-
-                    b.HasDiscriminator().HasValue((byte)1);
-                });
-
-            modelBuilder.Entity("API.Schema.Jobs.DownloadMangaCoverJob", b =>
-                {
-                    b.HasBaseType("API.Schema.Jobs.Job");
-
-                    b.Property<string>("MangaId")
-                        .IsRequired()
-                        .HasMaxLength(64)
-                        .HasColumnType("character varying(64)");
-
-                    b.HasIndex("MangaId");
-
-                    b.HasDiscriminator().HasValue((byte)4);
-                });
-
-            modelBuilder.Entity("API.Schema.Jobs.DownloadSingleChapterJob", b =>
-                {
-                    b.HasBaseType("API.Schema.Jobs.Job");
-
-                    b.Property<string>("ChapterId")
-                        .IsRequired()
-                        .HasMaxLength(64)
-                        .HasColumnType("character varying(64)");
-
-                    b.HasIndex("ChapterId");
-
-                    b.HasDiscriminator().HasValue((byte)0);
-                });
-
-            modelBuilder.Entity("API.Schema.Jobs.MoveFileOrFolderJob", b =>
-                {
-                    b.HasBaseType("API.Schema.Jobs.Job");
-
-                    b.Property<string>("FromLocation")
-                        .IsRequired()
-                        .HasMaxLength(256)
-                        .HasColumnType("character varying(256)");
-
-                    b.Property<string>("ToLocation")
-                        .IsRequired()
-                        .HasMaxLength(256)
-                        .HasColumnType("character varying(256)");
-
-                    b.HasDiscriminator().HasValue((byte)3);
-                });
-
-            modelBuilder.Entity("API.Schema.Jobs.RetrieveChaptersJob", b =>
-                {
-                    b.HasBaseType("API.Schema.Jobs.Job");
-
-                    b.Property<string>("MangaId")
-                        .IsRequired()
-                        .HasMaxLength(64)
-                        .HasColumnType("character varying(64)");
-
-                    b.HasIndex("MangaId");
-
-                    b.ToTable("Jobs", t =>
-                        {
-                            t.Property("MangaId")
-                                .HasColumnName("RetrieveChaptersJob_MangaId");
-                        });
-
-                    b.HasDiscriminator().HasValue((byte)5);
-                });
-
-            modelBuilder.Entity("API.Schema.Jobs.UpdateFilesDownloadedJob", b =>
-                {
-                    b.HasBaseType("API.Schema.Jobs.Job");
-
-                    b.Property<string>("MangaId")
-                        .IsRequired()
-                        .HasMaxLength(64)
-                        .HasColumnType("character varying(64)");
-
-                    b.HasIndex("MangaId");
-
-                    b.ToTable("Jobs", t =>
-                        {
-                            t.Property("MangaId")
-                                .HasColumnName("UpdateFilesDownloadedJob_MangaId");
-                        });
-
-                    b.HasDiscriminator().HasValue((byte)6);
-                });
-
-            modelBuilder.Entity("API.Schema.Jobs.UpdateMetadataJob", b =>
-                {
-                    b.HasBaseType("API.Schema.Jobs.Job");
-
-                    b.Property<string>("MangaId")
-                        .IsRequired()
-                        .HasMaxLength(64)
-                        .HasColumnType("character varying(64)");
-
-                    b.HasIndex("MangaId");
-
-                    b.ToTable("Jobs", t =>
-                        {
-                            t.Property("MangaId")
-                                .HasColumnName("UpdateMetadataJob_MangaId");
-                        });
-
-                    b.HasDiscriminator().HasValue((byte)2);
-                });
-
-            modelBuilder.Entity("API.Schema.LibraryConnectors.Kavita", b =>
-                {
-                    b.HasBaseType("API.Schema.LibraryConnectors.LibraryConnector");
-
-                    b.HasDiscriminator().HasValue((byte)1);
-                });
-
-            modelBuilder.Entity("API.Schema.LibraryConnectors.Komga", b =>
-                {
-                    b.HasBaseType("API.Schema.LibraryConnectors.LibraryConnector");
-
-                    b.HasDiscriminator().HasValue((byte)0);
-                });
-
-            modelBuilder.Entity("API.Schema.MangaConnectors.AsuraToon", b =>
-                {
-                    b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
-
-                    b.HasDiscriminator().HasValue("AsuraToon");
-                });
-
-            modelBuilder.Entity("API.Schema.MangaConnectors.Bato", b =>
-                {
-                    b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
-
-                    b.HasDiscriminator().HasValue("Bato");
-                });
-
-            modelBuilder.Entity("API.Schema.MangaConnectors.Global", b =>
-                {
-                    b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
-
-                    b.HasDiscriminator().HasValue("Global");
-                });
-
-            modelBuilder.Entity("API.Schema.MangaConnectors.MangaDex", b =>
-                {
-                    b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
-
-                    b.HasDiscriminator().HasValue("MangaDex");
-                });
-
-            modelBuilder.Entity("API.Schema.MangaConnectors.MangaHere", b =>
-                {
-                    b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
-
-                    b.HasDiscriminator().HasValue("MangaHere");
-                });
-
-            modelBuilder.Entity("API.Schema.MangaConnectors.MangaKatana", b =>
-                {
-                    b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
-
-                    b.HasDiscriminator().HasValue("MangaKatana");
-                });
-
-            modelBuilder.Entity("API.Schema.MangaConnectors.Manganato", b =>
-                {
-                    b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
-
-                    b.HasDiscriminator().HasValue("Manganato");
-                });
-
-            modelBuilder.Entity("API.Schema.MangaConnectors.Mangaworld", b =>
-                {
-                    b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
-
-                    b.HasDiscriminator().HasValue("Mangaworld");
-                });
-
-            modelBuilder.Entity("API.Schema.MangaConnectors.ManhuaPlus", b =>
-                {
-                    b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
-
-                    b.HasDiscriminator().HasValue("ManhuaPlus");
-                });
-
-            modelBuilder.Entity("API.Schema.MangaConnectors.Weebcentral", b =>
-                {
-                    b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
-
-                    b.HasDiscriminator().HasValue("Weebcentral");
-                });
-
-            modelBuilder.Entity("API.Schema.Chapter", b =>
-                {
-                    b.HasOne("API.Schema.Manga", "ParentManga")
-                        .WithMany()
-                        .HasForeignKey("ParentMangaId")
-                        .OnDelete(DeleteBehavior.Cascade)
-                        .IsRequired();
-
-                    b.Navigation("ParentManga");
-                });
-
-            modelBuilder.Entity("API.Schema.Jobs.Job", b =>
-                {
-                    b.HasOne("API.Schema.Jobs.Job", "ParentJob")
-                        .WithMany()
-                        .HasForeignKey("ParentJobId")
-                        .OnDelete(DeleteBehavior.Cascade);
-
-                    b.Navigation("ParentJob");
-                });
-
-            modelBuilder.Entity("API.Schema.Link", b =>
-                {
-                    b.HasOne("API.Schema.Manga", null)
-                        .WithMany("Links")
-                        .HasForeignKey("MangaId")
-                        .OnDelete(DeleteBehavior.Cascade);
-                });
-
-            modelBuilder.Entity("API.Schema.Manga", b =>
-                {
-                    b.HasOne("API.Schema.LocalLibrary", "Library")
-                        .WithMany()
-                        .HasForeignKey("LibraryLocalLibraryId")
-                        .OnDelete(DeleteBehavior.Restrict);
-
-                    b.HasOne("API.Schema.MangaConnectors.MangaConnector", "MangaConnector")
-                        .WithMany()
-                        .HasForeignKey("MangaConnectorId")
-                        .OnDelete(DeleteBehavior.Cascade)
-                        .IsRequired();
-
-                    b.Navigation("Library");
-
-                    b.Navigation("MangaConnector");
-                });
-
-            modelBuilder.Entity("API.Schema.MangaAltTitle", b =>
-                {
-                    b.HasOne("API.Schema.Manga", null)
-                        .WithMany("AltTitles")
-                        .HasForeignKey("MangaId")
-                        .OnDelete(DeleteBehavior.Cascade);
-                });
-
-            modelBuilder.Entity("AuthorManga", b =>
-                {
-                    b.HasOne("API.Schema.Author", null)
-                        .WithMany()
-                        .HasForeignKey("AuthorsAuthorId")
-                        .OnDelete(DeleteBehavior.Cascade)
-                        .IsRequired();
-
-                    b.HasOne("API.Schema.Manga", null)
-                        .WithMany()
-                        .HasForeignKey("MangaId")
-                        .OnDelete(DeleteBehavior.Cascade)
-                        .IsRequired();
-                });
-
-            modelBuilder.Entity("JobJob", b =>
-                {
-                    b.HasOne("API.Schema.Jobs.Job", null)
-                        .WithMany()
-                        .HasForeignKey("DependsOnJobsJobId")
-                        .OnDelete(DeleteBehavior.Cascade)
-                        .IsRequired();
-
-                    b.HasOne("API.Schema.Jobs.Job", null)
-                        .WithMany()
-                        .HasForeignKey("JobId")
-                        .OnDelete(DeleteBehavior.Cascade)
-                        .IsRequired();
-                });
-
-            modelBuilder.Entity("MangaMangaTag", b =>
-                {
-                    b.HasOne("API.Schema.Manga", null)
-                        .WithMany()
-                        .HasForeignKey("MangaId")
-                        .OnDelete(DeleteBehavior.Cascade)
-                        .IsRequired();
-
-                    b.HasOne("API.Schema.MangaTag", null)
-                        .WithMany()
-                        .HasForeignKey("MangaTagsTag")
-                        .OnDelete(DeleteBehavior.Cascade)
-                        .IsRequired();
-                });
-
-            modelBuilder.Entity("API.Schema.Jobs.DownloadAvailableChaptersJob", b =>
-                {
-                    b.HasOne("API.Schema.Manga", "Manga")
-                        .WithMany()
-                        .HasForeignKey("MangaId")
-                        .OnDelete(DeleteBehavior.Cascade)
-                        .IsRequired();
-
-                    b.Navigation("Manga");
-                });
-
-            modelBuilder.Entity("API.Schema.Jobs.DownloadMangaCoverJob", b =>
-                {
-                    b.HasOne("API.Schema.Manga", "Manga")
-                        .WithMany()
-                        .HasForeignKey("MangaId")
-                        .OnDelete(DeleteBehavior.Cascade)
-                        .IsRequired();
-
-                    b.Navigation("Manga");
-                });
-
-            modelBuilder.Entity("API.Schema.Jobs.DownloadSingleChapterJob", b =>
-                {
-                    b.HasOne("API.Schema.Chapter", "Chapter")
-                        .WithMany()
-                        .HasForeignKey("ChapterId")
-                        .OnDelete(DeleteBehavior.Cascade)
-                        .IsRequired();
-
-                    b.Navigation("Chapter");
-                });
-
-            modelBuilder.Entity("API.Schema.Jobs.RetrieveChaptersJob", b =>
-                {
-                    b.HasOne("API.Schema.Manga", "Manga")
-                        .WithMany()
-                        .HasForeignKey("MangaId")
-                        .OnDelete(DeleteBehavior.Cascade)
-                        .IsRequired();
-
-                    b.Navigation("Manga");
-                });
-
-            modelBuilder.Entity("API.Schema.Jobs.UpdateFilesDownloadedJob", b =>
-                {
-                    b.HasOne("API.Schema.Manga", "Manga")
-                        .WithMany()
-                        .HasForeignKey("MangaId")
-                        .OnDelete(DeleteBehavior.Cascade)
-                        .IsRequired();
-
-                    b.Navigation("Manga");
-                });
-
-            modelBuilder.Entity("API.Schema.Jobs.UpdateMetadataJob", b =>
-                {
-                    b.HasOne("API.Schema.Manga", "Manga")
-                        .WithMany()
-                        .HasForeignKey("MangaId")
-                        .OnDelete(DeleteBehavior.Cascade)
-                        .IsRequired();
-
-                    b.Navigation("Manga");
-                });
-
-            modelBuilder.Entity("API.Schema.Manga", b =>
-                {
-                    b.Navigation("AltTitles");
-
-                    b.Navigation("Links");
-                });
-#pragma warning restore 612, 618
-        }
-    }
-}
diff --git a/API/Migrations/20250401162026_dev-010425-2-Longer_Var_Chars.cs b/API/Migrations/20250401162026_dev-010425-2-Longer_Var_Chars.cs
deleted file mode 100644
index 2af08b9..0000000
--- a/API/Migrations/20250401162026_dev-010425-2-Longer_Var_Chars.cs
+++ /dev/null
@@ -1,98 +0,0 @@
-using Microsoft.EntityFrameworkCore.Migrations;
-
-#nullable disable
-
-namespace API.Migrations
-{
-    /// <inheritdoc />
-    public partial class dev0104252Longer_Var_Chars : Migration
-    {
-        /// <inheritdoc />
-        protected override void Up(MigrationBuilder migrationBuilder)
-        {
-            migrationBuilder.AlterColumn<string>(
-                name: "WebsiteUrl",
-                table: "Mangas",
-                type: "character varying(512)",
-                maxLength: 512,
-                nullable: false,
-                oldClrType: typeof(string),
-                oldType: "character varying(256)",
-                oldMaxLength: 256);
-
-            migrationBuilder.AlterColumn<string>(
-                name: "Name",
-                table: "Mangas",
-                type: "character varying(512)",
-                maxLength: 512,
-                nullable: false,
-                oldClrType: typeof(string),
-                oldType: "character varying(256)",
-                oldMaxLength: 256);
-
-            migrationBuilder.AlterColumn<string>(
-                name: "IdOnConnectorSite",
-                table: "Mangas",
-                type: "character varying(256)",
-                maxLength: 256,
-                nullable: false,
-                oldClrType: typeof(string),
-                oldType: "character varying(128)",
-                oldMaxLength: 128);
-
-            migrationBuilder.AlterColumn<string>(
-                name: "DirectoryName",
-                table: "Mangas",
-                type: "character varying(1024)",
-                maxLength: 1024,
-                nullable: false,
-                oldClrType: typeof(string),
-                oldType: "character varying(256)",
-                oldMaxLength: 256);
-        }
-
-        /// <inheritdoc />
-        protected override void Down(MigrationBuilder migrationBuilder)
-        {
-            migrationBuilder.AlterColumn<string>(
-                name: "WebsiteUrl",
-                table: "Mangas",
-                type: "character varying(256)",
-                maxLength: 256,
-                nullable: false,
-                oldClrType: typeof(string),
-                oldType: "character varying(512)",
-                oldMaxLength: 512);
-
-            migrationBuilder.AlterColumn<string>(
-                name: "Name",
-                table: "Mangas",
-                type: "character varying(256)",
-                maxLength: 256,
-                nullable: false,
-                oldClrType: typeof(string),
-                oldType: "character varying(512)",
-                oldMaxLength: 512);
-
-            migrationBuilder.AlterColumn<string>(
-                name: "IdOnConnectorSite",
-                table: "Mangas",
-                type: "character varying(128)",
-                maxLength: 128,
-                nullable: false,
-                oldClrType: typeof(string),
-                oldType: "character varying(256)",
-                oldMaxLength: 256);
-
-            migrationBuilder.AlterColumn<string>(
-                name: "DirectoryName",
-                table: "Mangas",
-                type: "character varying(256)",
-                maxLength: 256,
-                nullable: false,
-                oldClrType: typeof(string),
-                oldType: "character varying(1024)",
-                oldMaxLength: 1024);
-        }
-    }
-}
diff --git a/API/Migrations/20250402001438_dev-010425-4.Designer.cs b/API/Migrations/20250402001438_dev-010425-4.Designer.cs
deleted file mode 100644
index 88957a4..0000000
--- a/API/Migrations/20250402001438_dev-010425-4.Designer.cs
+++ /dev/null
@@ -1,762 +0,0 @@
-// <auto-generated />
-using System;
-using System.Collections.Generic;
-using API.Schema;
-using Microsoft.EntityFrameworkCore;
-using Microsoft.EntityFrameworkCore.Infrastructure;
-using Microsoft.EntityFrameworkCore.Migrations;
-using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
-using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
-
-#nullable disable
-
-namespace API.Migrations
-{
-    [DbContext(typeof(PgsqlContext))]
-    [Migration("20250402001438_dev-010425-4")]
-    partial class dev0104254
-    {
-        /// <inheritdoc />
-        protected override void BuildTargetModel(ModelBuilder modelBuilder)
-        {
-#pragma warning disable 612, 618
-            modelBuilder
-                .HasAnnotation("ProductVersion", "9.0.3")
-                .HasAnnotation("Relational:MaxIdentifierLength", 63);
-
-            NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "hstore");
-            NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
-
-            modelBuilder.Entity("API.Schema.Author", b =>
-                {
-                    b.Property<string>("AuthorId")
-                        .HasMaxLength(64)
-                        .HasColumnType("character varying(64)");
-
-                    b.Property<string>("AuthorName")
-                        .IsRequired()
-                        .HasMaxLength(128)
-                        .HasColumnType("character varying(128)");
-
-                    b.HasKey("AuthorId");
-
-                    b.ToTable("Authors");
-                });
-
-            modelBuilder.Entity("API.Schema.Chapter", b =>
-                {
-                    b.Property<string>("ChapterId")
-                        .HasMaxLength(64)
-                        .HasColumnType("character varying(64)");
-
-                    b.Property<string>("ChapterNumber")
-                        .IsRequired()
-                        .HasMaxLength(10)
-                        .HasColumnType("character varying(10)");
-
-                    b.Property<bool>("Downloaded")
-                        .HasColumnType("boolean");
-
-                    b.Property<string>("FileName")
-                        .IsRequired()
-                        .HasMaxLength(256)
-                        .HasColumnType("character varying(256)");
-
-                    b.Property<string>("ParentMangaId")
-                        .IsRequired()
-                        .HasMaxLength(64)
-                        .HasColumnType("character varying(64)");
-
-                    b.Property<string>("Title")
-                        .HasMaxLength(256)
-                        .HasColumnType("character varying(256)");
-
-                    b.Property<string>("Url")
-                        .IsRequired()
-                        .HasMaxLength(2048)
-                        .HasColumnType("character varying(2048)");
-
-                    b.Property<int?>("VolumeNumber")
-                        .HasColumnType("integer");
-
-                    b.HasKey("ChapterId");
-
-                    b.HasIndex("ParentMangaId");
-
-                    b.ToTable("Chapters");
-                });
-
-            modelBuilder.Entity("API.Schema.Jobs.Job", b =>
-                {
-                    b.Property<string>("JobId")
-                        .HasMaxLength(64)
-                        .HasColumnType("character varying(64)");
-
-                    b.PrimitiveCollection<string[]>("DependsOnJobsIds")
-                        .HasMaxLength(64)
-                        .HasColumnType("text[]");
-
-                    b.Property<bool>("Enabled")
-                        .HasColumnType("boolean");
-
-                    b.Property<byte>("JobType")
-                        .HasColumnType("smallint");
-
-                    b.Property<DateTime>("LastExecution")
-                        .HasColumnType("timestamp with time zone");
-
-                    b.Property<string>("ParentJobId")
-                        .HasMaxLength(64)
-                        .HasColumnType("character varying(64)");
-
-                    b.Property<decimal>("RecurrenceMs")
-                        .HasColumnType("numeric(20,0)");
-
-                    b.Property<byte>("state")
-                        .HasColumnType("smallint");
-
-                    b.HasKey("JobId");
-
-                    b.HasIndex("ParentJobId");
-
-                    b.ToTable("Jobs");
-
-                    b.HasDiscriminator<byte>("JobType");
-
-                    b.UseTphMappingStrategy();
-                });
-
-            modelBuilder.Entity("API.Schema.LibraryConnectors.LibraryConnector", b =>
-                {
-                    b.Property<string>("LibraryConnectorId")
-                        .HasMaxLength(64)
-                        .HasColumnType("character varying(64)");
-
-                    b.Property<string>("Auth")
-                        .IsRequired()
-                        .HasMaxLength(256)
-                        .HasColumnType("character varying(256)");
-
-                    b.Property<string>("BaseUrl")
-                        .IsRequired()
-                        .HasMaxLength(256)
-                        .HasColumnType("character varying(256)");
-
-                    b.Property<byte>("LibraryType")
-                        .HasColumnType("smallint");
-
-                    b.HasKey("LibraryConnectorId");
-
-                    b.ToTable("LibraryConnectors");
-
-                    b.HasDiscriminator<byte>("LibraryType");
-
-                    b.UseTphMappingStrategy();
-                });
-
-            modelBuilder.Entity("API.Schema.Link", b =>
-                {
-                    b.Property<string>("LinkId")
-                        .HasMaxLength(64)
-                        .HasColumnType("character varying(64)");
-
-                    b.Property<string>("LinkProvider")
-                        .IsRequired()
-                        .HasMaxLength(64)
-                        .HasColumnType("character varying(64)");
-
-                    b.Property<string>("LinkUrl")
-                        .IsRequired()
-                        .HasMaxLength(2048)
-                        .HasColumnType("character varying(2048)");
-
-                    b.Property<string>("MangaId")
-                        .HasColumnType("character varying(64)");
-
-                    b.HasKey("LinkId");
-
-                    b.HasIndex("MangaId");
-
-                    b.ToTable("Links");
-                });
-
-            modelBuilder.Entity("API.Schema.LocalLibrary", b =>
-                {
-                    b.Property<string>("LocalLibraryId")
-                        .HasMaxLength(64)
-                        .HasColumnType("character varying(64)");
-
-                    b.Property<string>("BasePath")
-                        .IsRequired()
-                        .HasMaxLength(256)
-                        .HasColumnType("character varying(256)");
-
-                    b.Property<string>("LibraryName")
-                        .IsRequired()
-                        .HasMaxLength(512)
-                        .HasColumnType("character varying(512)");
-
-                    b.HasKey("LocalLibraryId");
-
-                    b.ToTable("LocalLibraries");
-                });
-
-            modelBuilder.Entity("API.Schema.Manga", b =>
-                {
-                    b.Property<string>("MangaId")
-                        .HasMaxLength(64)
-                        .HasColumnType("character varying(64)");
-
-                    b.Property<string>("CoverFileNameInCache")
-                        .HasColumnType("text");
-
-                    b.Property<string>("CoverUrl")
-                        .IsRequired()
-                        .HasColumnType("text");
-
-                    b.Property<string>("Description")
-                        .IsRequired()
-                        .HasColumnType("text");
-
-                    b.Property<string>("DirectoryName")
-                        .IsRequired()
-                        .HasMaxLength(1024)
-                        .HasColumnType("character varying(1024)");
-
-                    b.Property<string>("IdOnConnectorSite")
-                        .IsRequired()
-                        .HasMaxLength(256)
-                        .HasColumnType("character varying(256)");
-
-                    b.Property<float>("IgnoreChapterBefore")
-                        .HasColumnType("real");
-
-                    b.Property<string>("LibraryLocalLibraryId")
-                        .HasColumnType("character varying(64)");
-
-                    b.Property<string>("MangaConnectorId")
-                        .IsRequired()
-                        .HasMaxLength(64)
-                        .HasColumnType("character varying(64)");
-
-                    b.Property<string>("Name")
-                        .IsRequired()
-                        .HasMaxLength(512)
-                        .HasColumnType("character varying(512)");
-
-                    b.Property<string>("OriginalLanguage")
-                        .HasMaxLength(8)
-                        .HasColumnType("character varying(8)");
-
-                    b.Property<byte>("ReleaseStatus")
-                        .HasColumnType("smallint");
-
-                    b.Property<string>("WebsiteUrl")
-                        .IsRequired()
-                        .HasMaxLength(512)
-                        .HasColumnType("character varying(512)");
-
-                    b.Property<long>("Year")
-                        .HasColumnType("bigint");
-
-                    b.HasKey("MangaId");
-
-                    b.HasIndex("LibraryLocalLibraryId");
-
-                    b.HasIndex("MangaConnectorId");
-
-                    b.ToTable("Mangas");
-                });
-
-            modelBuilder.Entity("API.Schema.MangaAltTitle", b =>
-                {
-                    b.Property<string>("AltTitleId")
-                        .HasMaxLength(64)
-                        .HasColumnType("character varying(64)");
-
-                    b.Property<string>("Language")
-                        .IsRequired()
-                        .HasMaxLength(8)
-                        .HasColumnType("character varying(8)");
-
-                    b.Property<string>("MangaId")
-                        .HasColumnType("character varying(64)");
-
-                    b.Property<string>("Title")
-                        .IsRequired()
-                        .HasMaxLength(256)
-                        .HasColumnType("character varying(256)");
-
-                    b.HasKey("AltTitleId");
-
-                    b.HasIndex("MangaId");
-
-                    b.ToTable("AltTitles");
-                });
-
-            modelBuilder.Entity("API.Schema.MangaConnectors.MangaConnector", b =>
-                {
-                    b.Property<string>("Name")
-                        .HasMaxLength(32)
-                        .HasColumnType("character varying(32)");
-
-                    b.PrimitiveCollection<string[]>("BaseUris")
-                        .IsRequired()
-                        .HasMaxLength(256)
-                        .HasColumnType("text[]");
-
-                    b.Property<bool>("Enabled")
-                        .HasColumnType("boolean");
-
-                    b.Property<string>("IconUrl")
-                        .IsRequired()
-                        .HasMaxLength(2048)
-                        .HasColumnType("character varying(2048)");
-
-                    b.PrimitiveCollection<string[]>("SupportedLanguages")
-                        .IsRequired()
-                        .HasMaxLength(8)
-                        .HasColumnType("text[]");
-
-                    b.HasKey("Name");
-
-                    b.ToTable("MangaConnectors");
-
-                    b.HasDiscriminator<string>("Name").HasValue("MangaConnector");
-
-                    b.UseTphMappingStrategy();
-                });
-
-            modelBuilder.Entity("API.Schema.MangaTag", b =>
-                {
-                    b.Property<string>("Tag")
-                        .HasMaxLength(64)
-                        .HasColumnType("character varying(64)");
-
-                    b.HasKey("Tag");
-
-                    b.ToTable("Tags");
-                });
-
-            modelBuilder.Entity("API.Schema.Notification", b =>
-                {
-                    b.Property<string>("NotificationId")
-                        .HasMaxLength(64)
-                        .HasColumnType("character varying(64)");
-
-                    b.Property<DateTime>("Date")
-                        .HasColumnType("timestamp with time zone");
-
-                    b.Property<string>("Message")
-                        .IsRequired()
-                        .HasMaxLength(512)
-                        .HasColumnType("character varying(512)");
-
-                    b.Property<string>("Title")
-                        .IsRequired()
-                        .HasMaxLength(128)
-                        .HasColumnType("character varying(128)");
-
-                    b.Property<byte>("Urgency")
-                        .HasColumnType("smallint");
-
-                    b.HasKey("NotificationId");
-
-                    b.ToTable("Notifications");
-                });
-
-            modelBuilder.Entity("API.Schema.NotificationConnectors.NotificationConnector", b =>
-                {
-                    b.Property<string>("Name")
-                        .HasMaxLength(64)
-                        .HasColumnType("character varying(64)");
-
-                    b.Property<string>("Body")
-                        .IsRequired()
-                        .HasMaxLength(4096)
-                        .HasColumnType("character varying(4096)");
-
-                    b.Property<Dictionary<string, string>>("Headers")
-                        .IsRequired()
-                        .HasColumnType("hstore");
-
-                    b.Property<string>("HttpMethod")
-                        .IsRequired()
-                        .HasMaxLength(8)
-                        .HasColumnType("character varying(8)");
-
-                    b.Property<string>("Url")
-                        .IsRequired()
-                        .HasMaxLength(2048)
-                        .HasColumnType("character varying(2048)");
-
-                    b.HasKey("Name");
-
-                    b.ToTable("NotificationConnectors");
-                });
-
-            modelBuilder.Entity("AuthorManga", b =>
-                {
-                    b.Property<string>("AuthorsAuthorId")
-                        .HasColumnType("character varying(64)");
-
-                    b.Property<string>("MangaId")
-                        .HasColumnType("character varying(64)");
-
-                    b.HasKey("AuthorsAuthorId", "MangaId");
-
-                    b.HasIndex("MangaId");
-
-                    b.ToTable("AuthorManga");
-                });
-
-            modelBuilder.Entity("JobJob", b =>
-                {
-                    b.Property<string>("DependsOnJobsJobId")
-                        .HasColumnType("character varying(64)");
-
-                    b.Property<string>("JobId")
-                        .HasColumnType("character varying(64)");
-
-                    b.HasKey("DependsOnJobsJobId", "JobId");
-
-                    b.HasIndex("JobId");
-
-                    b.ToTable("JobJob");
-                });
-
-            modelBuilder.Entity("MangaMangaTag", b =>
-                {
-                    b.Property<string>("MangaId")
-                        .HasColumnType("character varying(64)");
-
-                    b.Property<string>("MangaTagsTag")
-                        .HasColumnType("character varying(64)");
-
-                    b.HasKey("MangaId", "MangaTagsTag");
-
-                    b.HasIndex("MangaTagsTag");
-
-                    b.ToTable("MangaMangaTag");
-                });
-
-            modelBuilder.Entity("API.Schema.Jobs.DownloadAvailableChaptersJob", b =>
-                {
-                    b.HasBaseType("API.Schema.Jobs.Job");
-
-                    b.Property<string>("MangaId")
-                        .IsRequired()
-                        .HasMaxLength(64)
-                        .HasColumnType("character varying(64)");
-
-                    b.ToTable("Jobs", t =>
-                        {
-                            t.Property("MangaId")
-                                .HasColumnName("DownloadAvailableChaptersJob_MangaId");
-                        });
-
-                    b.HasDiscriminator().HasValue((byte)1);
-                });
-
-            modelBuilder.Entity("API.Schema.Jobs.DownloadMangaCoverJob", b =>
-                {
-                    b.HasBaseType("API.Schema.Jobs.Job");
-
-                    b.Property<string>("MangaId")
-                        .IsRequired()
-                        .HasMaxLength(64)
-                        .HasColumnType("character varying(64)");
-
-                    b.HasDiscriminator().HasValue((byte)4);
-                });
-
-            modelBuilder.Entity("API.Schema.Jobs.DownloadSingleChapterJob", b =>
-                {
-                    b.HasBaseType("API.Schema.Jobs.Job");
-
-                    b.Property<string>("ChapterId")
-                        .IsRequired()
-                        .HasMaxLength(64)
-                        .HasColumnType("character varying(64)");
-
-                    b.HasDiscriminator().HasValue((byte)0);
-                });
-
-            modelBuilder.Entity("API.Schema.Jobs.MoveFileOrFolderJob", b =>
-                {
-                    b.HasBaseType("API.Schema.Jobs.Job");
-
-                    b.Property<string>("FromLocation")
-                        .IsRequired()
-                        .HasMaxLength(256)
-                        .HasColumnType("character varying(256)");
-
-                    b.Property<string>("ToLocation")
-                        .IsRequired()
-                        .HasMaxLength(256)
-                        .HasColumnType("character varying(256)");
-
-                    b.HasDiscriminator().HasValue((byte)3);
-                });
-
-            modelBuilder.Entity("API.Schema.Jobs.RetrieveChaptersJob", b =>
-                {
-                    b.HasBaseType("API.Schema.Jobs.Job");
-
-                    b.Property<string>("MangaId")
-                        .IsRequired()
-                        .HasMaxLength(64)
-                        .HasColumnType("character varying(64)");
-
-                    b.ToTable("Jobs", t =>
-                        {
-                            t.Property("MangaId")
-                                .HasColumnName("RetrieveChaptersJob_MangaId");
-                        });
-
-                    b.HasDiscriminator().HasValue((byte)5);
-                });
-
-            modelBuilder.Entity("API.Schema.Jobs.UpdateFilesDownloadedJob", b =>
-                {
-                    b.HasBaseType("API.Schema.Jobs.Job");
-
-                    b.Property<string>("MangaId")
-                        .IsRequired()
-                        .HasMaxLength(64)
-                        .HasColumnType("character varying(64)");
-
-                    b.ToTable("Jobs", t =>
-                        {
-                            t.Property("MangaId")
-                                .HasColumnName("UpdateFilesDownloadedJob_MangaId");
-                        });
-
-                    b.HasDiscriminator().HasValue((byte)6);
-                });
-
-            modelBuilder.Entity("API.Schema.Jobs.UpdateMetadataJob", b =>
-                {
-                    b.HasBaseType("API.Schema.Jobs.Job");
-
-                    b.Property<string>("MangaId")
-                        .IsRequired()
-                        .HasMaxLength(64)
-                        .HasColumnType("character varying(64)");
-
-                    b.HasIndex("MangaId");
-
-                    b.ToTable("Jobs", t =>
-                        {
-                            t.Property("MangaId")
-                                .HasColumnName("UpdateMetadataJob_MangaId");
-                        });
-
-                    b.HasDiscriminator().HasValue((byte)2);
-                });
-
-            modelBuilder.Entity("API.Schema.LibraryConnectors.Kavita", b =>
-                {
-                    b.HasBaseType("API.Schema.LibraryConnectors.LibraryConnector");
-
-                    b.HasDiscriminator().HasValue((byte)1);
-                });
-
-            modelBuilder.Entity("API.Schema.LibraryConnectors.Komga", b =>
-                {
-                    b.HasBaseType("API.Schema.LibraryConnectors.LibraryConnector");
-
-                    b.HasDiscriminator().HasValue((byte)0);
-                });
-
-            modelBuilder.Entity("API.Schema.MangaConnectors.AsuraToon", b =>
-                {
-                    b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
-
-                    b.HasDiscriminator().HasValue("AsuraToon");
-                });
-
-            modelBuilder.Entity("API.Schema.MangaConnectors.Bato", b =>
-                {
-                    b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
-
-                    b.HasDiscriminator().HasValue("Bato");
-                });
-
-            modelBuilder.Entity("API.Schema.MangaConnectors.Global", b =>
-                {
-                    b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
-
-                    b.HasDiscriminator().HasValue("Global");
-                });
-
-            modelBuilder.Entity("API.Schema.MangaConnectors.MangaDex", b =>
-                {
-                    b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
-
-                    b.HasDiscriminator().HasValue("MangaDex");
-                });
-
-            modelBuilder.Entity("API.Schema.MangaConnectors.MangaHere", b =>
-                {
-                    b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
-
-                    b.HasDiscriminator().HasValue("MangaHere");
-                });
-
-            modelBuilder.Entity("API.Schema.MangaConnectors.MangaKatana", b =>
-                {
-                    b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
-
-                    b.HasDiscriminator().HasValue("MangaKatana");
-                });
-
-            modelBuilder.Entity("API.Schema.MangaConnectors.Manganato", b =>
-                {
-                    b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
-
-                    b.HasDiscriminator().HasValue("Manganato");
-                });
-
-            modelBuilder.Entity("API.Schema.MangaConnectors.Mangaworld", b =>
-                {
-                    b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
-
-                    b.HasDiscriminator().HasValue("Mangaworld");
-                });
-
-            modelBuilder.Entity("API.Schema.MangaConnectors.ManhuaPlus", b =>
-                {
-                    b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
-
-                    b.HasDiscriminator().HasValue("ManhuaPlus");
-                });
-
-            modelBuilder.Entity("API.Schema.MangaConnectors.Weebcentral", b =>
-                {
-                    b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
-
-                    b.HasDiscriminator().HasValue("Weebcentral");
-                });
-
-            modelBuilder.Entity("API.Schema.Chapter", b =>
-                {
-                    b.HasOne("API.Schema.Manga", "ParentManga")
-                        .WithMany()
-                        .HasForeignKey("ParentMangaId")
-                        .OnDelete(DeleteBehavior.Cascade)
-                        .IsRequired();
-
-                    b.Navigation("ParentManga");
-                });
-
-            modelBuilder.Entity("API.Schema.Jobs.Job", b =>
-                {
-                    b.HasOne("API.Schema.Jobs.Job", "ParentJob")
-                        .WithMany()
-                        .HasForeignKey("ParentJobId")
-                        .OnDelete(DeleteBehavior.Cascade);
-
-                    b.Navigation("ParentJob");
-                });
-
-            modelBuilder.Entity("API.Schema.Link", b =>
-                {
-                    b.HasOne("API.Schema.Manga", null)
-                        .WithMany("Links")
-                        .HasForeignKey("MangaId")
-                        .OnDelete(DeleteBehavior.Cascade);
-                });
-
-            modelBuilder.Entity("API.Schema.Manga", b =>
-                {
-                    b.HasOne("API.Schema.LocalLibrary", "Library")
-                        .WithMany()
-                        .HasForeignKey("LibraryLocalLibraryId")
-                        .OnDelete(DeleteBehavior.Restrict);
-
-                    b.HasOne("API.Schema.MangaConnectors.MangaConnector", "MangaConnector")
-                        .WithMany()
-                        .HasForeignKey("MangaConnectorId")
-                        .OnDelete(DeleteBehavior.Cascade)
-                        .IsRequired();
-
-                    b.Navigation("Library");
-
-                    b.Navigation("MangaConnector");
-                });
-
-            modelBuilder.Entity("API.Schema.MangaAltTitle", b =>
-                {
-                    b.HasOne("API.Schema.Manga", null)
-                        .WithMany("AltTitles")
-                        .HasForeignKey("MangaId")
-                        .OnDelete(DeleteBehavior.Cascade);
-                });
-
-            modelBuilder.Entity("AuthorManga", b =>
-                {
-                    b.HasOne("API.Schema.Author", null)
-                        .WithMany()
-                        .HasForeignKey("AuthorsAuthorId")
-                        .OnDelete(DeleteBehavior.Cascade)
-                        .IsRequired();
-
-                    b.HasOne("API.Schema.Manga", null)
-                        .WithMany()
-                        .HasForeignKey("MangaId")
-                        .OnDelete(DeleteBehavior.Cascade)
-                        .IsRequired();
-                });
-
-            modelBuilder.Entity("JobJob", b =>
-                {
-                    b.HasOne("API.Schema.Jobs.Job", null)
-                        .WithMany()
-                        .HasForeignKey("DependsOnJobsJobId")
-                        .OnDelete(DeleteBehavior.Cascade)
-                        .IsRequired();
-
-                    b.HasOne("API.Schema.Jobs.Job", null)
-                        .WithMany()
-                        .HasForeignKey("JobId")
-                        .OnDelete(DeleteBehavior.Cascade)
-                        .IsRequired();
-                });
-
-            modelBuilder.Entity("MangaMangaTag", b =>
-                {
-                    b.HasOne("API.Schema.Manga", null)
-                        .WithMany()
-                        .HasForeignKey("MangaId")
-                        .OnDelete(DeleteBehavior.Cascade)
-                        .IsRequired();
-
-                    b.HasOne("API.Schema.MangaTag", null)
-                        .WithMany()
-                        .HasForeignKey("MangaTagsTag")
-                        .OnDelete(DeleteBehavior.Cascade)
-                        .IsRequired();
-                });
-
-            modelBuilder.Entity("API.Schema.Jobs.UpdateMetadataJob", b =>
-                {
-                    b.HasOne("API.Schema.Manga", "Manga")
-                        .WithMany()
-                        .HasForeignKey("MangaId")
-                        .OnDelete(DeleteBehavior.Cascade)
-                        .IsRequired();
-
-                    b.Navigation("Manga");
-                });
-
-            modelBuilder.Entity("API.Schema.Manga", b =>
-                {
-                    b.Navigation("AltTitles");
-
-                    b.Navigation("Links");
-                });
-#pragma warning restore 612, 618
-        }
-    }
-}
diff --git a/API/Migrations/20250402001438_dev-010425-4.cs b/API/Migrations/20250402001438_dev-010425-4.cs
deleted file mode 100644
index 2b4bafd..0000000
--- a/API/Migrations/20250402001438_dev-010425-4.cs
+++ /dev/null
@@ -1,123 +0,0 @@
-using Microsoft.EntityFrameworkCore.Migrations;
-
-#nullable disable
-
-namespace API.Migrations
-{
-    /// <inheritdoc />
-    public partial class dev0104254 : Migration
-    {
-        /// <inheritdoc />
-        protected override void Up(MigrationBuilder migrationBuilder)
-        {
-            migrationBuilder.DropForeignKey(
-                name: "FK_Jobs_Chapters_ChapterId",
-                table: "Jobs");
-
-            migrationBuilder.DropForeignKey(
-                name: "FK_Jobs_Mangas_DownloadAvailableChaptersJob_MangaId",
-                table: "Jobs");
-
-            migrationBuilder.DropForeignKey(
-                name: "FK_Jobs_Mangas_MangaId",
-                table: "Jobs");
-
-            migrationBuilder.DropForeignKey(
-                name: "FK_Jobs_Mangas_RetrieveChaptersJob_MangaId",
-                table: "Jobs");
-
-            migrationBuilder.DropForeignKey(
-                name: "FK_Jobs_Mangas_UpdateFilesDownloadedJob_MangaId",
-                table: "Jobs");
-
-            migrationBuilder.DropIndex(
-                name: "IX_Jobs_ChapterId",
-                table: "Jobs");
-
-            migrationBuilder.DropIndex(
-                name: "IX_Jobs_DownloadAvailableChaptersJob_MangaId",
-                table: "Jobs");
-
-            migrationBuilder.DropIndex(
-                name: "IX_Jobs_MangaId",
-                table: "Jobs");
-
-            migrationBuilder.DropIndex(
-                name: "IX_Jobs_RetrieveChaptersJob_MangaId",
-                table: "Jobs");
-
-            migrationBuilder.DropIndex(
-                name: "IX_Jobs_UpdateFilesDownloadedJob_MangaId",
-                table: "Jobs");
-        }
-
-        /// <inheritdoc />
-        protected override void Down(MigrationBuilder migrationBuilder)
-        {
-            migrationBuilder.CreateIndex(
-                name: "IX_Jobs_ChapterId",
-                table: "Jobs",
-                column: "ChapterId");
-
-            migrationBuilder.CreateIndex(
-                name: "IX_Jobs_DownloadAvailableChaptersJob_MangaId",
-                table: "Jobs",
-                column: "DownloadAvailableChaptersJob_MangaId");
-
-            migrationBuilder.CreateIndex(
-                name: "IX_Jobs_MangaId",
-                table: "Jobs",
-                column: "MangaId");
-
-            migrationBuilder.CreateIndex(
-                name: "IX_Jobs_RetrieveChaptersJob_MangaId",
-                table: "Jobs",
-                column: "RetrieveChaptersJob_MangaId");
-
-            migrationBuilder.CreateIndex(
-                name: "IX_Jobs_UpdateFilesDownloadedJob_MangaId",
-                table: "Jobs",
-                column: "UpdateFilesDownloadedJob_MangaId");
-
-            migrationBuilder.AddForeignKey(
-                name: "FK_Jobs_Chapters_ChapterId",
-                table: "Jobs",
-                column: "ChapterId",
-                principalTable: "Chapters",
-                principalColumn: "ChapterId",
-                onDelete: ReferentialAction.Cascade);
-
-            migrationBuilder.AddForeignKey(
-                name: "FK_Jobs_Mangas_DownloadAvailableChaptersJob_MangaId",
-                table: "Jobs",
-                column: "DownloadAvailableChaptersJob_MangaId",
-                principalTable: "Mangas",
-                principalColumn: "MangaId",
-                onDelete: ReferentialAction.Cascade);
-
-            migrationBuilder.AddForeignKey(
-                name: "FK_Jobs_Mangas_MangaId",
-                table: "Jobs",
-                column: "MangaId",
-                principalTable: "Mangas",
-                principalColumn: "MangaId",
-                onDelete: ReferentialAction.Cascade);
-
-            migrationBuilder.AddForeignKey(
-                name: "FK_Jobs_Mangas_RetrieveChaptersJob_MangaId",
-                table: "Jobs",
-                column: "RetrieveChaptersJob_MangaId",
-                principalTable: "Mangas",
-                principalColumn: "MangaId",
-                onDelete: ReferentialAction.Cascade);
-
-            migrationBuilder.AddForeignKey(
-                name: "FK_Jobs_Mangas_UpdateFilesDownloadedJob_MangaId",
-                table: "Jobs",
-                column: "UpdateFilesDownloadedJob_MangaId",
-                principalTable: "Mangas",
-                principalColumn: "MangaId",
-                onDelete: ReferentialAction.Cascade);
-        }
-    }
-}
diff --git a/API/Migrations/20250401162026_dev-010425-2-Longer_Var_Chars.Designer.cs b/API/Migrations/20250509033915_Initial.Designer.cs
similarity index 78%
rename from API/Migrations/20250401162026_dev-010425-2-Longer_Var_Chars.Designer.cs
rename to API/Migrations/20250509033915_Initial.Designer.cs
index e71924e..0979c82 100644
--- a/API/Migrations/20250401162026_dev-010425-2-Longer_Var_Chars.Designer.cs
+++ b/API/Migrations/20250509033915_Initial.Designer.cs
@@ -13,8 +13,8 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
 namespace API.Migrations
 {
     [DbContext(typeof(PgsqlContext))]
-    [Migration("20250401162026_dev-010425-2-Longer_Var_Chars")]
-    partial class dev0104252Longer_Var_Chars
+    [Migration("20250509033915_Initial")]
+    partial class Initial
     {
         /// <inheritdoc />
         protected override void BuildTargetModel(ModelBuilder modelBuilder)
@@ -64,7 +64,6 @@ namespace API.Migrations
 
                     b.Property<string>("ParentMangaId")
                         .IsRequired()
-                        .HasMaxLength(64)
                         .HasColumnType("character varying(64)");
 
                     b.Property<string>("Title")
@@ -92,10 +91,6 @@ namespace API.Migrations
                         .HasMaxLength(64)
                         .HasColumnType("character varying(64)");
 
-                    b.PrimitiveCollection<string[]>("DependsOnJobsIds")
-                        .HasMaxLength(64)
-                        .HasColumnType("text[]");
-
                     b.Property<bool>("Enabled")
                         .HasColumnType("boolean");
 
@@ -106,6 +101,7 @@ namespace API.Migrations
                         .HasColumnType("timestamp with time zone");
 
                     b.Property<string>("ParentJobId")
+                        .IsRequired()
                         .HasMaxLength(64)
                         .HasColumnType("character varying(64)");
 
@@ -154,32 +150,6 @@ namespace API.Migrations
                     b.UseTphMappingStrategy();
                 });
 
-            modelBuilder.Entity("API.Schema.Link", b =>
-                {
-                    b.Property<string>("LinkId")
-                        .HasMaxLength(64)
-                        .HasColumnType("character varying(64)");
-
-                    b.Property<string>("LinkProvider")
-                        .IsRequired()
-                        .HasMaxLength(64)
-                        .HasColumnType("character varying(64)");
-
-                    b.Property<string>("LinkUrl")
-                        .IsRequired()
-                        .HasMaxLength(2048)
-                        .HasColumnType("character varying(2048)");
-
-                    b.Property<string>("MangaId")
-                        .HasColumnType("character varying(64)");
-
-                    b.HasKey("LinkId");
-
-                    b.HasIndex("MangaId");
-
-                    b.ToTable("Links");
-                });
-
             modelBuilder.Entity("API.Schema.LocalLibrary", b =>
                 {
                     b.Property<string>("LocalLibraryId")
@@ -208,11 +178,13 @@ namespace API.Migrations
                         .HasColumnType("character varying(64)");
 
                     b.Property<string>("CoverFileNameInCache")
-                        .HasColumnType("text");
+                        .HasMaxLength(512)
+                        .HasColumnType("character varying(512)");
 
                     b.Property<string>("CoverUrl")
                         .IsRequired()
-                        .HasColumnType("text");
+                        .HasMaxLength(512)
+                        .HasColumnType("character varying(512)");
 
                     b.Property<string>("Description")
                         .IsRequired()
@@ -228,17 +200,19 @@ namespace API.Migrations
                         .HasMaxLength(256)
                         .HasColumnType("character varying(256)");
 
-                    b.Property<float>("IgnoreChapterBefore")
+                    b.Property<float>("IgnoreChaptersBefore")
                         .HasColumnType("real");
 
-                    b.Property<string>("LibraryLocalLibraryId")
-                        .HasColumnType("character varying(64)");
-
-                    b.Property<string>("MangaConnectorId")
+                    b.Property<string>("LibraryId")
                         .IsRequired()
                         .HasMaxLength(64)
                         .HasColumnType("character varying(64)");
 
+                    b.Property<string>("MangaConnectorName")
+                        .IsRequired()
+                        .HasMaxLength(32)
+                        .HasColumnType("character varying(32)");
+
                     b.Property<string>("Name")
                         .IsRequired()
                         .HasMaxLength(512)
@@ -261,39 +235,13 @@ namespace API.Migrations
 
                     b.HasKey("MangaId");
 
-                    b.HasIndex("LibraryLocalLibraryId");
+                    b.HasIndex("LibraryId");
 
-                    b.HasIndex("MangaConnectorId");
+                    b.HasIndex("MangaConnectorName");
 
                     b.ToTable("Mangas");
                 });
 
-            modelBuilder.Entity("API.Schema.MangaAltTitle", b =>
-                {
-                    b.Property<string>("AltTitleId")
-                        .HasMaxLength(64)
-                        .HasColumnType("character varying(64)");
-
-                    b.Property<string>("Language")
-                        .IsRequired()
-                        .HasMaxLength(8)
-                        .HasColumnType("character varying(8)");
-
-                    b.Property<string>("MangaId")
-                        .HasColumnType("character varying(64)");
-
-                    b.Property<string>("Title")
-                        .IsRequired()
-                        .HasMaxLength(256)
-                        .HasColumnType("character varying(256)");
-
-                    b.HasKey("AltTitleId");
-
-                    b.HasIndex("MangaId");
-
-                    b.ToTable("AltTitles");
-                });
-
             modelBuilder.Entity("API.Schema.MangaConnectors.MangaConnector", b =>
                 {
                     b.Property<string>("Name")
@@ -395,19 +343,19 @@ namespace API.Migrations
                     b.ToTable("NotificationConnectors");
                 });
 
-            modelBuilder.Entity("AuthorManga", b =>
+            modelBuilder.Entity("AuthorToManga", b =>
                 {
-                    b.Property<string>("AuthorsAuthorId")
+                    b.Property<string>("AuthorIds")
                         .HasColumnType("character varying(64)");
 
-                    b.Property<string>("MangaId")
+                    b.Property<string>("MangaIds")
                         .HasColumnType("character varying(64)");
 
-                    b.HasKey("AuthorsAuthorId", "MangaId");
+                    b.HasKey("AuthorIds", "MangaIds");
 
-                    b.HasIndex("MangaId");
+                    b.HasIndex("MangaIds");
 
-                    b.ToTable("AuthorManga");
+                    b.ToTable("AuthorToManga");
                 });
 
             modelBuilder.Entity("JobJob", b =>
@@ -425,19 +373,19 @@ namespace API.Migrations
                     b.ToTable("JobJob");
                 });
 
-            modelBuilder.Entity("MangaMangaTag", b =>
+            modelBuilder.Entity("MangaTagToManga", b =>
                 {
-                    b.Property<string>("MangaId")
+                    b.Property<string>("MangaTagIds")
                         .HasColumnType("character varying(64)");
 
-                    b.Property<string>("MangaTagsTag")
+                    b.Property<string>("MangaIds")
                         .HasColumnType("character varying(64)");
 
-                    b.HasKey("MangaId", "MangaTagsTag");
+                    b.HasKey("MangaTagIds", "MangaIds");
 
-                    b.HasIndex("MangaTagsTag");
+                    b.HasIndex("MangaIds");
 
-                    b.ToTable("MangaMangaTag");
+                    b.ToTable("MangaTagToManga");
                 });
 
             modelBuilder.Entity("API.Schema.Jobs.DownloadAvailableChaptersJob", b =>
@@ -505,10 +453,42 @@ namespace API.Migrations
                     b.HasDiscriminator().HasValue((byte)3);
                 });
 
+            modelBuilder.Entity("API.Schema.Jobs.MoveMangaLibraryJob", b =>
+                {
+                    b.HasBaseType("API.Schema.Jobs.Job");
+
+                    b.Property<string>("MangaId")
+                        .IsRequired()
+                        .HasMaxLength(64)
+                        .HasColumnType("character varying(64)");
+
+                    b.Property<string>("ToLibraryId")
+                        .IsRequired()
+                        .HasMaxLength(64)
+                        .HasColumnType("character varying(64)");
+
+                    b.HasIndex("MangaId");
+
+                    b.HasIndex("ToLibraryId");
+
+                    b.ToTable("Jobs", t =>
+                        {
+                            t.Property("MangaId")
+                                .HasColumnName("MoveMangaLibraryJob_MangaId");
+                        });
+
+                    b.HasDiscriminator().HasValue((byte)7);
+                });
+
             modelBuilder.Entity("API.Schema.Jobs.RetrieveChaptersJob", b =>
                 {
                     b.HasBaseType("API.Schema.Jobs.Job");
 
+                    b.Property<string>("Language")
+                        .IsRequired()
+                        .HasMaxLength(8)
+                        .HasColumnType("character varying(8)");
+
                     b.Property<string>("MangaId")
                         .IsRequired()
                         .HasMaxLength(64)
@@ -545,26 +525,6 @@ namespace API.Migrations
                     b.HasDiscriminator().HasValue((byte)6);
                 });
 
-            modelBuilder.Entity("API.Schema.Jobs.UpdateMetadataJob", b =>
-                {
-                    b.HasBaseType("API.Schema.Jobs.Job");
-
-                    b.Property<string>("MangaId")
-                        .IsRequired()
-                        .HasMaxLength(64)
-                        .HasColumnType("character varying(64)");
-
-                    b.HasIndex("MangaId");
-
-                    b.ToTable("Jobs", t =>
-                        {
-                            t.Property("MangaId")
-                                .HasColumnName("UpdateMetadataJob_MangaId");
-                        });
-
-                    b.HasDiscriminator().HasValue((byte)2);
-                });
-
             modelBuilder.Entity("API.Schema.LibraryConnectors.Kavita", b =>
                 {
                     b.HasBaseType("API.Schema.LibraryConnectors.LibraryConnector");
@@ -579,20 +539,6 @@ namespace API.Migrations
                     b.HasDiscriminator().HasValue((byte)0);
                 });
 
-            modelBuilder.Entity("API.Schema.MangaConnectors.AsuraToon", b =>
-                {
-                    b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
-
-                    b.HasDiscriminator().HasValue("AsuraToon");
-                });
-
-            modelBuilder.Entity("API.Schema.MangaConnectors.Bato", b =>
-                {
-                    b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
-
-                    b.HasDiscriminator().HasValue("Bato");
-                });
-
             modelBuilder.Entity("API.Schema.MangaConnectors.Global", b =>
                 {
                     b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
@@ -607,52 +553,10 @@ namespace API.Migrations
                     b.HasDiscriminator().HasValue("MangaDex");
                 });
 
-            modelBuilder.Entity("API.Schema.MangaConnectors.MangaHere", b =>
-                {
-                    b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
-
-                    b.HasDiscriminator().HasValue("MangaHere");
-                });
-
-            modelBuilder.Entity("API.Schema.MangaConnectors.MangaKatana", b =>
-                {
-                    b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
-
-                    b.HasDiscriminator().HasValue("MangaKatana");
-                });
-
-            modelBuilder.Entity("API.Schema.MangaConnectors.Manganato", b =>
-                {
-                    b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
-
-                    b.HasDiscriminator().HasValue("Manganato");
-                });
-
-            modelBuilder.Entity("API.Schema.MangaConnectors.Mangaworld", b =>
-                {
-                    b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
-
-                    b.HasDiscriminator().HasValue("Mangaworld");
-                });
-
-            modelBuilder.Entity("API.Schema.MangaConnectors.ManhuaPlus", b =>
-                {
-                    b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
-
-                    b.HasDiscriminator().HasValue("ManhuaPlus");
-                });
-
-            modelBuilder.Entity("API.Schema.MangaConnectors.Weebcentral", b =>
-                {
-                    b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
-
-                    b.HasDiscriminator().HasValue("Weebcentral");
-                });
-
             modelBuilder.Entity("API.Schema.Chapter", b =>
                 {
                     b.HasOne("API.Schema.Manga", "ParentManga")
-                        .WithMany()
+                        .WithMany("Chapters")
                         .HasForeignKey("ParentMangaId")
                         .OnDelete(DeleteBehavior.Cascade)
                         .IsRequired();
@@ -670,51 +574,100 @@ namespace API.Migrations
                     b.Navigation("ParentJob");
                 });
 
-            modelBuilder.Entity("API.Schema.Link", b =>
-                {
-                    b.HasOne("API.Schema.Manga", null)
-                        .WithMany("Links")
-                        .HasForeignKey("MangaId")
-                        .OnDelete(DeleteBehavior.Cascade);
-                });
-
             modelBuilder.Entity("API.Schema.Manga", b =>
                 {
                     b.HasOne("API.Schema.LocalLibrary", "Library")
                         .WithMany()
-                        .HasForeignKey("LibraryLocalLibraryId")
-                        .OnDelete(DeleteBehavior.Restrict);
+                        .HasForeignKey("LibraryId")
+                        .OnDelete(DeleteBehavior.SetNull)
+                        .IsRequired();
 
                     b.HasOne("API.Schema.MangaConnectors.MangaConnector", "MangaConnector")
                         .WithMany()
-                        .HasForeignKey("MangaConnectorId")
+                        .HasForeignKey("MangaConnectorName")
                         .OnDelete(DeleteBehavior.Cascade)
                         .IsRequired();
 
+                    b.OwnsMany("API.Schema.Link", "Links", b1 =>
+                        {
+                            b1.Property<string>("LinkId")
+                                .HasMaxLength(64)
+                                .HasColumnType("character varying(64)");
+
+                            b1.Property<string>("LinkProvider")
+                                .IsRequired()
+                                .HasMaxLength(64)
+                                .HasColumnType("character varying(64)");
+
+                            b1.Property<string>("LinkUrl")
+                                .IsRequired()
+                                .HasMaxLength(2048)
+                                .HasColumnType("character varying(2048)");
+
+                            b1.Property<string>("MangaId")
+                                .IsRequired()
+                                .HasColumnType("character varying(64)");
+
+                            b1.HasKey("LinkId");
+
+                            b1.HasIndex("MangaId");
+
+                            b1.ToTable("Links");
+
+                            b1.WithOwner()
+                                .HasForeignKey("MangaId");
+                        });
+
+                    b.OwnsMany("API.Schema.MangaAltTitle", "AltTitles", b1 =>
+                        {
+                            b1.Property<string>("AltTitleId")
+                                .HasMaxLength(64)
+                                .HasColumnType("character varying(64)");
+
+                            b1.Property<string>("Language")
+                                .IsRequired()
+                                .HasMaxLength(8)
+                                .HasColumnType("character varying(8)");
+
+                            b1.Property<string>("MangaId")
+                                .IsRequired()
+                                .HasColumnType("character varying(64)");
+
+                            b1.Property<string>("Title")
+                                .IsRequired()
+                                .HasMaxLength(256)
+                                .HasColumnType("character varying(256)");
+
+                            b1.HasKey("AltTitleId");
+
+                            b1.HasIndex("MangaId");
+
+                            b1.ToTable("AltTitles");
+
+                            b1.WithOwner()
+                                .HasForeignKey("MangaId");
+                        });
+
+                    b.Navigation("AltTitles");
+
                     b.Navigation("Library");
 
+                    b.Navigation("Links");
+
                     b.Navigation("MangaConnector");
                 });
 
-            modelBuilder.Entity("API.Schema.MangaAltTitle", b =>
-                {
-                    b.HasOne("API.Schema.Manga", null)
-                        .WithMany("AltTitles")
-                        .HasForeignKey("MangaId")
-                        .OnDelete(DeleteBehavior.Cascade);
-                });
-
-            modelBuilder.Entity("AuthorManga", b =>
+            modelBuilder.Entity("AuthorToManga", b =>
                 {
                     b.HasOne("API.Schema.Author", null)
                         .WithMany()
-                        .HasForeignKey("AuthorsAuthorId")
+                        .HasForeignKey("AuthorIds")
                         .OnDelete(DeleteBehavior.Cascade)
                         .IsRequired();
 
                     b.HasOne("API.Schema.Manga", null)
                         .WithMany()
-                        .HasForeignKey("MangaId")
+                        .HasForeignKey("MangaIds")
                         .OnDelete(DeleteBehavior.Cascade)
                         .IsRequired();
                 });
@@ -734,17 +687,17 @@ namespace API.Migrations
                         .IsRequired();
                 });
 
-            modelBuilder.Entity("MangaMangaTag", b =>
+            modelBuilder.Entity("MangaTagToManga", b =>
                 {
                     b.HasOne("API.Schema.Manga", null)
                         .WithMany()
-                        .HasForeignKey("MangaId")
+                        .HasForeignKey("MangaIds")
                         .OnDelete(DeleteBehavior.Cascade)
                         .IsRequired();
 
                     b.HasOne("API.Schema.MangaTag", null)
                         .WithMany()
-                        .HasForeignKey("MangaTagsTag")
+                        .HasForeignKey("MangaTagIds")
                         .OnDelete(DeleteBehavior.Cascade)
                         .IsRequired();
                 });
@@ -782,6 +735,25 @@ namespace API.Migrations
                     b.Navigation("Chapter");
                 });
 
+            modelBuilder.Entity("API.Schema.Jobs.MoveMangaLibraryJob", b =>
+                {
+                    b.HasOne("API.Schema.Manga", "Manga")
+                        .WithMany()
+                        .HasForeignKey("MangaId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.HasOne("API.Schema.LocalLibrary", "ToLibrary")
+                        .WithMany()
+                        .HasForeignKey("ToLibraryId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("Manga");
+
+                    b.Navigation("ToLibrary");
+                });
+
             modelBuilder.Entity("API.Schema.Jobs.RetrieveChaptersJob", b =>
                 {
                     b.HasOne("API.Schema.Manga", "Manga")
@@ -804,22 +776,9 @@ namespace API.Migrations
                     b.Navigation("Manga");
                 });
 
-            modelBuilder.Entity("API.Schema.Jobs.UpdateMetadataJob", b =>
-                {
-                    b.HasOne("API.Schema.Manga", "Manga")
-                        .WithMany()
-                        .HasForeignKey("MangaId")
-                        .OnDelete(DeleteBehavior.Cascade)
-                        .IsRequired();
-
-                    b.Navigation("Manga");
-                });
-
             modelBuilder.Entity("API.Schema.Manga", b =>
                 {
-                    b.Navigation("AltTitles");
-
-                    b.Navigation("Links");
+                    b.Navigation("Chapters");
                 });
 #pragma warning restore 612, 618
         }
diff --git a/API/Migrations/20250316143014_dev-160325-Initial.cs b/API/Migrations/20250509033915_Initial.cs
similarity index 83%
rename from API/Migrations/20250316143014_dev-160325-Initial.cs
rename to API/Migrations/20250509033915_Initial.cs
index c447504..509a34d 100644
--- a/API/Migrations/20250316143014_dev-160325-Initial.cs
+++ b/API/Migrations/20250509033915_Initial.cs
@@ -7,7 +7,7 @@ using Microsoft.EntityFrameworkCore.Migrations;
 namespace API.Migrations
 {
     /// <inheritdoc />
-    public partial class dev160325Initial : Migration
+    public partial class Initial : Migration
     {
         /// <inheritdoc />
         protected override void Up(MigrationBuilder migrationBuilder)
@@ -115,32 +115,32 @@ namespace API.Migrations
                 columns: table => new
                 {
                     MangaId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
-                    IdOnConnectorSite = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: false),
-                    Name = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
+                    IdOnConnectorSite = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
+                    Name = table.Column<string>(type: "character varying(512)", maxLength: 512, nullable: false),
                     Description = table.Column<string>(type: "text", nullable: false),
-                    WebsiteUrl = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
-                    CoverUrl = table.Column<string>(type: "text", nullable: false),
-                    CoverFileNameInCache = table.Column<string>(type: "text", nullable: true),
-                    Year = table.Column<long>(type: "bigint", nullable: false),
-                    OriginalLanguage = table.Column<string>(type: "character varying(8)", maxLength: 8, nullable: false),
+                    WebsiteUrl = table.Column<string>(type: "character varying(512)", maxLength: 512, nullable: false),
+                    CoverUrl = table.Column<string>(type: "character varying(512)", maxLength: 512, nullable: false),
                     ReleaseStatus = table.Column<byte>(type: "smallint", nullable: false),
-                    DirectoryName = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
-                    LibraryLocalLibraryId = table.Column<string>(type: "character varying(64)", nullable: true),
-                    IgnoreChapterBefore = table.Column<float>(type: "real", nullable: false),
-                    MangaConnectorId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false)
+                    LibraryId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
+                    MangaConnectorName = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: false),
+                    IgnoreChaptersBefore = table.Column<float>(type: "real", nullable: false),
+                    DirectoryName = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: false),
+                    CoverFileNameInCache = table.Column<string>(type: "character varying(512)", maxLength: 512, nullable: true),
+                    Year = table.Column<long>(type: "bigint", nullable: false),
+                    OriginalLanguage = table.Column<string>(type: "character varying(8)", maxLength: 8, nullable: true)
                 },
                 constraints: table =>
                 {
                     table.PrimaryKey("PK_Mangas", x => x.MangaId);
                     table.ForeignKey(
-                        name: "FK_Mangas_LocalLibraries_LibraryLocalLibraryId",
-                        column: x => x.LibraryLocalLibraryId,
+                        name: "FK_Mangas_LocalLibraries_LibraryId",
+                        column: x => x.LibraryId,
                         principalTable: "LocalLibraries",
                         principalColumn: "LocalLibraryId",
-                        onDelete: ReferentialAction.Cascade);
+                        onDelete: ReferentialAction.SetNull);
                     table.ForeignKey(
-                        name: "FK_Mangas_MangaConnectors_MangaConnectorId",
-                        column: x => x.MangaConnectorId,
+                        name: "FK_Mangas_MangaConnectors_MangaConnectorName",
+                        column: x => x.MangaConnectorName,
                         principalTable: "MangaConnectors",
                         principalColumn: "Name",
                         onDelete: ReferentialAction.Cascade);
@@ -153,7 +153,7 @@ namespace API.Migrations
                     AltTitleId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
                     Language = table.Column<string>(type: "character varying(8)", maxLength: 8, nullable: false),
                     Title = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
-                    MangaId = table.Column<string>(type: "character varying(64)", nullable: true)
+                    MangaId = table.Column<string>(type: "character varying(64)", nullable: false)
                 },
                 constraints: table =>
                 {
@@ -167,24 +167,24 @@ namespace API.Migrations
                 });
 
             migrationBuilder.CreateTable(
-                name: "AuthorManga",
+                name: "AuthorToManga",
                 columns: table => new
                 {
-                    AuthorsAuthorId = table.Column<string>(type: "character varying(64)", nullable: false),
-                    MangaId = table.Column<string>(type: "character varying(64)", nullable: false)
+                    AuthorIds = table.Column<string>(type: "character varying(64)", nullable: false),
+                    MangaIds = table.Column<string>(type: "character varying(64)", nullable: false)
                 },
                 constraints: table =>
                 {
-                    table.PrimaryKey("PK_AuthorManga", x => new { x.AuthorsAuthorId, x.MangaId });
+                    table.PrimaryKey("PK_AuthorToManga", x => new { x.AuthorIds, x.MangaIds });
                     table.ForeignKey(
-                        name: "FK_AuthorManga_Authors_AuthorsAuthorId",
-                        column: x => x.AuthorsAuthorId,
+                        name: "FK_AuthorToManga_Authors_AuthorIds",
+                        column: x => x.AuthorIds,
                         principalTable: "Authors",
                         principalColumn: "AuthorId",
                         onDelete: ReferentialAction.Cascade);
                     table.ForeignKey(
-                        name: "FK_AuthorManga_Mangas_MangaId",
-                        column: x => x.MangaId,
+                        name: "FK_AuthorToManga_Mangas_MangaIds",
+                        column: x => x.MangaIds,
                         principalTable: "Mangas",
                         principalColumn: "MangaId",
                         onDelete: ReferentialAction.Cascade);
@@ -195,13 +195,13 @@ namespace API.Migrations
                 columns: table => new
                 {
                     ChapterId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
+                    ParentMangaId = table.Column<string>(type: "character varying(64)", nullable: false),
                     VolumeNumber = table.Column<int>(type: "integer", nullable: true),
                     ChapterNumber = table.Column<string>(type: "character varying(10)", maxLength: 10, nullable: false),
                     Url = table.Column<string>(type: "character varying(2048)", maxLength: 2048, nullable: false),
                     Title = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
                     FileName = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
-                    Downloaded = table.Column<bool>(type: "boolean", nullable: false),
-                    ParentMangaId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false)
+                    Downloaded = table.Column<bool>(type: "boolean", nullable: false)
                 },
                 constraints: table =>
                 {
@@ -221,7 +221,7 @@ namespace API.Migrations
                     LinkId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
                     LinkProvider = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
                     LinkUrl = table.Column<string>(type: "character varying(2048)", maxLength: 2048, nullable: false),
-                    MangaId = table.Column<string>(type: "character varying(64)", nullable: true)
+                    MangaId = table.Column<string>(type: "character varying(64)", nullable: false)
                 },
                 constraints: table =>
                 {
@@ -235,24 +235,24 @@ namespace API.Migrations
                 });
 
             migrationBuilder.CreateTable(
-                name: "MangaMangaTag",
+                name: "MangaTagToManga",
                 columns: table => new
                 {
-                    MangaId = table.Column<string>(type: "character varying(64)", nullable: false),
-                    MangaTagsTag = table.Column<string>(type: "character varying(64)", nullable: false)
+                    MangaTagIds = table.Column<string>(type: "character varying(64)", nullable: false),
+                    MangaIds = table.Column<string>(type: "character varying(64)", nullable: false)
                 },
                 constraints: table =>
                 {
-                    table.PrimaryKey("PK_MangaMangaTag", x => new { x.MangaId, x.MangaTagsTag });
+                    table.PrimaryKey("PK_MangaTagToManga", x => new { x.MangaTagIds, x.MangaIds });
                     table.ForeignKey(
-                        name: "FK_MangaMangaTag_Mangas_MangaId",
-                        column: x => x.MangaId,
+                        name: "FK_MangaTagToManga_Mangas_MangaIds",
+                        column: x => x.MangaIds,
                         principalTable: "Mangas",
                         principalColumn: "MangaId",
                         onDelete: ReferentialAction.Cascade);
                     table.ForeignKey(
-                        name: "FK_MangaMangaTag_Tags_MangaTagsTag",
-                        column: x => x.MangaTagsTag,
+                        name: "FK_MangaTagToManga_Tags_MangaTagIds",
+                        column: x => x.MangaTagIds,
                         principalTable: "Tags",
                         principalColumn: "Tag",
                         onDelete: ReferentialAction.Cascade);
@@ -263,8 +263,7 @@ namespace API.Migrations
                 columns: table => new
                 {
                     JobId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
-                    ParentJobId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: true),
-                    DependsOnJobsIds = table.Column<string[]>(type: "text[]", maxLength: 64, nullable: true),
+                    ParentJobId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
                     JobType = table.Column<byte>(type: "smallint", nullable: false),
                     RecurrenceMs = table.Column<decimal>(type: "numeric(20,0)", nullable: false),
                     LastExecution = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
@@ -275,9 +274,11 @@ namespace API.Migrations
                     ChapterId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: true),
                     FromLocation = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
                     ToLocation = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
+                    MoveMangaLibraryJob_MangaId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: true),
+                    ToLibraryId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: true),
                     RetrieveChaptersJob_MangaId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: true),
-                    UpdateFilesDownloadedJob_MangaId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: true),
-                    UpdateMetadataJob_MangaId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: true)
+                    Language = table.Column<string>(type: "character varying(8)", maxLength: 8, nullable: true),
+                    UpdateFilesDownloadedJob_MangaId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: true)
                 },
                 constraints: table =>
                 {
@@ -294,6 +295,12 @@ namespace API.Migrations
                         principalTable: "Jobs",
                         principalColumn: "JobId",
                         onDelete: ReferentialAction.Cascade);
+                    table.ForeignKey(
+                        name: "FK_Jobs_LocalLibraries_ToLibraryId",
+                        column: x => x.ToLibraryId,
+                        principalTable: "LocalLibraries",
+                        principalColumn: "LocalLibraryId",
+                        onDelete: ReferentialAction.Cascade);
                     table.ForeignKey(
                         name: "FK_Jobs_Mangas_DownloadAvailableChaptersJob_MangaId",
                         column: x => x.DownloadAvailableChaptersJob_MangaId,
@@ -306,6 +313,12 @@ namespace API.Migrations
                         principalTable: "Mangas",
                         principalColumn: "MangaId",
                         onDelete: ReferentialAction.Cascade);
+                    table.ForeignKey(
+                        name: "FK_Jobs_Mangas_MoveMangaLibraryJob_MangaId",
+                        column: x => x.MoveMangaLibraryJob_MangaId,
+                        principalTable: "Mangas",
+                        principalColumn: "MangaId",
+                        onDelete: ReferentialAction.Cascade);
                     table.ForeignKey(
                         name: "FK_Jobs_Mangas_RetrieveChaptersJob_MangaId",
                         column: x => x.RetrieveChaptersJob_MangaId,
@@ -318,12 +331,6 @@ namespace API.Migrations
                         principalTable: "Mangas",
                         principalColumn: "MangaId",
                         onDelete: ReferentialAction.Cascade);
-                    table.ForeignKey(
-                        name: "FK_Jobs_Mangas_UpdateMetadataJob_MangaId",
-                        column: x => x.UpdateMetadataJob_MangaId,
-                        principalTable: "Mangas",
-                        principalColumn: "MangaId",
-                        onDelete: ReferentialAction.Cascade);
                 });
 
             migrationBuilder.CreateTable(
@@ -356,9 +363,9 @@ namespace API.Migrations
                 column: "MangaId");
 
             migrationBuilder.CreateIndex(
-                name: "IX_AuthorManga_MangaId",
-                table: "AuthorManga",
-                column: "MangaId");
+                name: "IX_AuthorToManga_MangaIds",
+                table: "AuthorToManga",
+                column: "MangaIds");
 
             migrationBuilder.CreateIndex(
                 name: "IX_Chapters_ParentMangaId",
@@ -385,6 +392,11 @@ namespace API.Migrations
                 table: "Jobs",
                 column: "MangaId");
 
+            migrationBuilder.CreateIndex(
+                name: "IX_Jobs_MoveMangaLibraryJob_MangaId",
+                table: "Jobs",
+                column: "MoveMangaLibraryJob_MangaId");
+
             migrationBuilder.CreateIndex(
                 name: "IX_Jobs_ParentJobId",
                 table: "Jobs",
@@ -395,35 +407,35 @@ namespace API.Migrations
                 table: "Jobs",
                 column: "RetrieveChaptersJob_MangaId");
 
+            migrationBuilder.CreateIndex(
+                name: "IX_Jobs_ToLibraryId",
+                table: "Jobs",
+                column: "ToLibraryId");
+
             migrationBuilder.CreateIndex(
                 name: "IX_Jobs_UpdateFilesDownloadedJob_MangaId",
                 table: "Jobs",
                 column: "UpdateFilesDownloadedJob_MangaId");
 
-            migrationBuilder.CreateIndex(
-                name: "IX_Jobs_UpdateMetadataJob_MangaId",
-                table: "Jobs",
-                column: "UpdateMetadataJob_MangaId");
-
             migrationBuilder.CreateIndex(
                 name: "IX_Links_MangaId",
                 table: "Links",
                 column: "MangaId");
 
             migrationBuilder.CreateIndex(
-                name: "IX_MangaMangaTag_MangaTagsTag",
-                table: "MangaMangaTag",
-                column: "MangaTagsTag");
+                name: "IX_Mangas_LibraryId",
+                table: "Mangas",
+                column: "LibraryId");
 
             migrationBuilder.CreateIndex(
-                name: "IX_Mangas_LibraryLocalLibraryId",
+                name: "IX_Mangas_MangaConnectorName",
                 table: "Mangas",
-                column: "LibraryLocalLibraryId");
+                column: "MangaConnectorName");
 
             migrationBuilder.CreateIndex(
-                name: "IX_Mangas_MangaConnectorId",
-                table: "Mangas",
-                column: "MangaConnectorId");
+                name: "IX_MangaTagToManga_MangaIds",
+                table: "MangaTagToManga",
+                column: "MangaIds");
         }
 
         /// <inheritdoc />
@@ -433,7 +445,7 @@ namespace API.Migrations
                 name: "AltTitles");
 
             migrationBuilder.DropTable(
-                name: "AuthorManga");
+                name: "AuthorToManga");
 
             migrationBuilder.DropTable(
                 name: "JobJob");
@@ -445,7 +457,7 @@ namespace API.Migrations
                 name: "Links");
 
             migrationBuilder.DropTable(
-                name: "MangaMangaTag");
+                name: "MangaTagToManga");
 
             migrationBuilder.DropTable(
                 name: "NotificationConnectors");
diff --git a/API/Migrations/20250401234456_dev-010425-3-ParentJobOwnership.Designer.cs b/API/Migrations/20250509034207_Initial-2.Designer.cs
similarity index 78%
rename from API/Migrations/20250401234456_dev-010425-3-ParentJobOwnership.Designer.cs
rename to API/Migrations/20250509034207_Initial-2.Designer.cs
index 6e2a0bd..e73b2c8 100644
--- a/API/Migrations/20250401234456_dev-010425-3-ParentJobOwnership.Designer.cs
+++ b/API/Migrations/20250509034207_Initial-2.Designer.cs
@@ -13,8 +13,8 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
 namespace API.Migrations
 {
     [DbContext(typeof(PgsqlContext))]
-    [Migration("20250401234456_dev-010425-3-ParentJobOwnership")]
-    partial class dev0104253ParentJobOwnership
+    [Migration("20250509034207_Initial-2")]
+    partial class Initial2
     {
         /// <inheritdoc />
         protected override void BuildTargetModel(ModelBuilder modelBuilder)
@@ -64,7 +64,6 @@ namespace API.Migrations
 
                     b.Property<string>("ParentMangaId")
                         .IsRequired()
-                        .HasMaxLength(64)
                         .HasColumnType("character varying(64)");
 
                     b.Property<string>("Title")
@@ -92,10 +91,6 @@ namespace API.Migrations
                         .HasMaxLength(64)
                         .HasColumnType("character varying(64)");
 
-                    b.PrimitiveCollection<string[]>("DependsOnJobsIds")
-                        .HasMaxLength(64)
-                        .HasColumnType("text[]");
-
                     b.Property<bool>("Enabled")
                         .HasColumnType("boolean");
 
@@ -106,6 +101,7 @@ namespace API.Migrations
                         .HasColumnType("timestamp with time zone");
 
                     b.Property<string>("ParentJobId")
+                        .IsRequired()
                         .HasMaxLength(64)
                         .HasColumnType("character varying(64)");
 
@@ -154,32 +150,6 @@ namespace API.Migrations
                     b.UseTphMappingStrategy();
                 });
 
-            modelBuilder.Entity("API.Schema.Link", b =>
-                {
-                    b.Property<string>("LinkId")
-                        .HasMaxLength(64)
-                        .HasColumnType("character varying(64)");
-
-                    b.Property<string>("LinkProvider")
-                        .IsRequired()
-                        .HasMaxLength(64)
-                        .HasColumnType("character varying(64)");
-
-                    b.Property<string>("LinkUrl")
-                        .IsRequired()
-                        .HasMaxLength(2048)
-                        .HasColumnType("character varying(2048)");
-
-                    b.Property<string>("MangaId")
-                        .HasColumnType("character varying(64)");
-
-                    b.HasKey("LinkId");
-
-                    b.HasIndex("MangaId");
-
-                    b.ToTable("Links");
-                });
-
             modelBuilder.Entity("API.Schema.LocalLibrary", b =>
                 {
                     b.Property<string>("LocalLibraryId")
@@ -208,11 +178,13 @@ namespace API.Migrations
                         .HasColumnType("character varying(64)");
 
                     b.Property<string>("CoverFileNameInCache")
-                        .HasColumnType("text");
+                        .HasMaxLength(512)
+                        .HasColumnType("character varying(512)");
 
                     b.Property<string>("CoverUrl")
                         .IsRequired()
-                        .HasColumnType("text");
+                        .HasMaxLength(512)
+                        .HasColumnType("character varying(512)");
 
                     b.Property<string>("Description")
                         .IsRequired()
@@ -228,17 +200,19 @@ namespace API.Migrations
                         .HasMaxLength(256)
                         .HasColumnType("character varying(256)");
 
-                    b.Property<float>("IgnoreChapterBefore")
+                    b.Property<float>("IgnoreChaptersBefore")
                         .HasColumnType("real");
 
-                    b.Property<string>("LibraryLocalLibraryId")
-                        .HasColumnType("character varying(64)");
-
-                    b.Property<string>("MangaConnectorId")
+                    b.Property<string>("LibraryId")
                         .IsRequired()
                         .HasMaxLength(64)
                         .HasColumnType("character varying(64)");
 
+                    b.Property<string>("MangaConnectorName")
+                        .IsRequired()
+                        .HasMaxLength(32)
+                        .HasColumnType("character varying(32)");
+
                     b.Property<string>("Name")
                         .IsRequired()
                         .HasMaxLength(512)
@@ -261,39 +235,13 @@ namespace API.Migrations
 
                     b.HasKey("MangaId");
 
-                    b.HasIndex("LibraryLocalLibraryId");
+                    b.HasIndex("LibraryId");
 
-                    b.HasIndex("MangaConnectorId");
+                    b.HasIndex("MangaConnectorName");
 
                     b.ToTable("Mangas");
                 });
 
-            modelBuilder.Entity("API.Schema.MangaAltTitle", b =>
-                {
-                    b.Property<string>("AltTitleId")
-                        .HasMaxLength(64)
-                        .HasColumnType("character varying(64)");
-
-                    b.Property<string>("Language")
-                        .IsRequired()
-                        .HasMaxLength(8)
-                        .HasColumnType("character varying(8)");
-
-                    b.Property<string>("MangaId")
-                        .HasColumnType("character varying(64)");
-
-                    b.Property<string>("Title")
-                        .IsRequired()
-                        .HasMaxLength(256)
-                        .HasColumnType("character varying(256)");
-
-                    b.HasKey("AltTitleId");
-
-                    b.HasIndex("MangaId");
-
-                    b.ToTable("AltTitles");
-                });
-
             modelBuilder.Entity("API.Schema.MangaConnectors.MangaConnector", b =>
                 {
                     b.Property<string>("Name")
@@ -395,19 +343,19 @@ namespace API.Migrations
                     b.ToTable("NotificationConnectors");
                 });
 
-            modelBuilder.Entity("AuthorManga", b =>
+            modelBuilder.Entity("AuthorToManga", b =>
                 {
-                    b.Property<string>("AuthorsAuthorId")
+                    b.Property<string>("AuthorIds")
                         .HasColumnType("character varying(64)");
 
-                    b.Property<string>("MangaId")
+                    b.Property<string>("MangaIds")
                         .HasColumnType("character varying(64)");
 
-                    b.HasKey("AuthorsAuthorId", "MangaId");
+                    b.HasKey("AuthorIds", "MangaIds");
 
-                    b.HasIndex("MangaId");
+                    b.HasIndex("MangaIds");
 
-                    b.ToTable("AuthorManga");
+                    b.ToTable("AuthorToManga");
                 });
 
             modelBuilder.Entity("JobJob", b =>
@@ -425,19 +373,19 @@ namespace API.Migrations
                     b.ToTable("JobJob");
                 });
 
-            modelBuilder.Entity("MangaMangaTag", b =>
+            modelBuilder.Entity("MangaTagToManga", b =>
                 {
-                    b.Property<string>("MangaId")
+                    b.Property<string>("MangaTagIds")
                         .HasColumnType("character varying(64)");
 
-                    b.Property<string>("MangaTagsTag")
+                    b.Property<string>("MangaIds")
                         .HasColumnType("character varying(64)");
 
-                    b.HasKey("MangaId", "MangaTagsTag");
+                    b.HasKey("MangaTagIds", "MangaIds");
 
-                    b.HasIndex("MangaTagsTag");
+                    b.HasIndex("MangaIds");
 
-                    b.ToTable("MangaMangaTag");
+                    b.ToTable("MangaTagToManga");
                 });
 
             modelBuilder.Entity("API.Schema.Jobs.DownloadAvailableChaptersJob", b =>
@@ -505,10 +453,42 @@ namespace API.Migrations
                     b.HasDiscriminator().HasValue((byte)3);
                 });
 
+            modelBuilder.Entity("API.Schema.Jobs.MoveMangaLibraryJob", b =>
+                {
+                    b.HasBaseType("API.Schema.Jobs.Job");
+
+                    b.Property<string>("MangaId")
+                        .IsRequired()
+                        .HasMaxLength(64)
+                        .HasColumnType("character varying(64)");
+
+                    b.Property<string>("ToLibraryId")
+                        .IsRequired()
+                        .HasMaxLength(64)
+                        .HasColumnType("character varying(64)");
+
+                    b.HasIndex("MangaId");
+
+                    b.HasIndex("ToLibraryId");
+
+                    b.ToTable("Jobs", t =>
+                        {
+                            t.Property("MangaId")
+                                .HasColumnName("MoveMangaLibraryJob_MangaId");
+                        });
+
+                    b.HasDiscriminator().HasValue((byte)7);
+                });
+
             modelBuilder.Entity("API.Schema.Jobs.RetrieveChaptersJob", b =>
                 {
                     b.HasBaseType("API.Schema.Jobs.Job");
 
+                    b.Property<string>("Language")
+                        .IsRequired()
+                        .HasMaxLength(8)
+                        .HasColumnType("character varying(8)");
+
                     b.Property<string>("MangaId")
                         .IsRequired()
                         .HasMaxLength(64)
@@ -545,26 +525,6 @@ namespace API.Migrations
                     b.HasDiscriminator().HasValue((byte)6);
                 });
 
-            modelBuilder.Entity("API.Schema.Jobs.UpdateMetadataJob", b =>
-                {
-                    b.HasBaseType("API.Schema.Jobs.Job");
-
-                    b.Property<string>("MangaId")
-                        .IsRequired()
-                        .HasMaxLength(64)
-                        .HasColumnType("character varying(64)");
-
-                    b.HasIndex("MangaId");
-
-                    b.ToTable("Jobs", t =>
-                        {
-                            t.Property("MangaId")
-                                .HasColumnName("UpdateMetadataJob_MangaId");
-                        });
-
-                    b.HasDiscriminator().HasValue((byte)2);
-                });
-
             modelBuilder.Entity("API.Schema.LibraryConnectors.Kavita", b =>
                 {
                     b.HasBaseType("API.Schema.LibraryConnectors.LibraryConnector");
@@ -579,20 +539,6 @@ namespace API.Migrations
                     b.HasDiscriminator().HasValue((byte)0);
                 });
 
-            modelBuilder.Entity("API.Schema.MangaConnectors.AsuraToon", b =>
-                {
-                    b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
-
-                    b.HasDiscriminator().HasValue("AsuraToon");
-                });
-
-            modelBuilder.Entity("API.Schema.MangaConnectors.Bato", b =>
-                {
-                    b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
-
-                    b.HasDiscriminator().HasValue("Bato");
-                });
-
             modelBuilder.Entity("API.Schema.MangaConnectors.Global", b =>
                 {
                     b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
@@ -607,52 +553,10 @@ namespace API.Migrations
                     b.HasDiscriminator().HasValue("MangaDex");
                 });
 
-            modelBuilder.Entity("API.Schema.MangaConnectors.MangaHere", b =>
-                {
-                    b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
-
-                    b.HasDiscriminator().HasValue("MangaHere");
-                });
-
-            modelBuilder.Entity("API.Schema.MangaConnectors.MangaKatana", b =>
-                {
-                    b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
-
-                    b.HasDiscriminator().HasValue("MangaKatana");
-                });
-
-            modelBuilder.Entity("API.Schema.MangaConnectors.Manganato", b =>
-                {
-                    b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
-
-                    b.HasDiscriminator().HasValue("Manganato");
-                });
-
-            modelBuilder.Entity("API.Schema.MangaConnectors.Mangaworld", b =>
-                {
-                    b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
-
-                    b.HasDiscriminator().HasValue("Mangaworld");
-                });
-
-            modelBuilder.Entity("API.Schema.MangaConnectors.ManhuaPlus", b =>
-                {
-                    b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
-
-                    b.HasDiscriminator().HasValue("ManhuaPlus");
-                });
-
-            modelBuilder.Entity("API.Schema.MangaConnectors.Weebcentral", b =>
-                {
-                    b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
-
-                    b.HasDiscriminator().HasValue("Weebcentral");
-                });
-
             modelBuilder.Entity("API.Schema.Chapter", b =>
                 {
                     b.HasOne("API.Schema.Manga", "ParentManga")
-                        .WithMany()
+                        .WithMany("Chapters")
                         .HasForeignKey("ParentMangaId")
                         .OnDelete(DeleteBehavior.Cascade)
                         .IsRequired();
@@ -670,51 +574,100 @@ namespace API.Migrations
                     b.Navigation("ParentJob");
                 });
 
-            modelBuilder.Entity("API.Schema.Link", b =>
-                {
-                    b.HasOne("API.Schema.Manga", null)
-                        .WithMany("Links")
-                        .HasForeignKey("MangaId")
-                        .OnDelete(DeleteBehavior.Cascade);
-                });
-
             modelBuilder.Entity("API.Schema.Manga", b =>
                 {
                     b.HasOne("API.Schema.LocalLibrary", "Library")
                         .WithMany()
-                        .HasForeignKey("LibraryLocalLibraryId")
-                        .OnDelete(DeleteBehavior.Restrict);
+                        .HasForeignKey("LibraryId")
+                        .OnDelete(DeleteBehavior.SetNull)
+                        .IsRequired();
 
                     b.HasOne("API.Schema.MangaConnectors.MangaConnector", "MangaConnector")
                         .WithMany()
-                        .HasForeignKey("MangaConnectorId")
+                        .HasForeignKey("MangaConnectorName")
                         .OnDelete(DeleteBehavior.Cascade)
                         .IsRequired();
 
+                    b.OwnsMany("API.Schema.Link", "Links", b1 =>
+                        {
+                            b1.Property<string>("LinkId")
+                                .HasMaxLength(64)
+                                .HasColumnType("character varying(64)");
+
+                            b1.Property<string>("LinkProvider")
+                                .IsRequired()
+                                .HasMaxLength(64)
+                                .HasColumnType("character varying(64)");
+
+                            b1.Property<string>("LinkUrl")
+                                .IsRequired()
+                                .HasMaxLength(2048)
+                                .HasColumnType("character varying(2048)");
+
+                            b1.Property<string>("MangaId")
+                                .IsRequired()
+                                .HasColumnType("character varying(64)");
+
+                            b1.HasKey("LinkId");
+
+                            b1.HasIndex("MangaId");
+
+                            b1.ToTable("Links");
+
+                            b1.WithOwner()
+                                .HasForeignKey("MangaId");
+                        });
+
+                    b.OwnsMany("API.Schema.MangaAltTitle", "AltTitles", b1 =>
+                        {
+                            b1.Property<string>("AltTitleId")
+                                .HasMaxLength(64)
+                                .HasColumnType("character varying(64)");
+
+                            b1.Property<string>("Language")
+                                .IsRequired()
+                                .HasMaxLength(8)
+                                .HasColumnType("character varying(8)");
+
+                            b1.Property<string>("MangaId")
+                                .IsRequired()
+                                .HasColumnType("character varying(64)");
+
+                            b1.Property<string>("Title")
+                                .IsRequired()
+                                .HasMaxLength(256)
+                                .HasColumnType("character varying(256)");
+
+                            b1.HasKey("AltTitleId");
+
+                            b1.HasIndex("MangaId");
+
+                            b1.ToTable("AltTitles");
+
+                            b1.WithOwner()
+                                .HasForeignKey("MangaId");
+                        });
+
+                    b.Navigation("AltTitles");
+
                     b.Navigation("Library");
 
+                    b.Navigation("Links");
+
                     b.Navigation("MangaConnector");
                 });
 
-            modelBuilder.Entity("API.Schema.MangaAltTitle", b =>
-                {
-                    b.HasOne("API.Schema.Manga", null)
-                        .WithMany("AltTitles")
-                        .HasForeignKey("MangaId")
-                        .OnDelete(DeleteBehavior.Cascade);
-                });
-
-            modelBuilder.Entity("AuthorManga", b =>
+            modelBuilder.Entity("AuthorToManga", b =>
                 {
                     b.HasOne("API.Schema.Author", null)
                         .WithMany()
-                        .HasForeignKey("AuthorsAuthorId")
+                        .HasForeignKey("AuthorIds")
                         .OnDelete(DeleteBehavior.Cascade)
                         .IsRequired();
 
                     b.HasOne("API.Schema.Manga", null)
                         .WithMany()
-                        .HasForeignKey("MangaId")
+                        .HasForeignKey("MangaIds")
                         .OnDelete(DeleteBehavior.Cascade)
                         .IsRequired();
                 });
@@ -734,17 +687,17 @@ namespace API.Migrations
                         .IsRequired();
                 });
 
-            modelBuilder.Entity("MangaMangaTag", b =>
+            modelBuilder.Entity("MangaTagToManga", b =>
                 {
                     b.HasOne("API.Schema.Manga", null)
                         .WithMany()
-                        .HasForeignKey("MangaId")
+                        .HasForeignKey("MangaIds")
                         .OnDelete(DeleteBehavior.Cascade)
                         .IsRequired();
 
                     b.HasOne("API.Schema.MangaTag", null)
                         .WithMany()
-                        .HasForeignKey("MangaTagsTag")
+                        .HasForeignKey("MangaTagIds")
                         .OnDelete(DeleteBehavior.Cascade)
                         .IsRequired();
                 });
@@ -782,6 +735,25 @@ namespace API.Migrations
                     b.Navigation("Chapter");
                 });
 
+            modelBuilder.Entity("API.Schema.Jobs.MoveMangaLibraryJob", b =>
+                {
+                    b.HasOne("API.Schema.Manga", "Manga")
+                        .WithMany()
+                        .HasForeignKey("MangaId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.HasOne("API.Schema.LocalLibrary", "ToLibrary")
+                        .WithMany()
+                        .HasForeignKey("ToLibraryId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("Manga");
+
+                    b.Navigation("ToLibrary");
+                });
+
             modelBuilder.Entity("API.Schema.Jobs.RetrieveChaptersJob", b =>
                 {
                     b.HasOne("API.Schema.Manga", "Manga")
@@ -804,22 +776,9 @@ namespace API.Migrations
                     b.Navigation("Manga");
                 });
 
-            modelBuilder.Entity("API.Schema.Jobs.UpdateMetadataJob", b =>
-                {
-                    b.HasOne("API.Schema.Manga", "Manga")
-                        .WithMany()
-                        .HasForeignKey("MangaId")
-                        .OnDelete(DeleteBehavior.Cascade)
-                        .IsRequired();
-
-                    b.Navigation("Manga");
-                });
-
             modelBuilder.Entity("API.Schema.Manga", b =>
                 {
-                    b.Navigation("AltTitles");
-
-                    b.Navigation("Links");
+                    b.Navigation("Chapters");
                 });
 #pragma warning restore 612, 618
         }
diff --git a/API/Migrations/20250401234456_dev-010425-3-ParentJobOwnership.cs b/API/Migrations/20250509034207_Initial-2.cs
similarity index 85%
rename from API/Migrations/20250401234456_dev-010425-3-ParentJobOwnership.cs
rename to API/Migrations/20250509034207_Initial-2.cs
index e6c528e..b14848f 100644
--- a/API/Migrations/20250401234456_dev-010425-3-ParentJobOwnership.cs
+++ b/API/Migrations/20250509034207_Initial-2.cs
@@ -5,7 +5,7 @@
 namespace API.Migrations
 {
     /// <inheritdoc />
-    public partial class dev0104253ParentJobOwnership : Migration
+    public partial class Initial2 : Migration
     {
         /// <inheritdoc />
         protected override void Up(MigrationBuilder migrationBuilder)
diff --git a/API/Migrations/20250316150158_dev-160325-2.Designer.cs b/API/Migrations/20250509035413_Initial-3.Designer.cs
similarity index 78%
rename from API/Migrations/20250316150158_dev-160325-2.Designer.cs
rename to API/Migrations/20250509035413_Initial-3.Designer.cs
index e2bd7a1..453ec1b 100644
--- a/API/Migrations/20250316150158_dev-160325-2.Designer.cs
+++ b/API/Migrations/20250509035413_Initial-3.Designer.cs
@@ -13,8 +13,8 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
 namespace API.Migrations
 {
     [DbContext(typeof(PgsqlContext))]
-    [Migration("20250316150158_dev-160325-2")]
-    partial class dev1603252
+    [Migration("20250509035413_Initial-3")]
+    partial class Initial3
     {
         /// <inheritdoc />
         protected override void BuildTargetModel(ModelBuilder modelBuilder)
@@ -64,7 +64,6 @@ namespace API.Migrations
 
                     b.Property<string>("ParentMangaId")
                         .IsRequired()
-                        .HasMaxLength(64)
                         .HasColumnType("character varying(64)");
 
                     b.Property<string>("Title")
@@ -92,10 +91,6 @@ namespace API.Migrations
                         .HasMaxLength(64)
                         .HasColumnType("character varying(64)");
 
-                    b.PrimitiveCollection<string[]>("DependsOnJobsIds")
-                        .HasMaxLength(64)
-                        .HasColumnType("text[]");
-
                     b.Property<bool>("Enabled")
                         .HasColumnType("boolean");
 
@@ -106,6 +101,7 @@ namespace API.Migrations
                         .HasColumnType("timestamp with time zone");
 
                     b.Property<string>("ParentJobId")
+                        .IsRequired()
                         .HasMaxLength(64)
                         .HasColumnType("character varying(64)");
 
@@ -154,32 +150,6 @@ namespace API.Migrations
                     b.UseTphMappingStrategy();
                 });
 
-            modelBuilder.Entity("API.Schema.Link", b =>
-                {
-                    b.Property<string>("LinkId")
-                        .HasMaxLength(64)
-                        .HasColumnType("character varying(64)");
-
-                    b.Property<string>("LinkProvider")
-                        .IsRequired()
-                        .HasMaxLength(64)
-                        .HasColumnType("character varying(64)");
-
-                    b.Property<string>("LinkUrl")
-                        .IsRequired()
-                        .HasMaxLength(2048)
-                        .HasColumnType("character varying(2048)");
-
-                    b.Property<string>("MangaId")
-                        .HasColumnType("character varying(64)");
-
-                    b.HasKey("LinkId");
-
-                    b.HasIndex("MangaId");
-
-                    b.ToTable("Links");
-                });
-
             modelBuilder.Entity("API.Schema.LocalLibrary", b =>
                 {
                     b.Property<string>("LocalLibraryId")
@@ -208,11 +178,13 @@ namespace API.Migrations
                         .HasColumnType("character varying(64)");
 
                     b.Property<string>("CoverFileNameInCache")
-                        .HasColumnType("text");
+                        .HasMaxLength(512)
+                        .HasColumnType("character varying(512)");
 
                     b.Property<string>("CoverUrl")
                         .IsRequired()
-                        .HasColumnType("text");
+                        .HasMaxLength(512)
+                        .HasColumnType("character varying(512)");
 
                     b.Property<string>("Description")
                         .IsRequired()
@@ -220,32 +192,33 @@ namespace API.Migrations
 
                     b.Property<string>("DirectoryName")
                         .IsRequired()
-                        .HasMaxLength(256)
-                        .HasColumnType("character varying(256)");
+                        .HasMaxLength(1024)
+                        .HasColumnType("character varying(1024)");
 
                     b.Property<string>("IdOnConnectorSite")
                         .IsRequired()
-                        .HasMaxLength(128)
-                        .HasColumnType("character varying(128)");
+                        .HasMaxLength(256)
+                        .HasColumnType("character varying(256)");
 
-                    b.Property<float>("IgnoreChapterBefore")
+                    b.Property<float>("IgnoreChaptersBefore")
                         .HasColumnType("real");
 
-                    b.Property<string>("LibraryLocalLibraryId")
-                        .HasColumnType("character varying(64)");
-
-                    b.Property<string>("MangaConnectorId")
+                    b.Property<string>("LibraryId")
                         .IsRequired()
                         .HasMaxLength(64)
                         .HasColumnType("character varying(64)");
 
+                    b.Property<string>("MangaConnectorName")
+                        .IsRequired()
+                        .HasMaxLength(32)
+                        .HasColumnType("character varying(32)");
+
                     b.Property<string>("Name")
                         .IsRequired()
-                        .HasMaxLength(256)
-                        .HasColumnType("character varying(256)");
+                        .HasMaxLength(512)
+                        .HasColumnType("character varying(512)");
 
                     b.Property<string>("OriginalLanguage")
-                        .IsRequired()
                         .HasMaxLength(8)
                         .HasColumnType("character varying(8)");
 
@@ -254,47 +227,21 @@ namespace API.Migrations
 
                     b.Property<string>("WebsiteUrl")
                         .IsRequired()
-                        .HasMaxLength(256)
-                        .HasColumnType("character varying(256)");
+                        .HasMaxLength(512)
+                        .HasColumnType("character varying(512)");
 
                     b.Property<long>("Year")
                         .HasColumnType("bigint");
 
                     b.HasKey("MangaId");
 
-                    b.HasIndex("LibraryLocalLibraryId");
+                    b.HasIndex("LibraryId");
 
-                    b.HasIndex("MangaConnectorId");
+                    b.HasIndex("MangaConnectorName");
 
                     b.ToTable("Mangas");
                 });
 
-            modelBuilder.Entity("API.Schema.MangaAltTitle", b =>
-                {
-                    b.Property<string>("AltTitleId")
-                        .HasMaxLength(64)
-                        .HasColumnType("character varying(64)");
-
-                    b.Property<string>("Language")
-                        .IsRequired()
-                        .HasMaxLength(8)
-                        .HasColumnType("character varying(8)");
-
-                    b.Property<string>("MangaId")
-                        .HasColumnType("character varying(64)");
-
-                    b.Property<string>("Title")
-                        .IsRequired()
-                        .HasMaxLength(256)
-                        .HasColumnType("character varying(256)");
-
-                    b.HasKey("AltTitleId");
-
-                    b.HasIndex("MangaId");
-
-                    b.ToTable("AltTitles");
-                });
-
             modelBuilder.Entity("API.Schema.MangaConnectors.MangaConnector", b =>
                 {
                     b.Property<string>("Name")
@@ -396,19 +343,19 @@ namespace API.Migrations
                     b.ToTable("NotificationConnectors");
                 });
 
-            modelBuilder.Entity("AuthorManga", b =>
+            modelBuilder.Entity("AuthorToManga", b =>
                 {
-                    b.Property<string>("AuthorsAuthorId")
+                    b.Property<string>("AuthorIds")
                         .HasColumnType("character varying(64)");
 
-                    b.Property<string>("MangaId")
+                    b.Property<string>("MangaIds")
                         .HasColumnType("character varying(64)");
 
-                    b.HasKey("AuthorsAuthorId", "MangaId");
+                    b.HasKey("AuthorIds", "MangaIds");
 
-                    b.HasIndex("MangaId");
+                    b.HasIndex("MangaIds");
 
-                    b.ToTable("AuthorManga");
+                    b.ToTable("AuthorToManga");
                 });
 
             modelBuilder.Entity("JobJob", b =>
@@ -426,19 +373,19 @@ namespace API.Migrations
                     b.ToTable("JobJob");
                 });
 
-            modelBuilder.Entity("MangaMangaTag", b =>
+            modelBuilder.Entity("MangaTagToManga", b =>
                 {
-                    b.Property<string>("MangaId")
+                    b.Property<string>("MangaTagIds")
                         .HasColumnType("character varying(64)");
 
-                    b.Property<string>("MangaTagsTag")
+                    b.Property<string>("MangaIds")
                         .HasColumnType("character varying(64)");
 
-                    b.HasKey("MangaId", "MangaTagsTag");
+                    b.HasKey("MangaTagIds", "MangaIds");
 
-                    b.HasIndex("MangaTagsTag");
+                    b.HasIndex("MangaIds");
 
-                    b.ToTable("MangaMangaTag");
+                    b.ToTable("MangaTagToManga");
                 });
 
             modelBuilder.Entity("API.Schema.Jobs.DownloadAvailableChaptersJob", b =>
@@ -506,10 +453,42 @@ namespace API.Migrations
                     b.HasDiscriminator().HasValue((byte)3);
                 });
 
+            modelBuilder.Entity("API.Schema.Jobs.MoveMangaLibraryJob", b =>
+                {
+                    b.HasBaseType("API.Schema.Jobs.Job");
+
+                    b.Property<string>("MangaId")
+                        .IsRequired()
+                        .HasMaxLength(64)
+                        .HasColumnType("character varying(64)");
+
+                    b.Property<string>("ToLibraryId")
+                        .IsRequired()
+                        .HasMaxLength(64)
+                        .HasColumnType("character varying(64)");
+
+                    b.HasIndex("MangaId");
+
+                    b.HasIndex("ToLibraryId");
+
+                    b.ToTable("Jobs", t =>
+                        {
+                            t.Property("MangaId")
+                                .HasColumnName("MoveMangaLibraryJob_MangaId");
+                        });
+
+                    b.HasDiscriminator().HasValue((byte)7);
+                });
+
             modelBuilder.Entity("API.Schema.Jobs.RetrieveChaptersJob", b =>
                 {
                     b.HasBaseType("API.Schema.Jobs.Job");
 
+                    b.Property<string>("Language")
+                        .IsRequired()
+                        .HasMaxLength(8)
+                        .HasColumnType("character varying(8)");
+
                     b.Property<string>("MangaId")
                         .IsRequired()
                         .HasMaxLength(64)
@@ -546,26 +525,6 @@ namespace API.Migrations
                     b.HasDiscriminator().HasValue((byte)6);
                 });
 
-            modelBuilder.Entity("API.Schema.Jobs.UpdateMetadataJob", b =>
-                {
-                    b.HasBaseType("API.Schema.Jobs.Job");
-
-                    b.Property<string>("MangaId")
-                        .IsRequired()
-                        .HasMaxLength(64)
-                        .HasColumnType("character varying(64)");
-
-                    b.HasIndex("MangaId");
-
-                    b.ToTable("Jobs", t =>
-                        {
-                            t.Property("MangaId")
-                                .HasColumnName("UpdateMetadataJob_MangaId");
-                        });
-
-                    b.HasDiscriminator().HasValue((byte)2);
-                });
-
             modelBuilder.Entity("API.Schema.LibraryConnectors.Kavita", b =>
                 {
                     b.HasBaseType("API.Schema.LibraryConnectors.LibraryConnector");
@@ -580,18 +539,11 @@ namespace API.Migrations
                     b.HasDiscriminator().HasValue((byte)0);
                 });
 
-            modelBuilder.Entity("API.Schema.MangaConnectors.AsuraToon", b =>
+            modelBuilder.Entity("API.Schema.MangaConnectors.Global", b =>
                 {
                     b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
 
-                    b.HasDiscriminator().HasValue("AsuraToon");
-                });
-
-            modelBuilder.Entity("API.Schema.MangaConnectors.Bato", b =>
-                {
-                    b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
-
-                    b.HasDiscriminator().HasValue("Bato");
+                    b.HasDiscriminator().HasValue("Global");
                 });
 
             modelBuilder.Entity("API.Schema.MangaConnectors.MangaDex", b =>
@@ -601,52 +553,10 @@ namespace API.Migrations
                     b.HasDiscriminator().HasValue("MangaDex");
                 });
 
-            modelBuilder.Entity("API.Schema.MangaConnectors.MangaHere", b =>
-                {
-                    b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
-
-                    b.HasDiscriminator().HasValue("MangaHere");
-                });
-
-            modelBuilder.Entity("API.Schema.MangaConnectors.MangaKatana", b =>
-                {
-                    b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
-
-                    b.HasDiscriminator().HasValue("MangaKatana");
-                });
-
-            modelBuilder.Entity("API.Schema.MangaConnectors.Manganato", b =>
-                {
-                    b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
-
-                    b.HasDiscriminator().HasValue("Manganato");
-                });
-
-            modelBuilder.Entity("API.Schema.MangaConnectors.Mangaworld", b =>
-                {
-                    b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
-
-                    b.HasDiscriminator().HasValue("Mangaworld");
-                });
-
-            modelBuilder.Entity("API.Schema.MangaConnectors.ManhuaPlus", b =>
-                {
-                    b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
-
-                    b.HasDiscriminator().HasValue("ManhuaPlus");
-                });
-
-            modelBuilder.Entity("API.Schema.MangaConnectors.Weebcentral", b =>
-                {
-                    b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
-
-                    b.HasDiscriminator().HasValue("Weebcentral");
-                });
-
             modelBuilder.Entity("API.Schema.Chapter", b =>
                 {
                     b.HasOne("API.Schema.Manga", "ParentManga")
-                        .WithMany()
+                        .WithMany("Chapters")
                         .HasForeignKey("ParentMangaId")
                         .OnDelete(DeleteBehavior.Cascade)
                         .IsRequired();
@@ -664,51 +574,100 @@ namespace API.Migrations
                     b.Navigation("ParentJob");
                 });
 
-            modelBuilder.Entity("API.Schema.Link", b =>
-                {
-                    b.HasOne("API.Schema.Manga", null)
-                        .WithMany("Links")
-                        .HasForeignKey("MangaId")
-                        .OnDelete(DeleteBehavior.Cascade);
-                });
-
             modelBuilder.Entity("API.Schema.Manga", b =>
                 {
                     b.HasOne("API.Schema.LocalLibrary", "Library")
                         .WithMany()
-                        .HasForeignKey("LibraryLocalLibraryId")
-                        .OnDelete(DeleteBehavior.Restrict);
+                        .HasForeignKey("LibraryId")
+                        .OnDelete(DeleteBehavior.SetNull)
+                        .IsRequired();
 
                     b.HasOne("API.Schema.MangaConnectors.MangaConnector", "MangaConnector")
                         .WithMany()
-                        .HasForeignKey("MangaConnectorId")
+                        .HasForeignKey("MangaConnectorName")
                         .OnDelete(DeleteBehavior.Cascade)
                         .IsRequired();
 
+                    b.OwnsMany("API.Schema.Link", "Links", b1 =>
+                        {
+                            b1.Property<string>("LinkId")
+                                .HasMaxLength(64)
+                                .HasColumnType("character varying(64)");
+
+                            b1.Property<string>("LinkProvider")
+                                .IsRequired()
+                                .HasMaxLength(64)
+                                .HasColumnType("character varying(64)");
+
+                            b1.Property<string>("LinkUrl")
+                                .IsRequired()
+                                .HasMaxLength(2048)
+                                .HasColumnType("character varying(2048)");
+
+                            b1.Property<string>("MangaId")
+                                .IsRequired()
+                                .HasColumnType("character varying(64)");
+
+                            b1.HasKey("LinkId");
+
+                            b1.HasIndex("MangaId");
+
+                            b1.ToTable("Link");
+
+                            b1.WithOwner()
+                                .HasForeignKey("MangaId");
+                        });
+
+                    b.OwnsMany("API.Schema.MangaAltTitle", "AltTitles", b1 =>
+                        {
+                            b1.Property<string>("AltTitleId")
+                                .HasMaxLength(64)
+                                .HasColumnType("character varying(64)");
+
+                            b1.Property<string>("Language")
+                                .IsRequired()
+                                .HasMaxLength(8)
+                                .HasColumnType("character varying(8)");
+
+                            b1.Property<string>("MangaId")
+                                .IsRequired()
+                                .HasColumnType("character varying(64)");
+
+                            b1.Property<string>("Title")
+                                .IsRequired()
+                                .HasMaxLength(256)
+                                .HasColumnType("character varying(256)");
+
+                            b1.HasKey("AltTitleId");
+
+                            b1.HasIndex("MangaId");
+
+                            b1.ToTable("MangaAltTitle");
+
+                            b1.WithOwner()
+                                .HasForeignKey("MangaId");
+                        });
+
+                    b.Navigation("AltTitles");
+
                     b.Navigation("Library");
 
+                    b.Navigation("Links");
+
                     b.Navigation("MangaConnector");
                 });
 
-            modelBuilder.Entity("API.Schema.MangaAltTitle", b =>
-                {
-                    b.HasOne("API.Schema.Manga", null)
-                        .WithMany("AltTitles")
-                        .HasForeignKey("MangaId")
-                        .OnDelete(DeleteBehavior.Cascade);
-                });
-
-            modelBuilder.Entity("AuthorManga", b =>
+            modelBuilder.Entity("AuthorToManga", b =>
                 {
                     b.HasOne("API.Schema.Author", null)
                         .WithMany()
-                        .HasForeignKey("AuthorsAuthorId")
+                        .HasForeignKey("AuthorIds")
                         .OnDelete(DeleteBehavior.Cascade)
                         .IsRequired();
 
                     b.HasOne("API.Schema.Manga", null)
                         .WithMany()
-                        .HasForeignKey("MangaId")
+                        .HasForeignKey("MangaIds")
                         .OnDelete(DeleteBehavior.Cascade)
                         .IsRequired();
                 });
@@ -728,17 +687,17 @@ namespace API.Migrations
                         .IsRequired();
                 });
 
-            modelBuilder.Entity("MangaMangaTag", b =>
+            modelBuilder.Entity("MangaTagToManga", b =>
                 {
                     b.HasOne("API.Schema.Manga", null)
                         .WithMany()
-                        .HasForeignKey("MangaId")
+                        .HasForeignKey("MangaIds")
                         .OnDelete(DeleteBehavior.Cascade)
                         .IsRequired();
 
                     b.HasOne("API.Schema.MangaTag", null)
                         .WithMany()
-                        .HasForeignKey("MangaTagsTag")
+                        .HasForeignKey("MangaTagIds")
                         .OnDelete(DeleteBehavior.Cascade)
                         .IsRequired();
                 });
@@ -776,6 +735,25 @@ namespace API.Migrations
                     b.Navigation("Chapter");
                 });
 
+            modelBuilder.Entity("API.Schema.Jobs.MoveMangaLibraryJob", b =>
+                {
+                    b.HasOne("API.Schema.Manga", "Manga")
+                        .WithMany()
+                        .HasForeignKey("MangaId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.HasOne("API.Schema.LocalLibrary", "ToLibrary")
+                        .WithMany()
+                        .HasForeignKey("ToLibraryId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("Manga");
+
+                    b.Navigation("ToLibrary");
+                });
+
             modelBuilder.Entity("API.Schema.Jobs.RetrieveChaptersJob", b =>
                 {
                     b.HasOne("API.Schema.Manga", "Manga")
@@ -798,22 +776,9 @@ namespace API.Migrations
                     b.Navigation("Manga");
                 });
 
-            modelBuilder.Entity("API.Schema.Jobs.UpdateMetadataJob", b =>
-                {
-                    b.HasOne("API.Schema.Manga", "Manga")
-                        .WithMany()
-                        .HasForeignKey("MangaId")
-                        .OnDelete(DeleteBehavior.Cascade)
-                        .IsRequired();
-
-                    b.Navigation("Manga");
-                });
-
             modelBuilder.Entity("API.Schema.Manga", b =>
                 {
-                    b.Navigation("AltTitles");
-
-                    b.Navigation("Links");
+                    b.Navigation("Chapters");
                 });
 #pragma warning restore 612, 618
         }
diff --git a/API/Migrations/20250509035413_Initial-3.cs b/API/Migrations/20250509035413_Initial-3.cs
new file mode 100644
index 0000000..1b96dac
--- /dev/null
+++ b/API/Migrations/20250509035413_Initial-3.cs
@@ -0,0 +1,130 @@
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace API.Migrations
+{
+    /// <inheritdoc />
+    public partial class Initial3 : Migration
+    {
+        /// <inheritdoc />
+        protected override void Up(MigrationBuilder migrationBuilder)
+        {
+            migrationBuilder.DropTable(
+                name: "AltTitles");
+
+            migrationBuilder.DropTable(
+                name: "Links");
+
+            migrationBuilder.CreateTable(
+                name: "Link",
+                columns: table => new
+                {
+                    LinkId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
+                    LinkProvider = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
+                    LinkUrl = table.Column<string>(type: "character varying(2048)", maxLength: 2048, nullable: false),
+                    MangaId = table.Column<string>(type: "character varying(64)", nullable: false)
+                },
+                constraints: table =>
+                {
+                    table.PrimaryKey("PK_Link", x => x.LinkId);
+                    table.ForeignKey(
+                        name: "FK_Link_Mangas_MangaId",
+                        column: x => x.MangaId,
+                        principalTable: "Mangas",
+                        principalColumn: "MangaId",
+                        onDelete: ReferentialAction.Cascade);
+                });
+
+            migrationBuilder.CreateTable(
+                name: "MangaAltTitle",
+                columns: table => new
+                {
+                    AltTitleId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
+                    Language = table.Column<string>(type: "character varying(8)", maxLength: 8, nullable: false),
+                    Title = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
+                    MangaId = table.Column<string>(type: "character varying(64)", nullable: false)
+                },
+                constraints: table =>
+                {
+                    table.PrimaryKey("PK_MangaAltTitle", x => x.AltTitleId);
+                    table.ForeignKey(
+                        name: "FK_MangaAltTitle_Mangas_MangaId",
+                        column: x => x.MangaId,
+                        principalTable: "Mangas",
+                        principalColumn: "MangaId",
+                        onDelete: ReferentialAction.Cascade);
+                });
+
+            migrationBuilder.CreateIndex(
+                name: "IX_Link_MangaId",
+                table: "Link",
+                column: "MangaId");
+
+            migrationBuilder.CreateIndex(
+                name: "IX_MangaAltTitle_MangaId",
+                table: "MangaAltTitle",
+                column: "MangaId");
+        }
+
+        /// <inheritdoc />
+        protected override void Down(MigrationBuilder migrationBuilder)
+        {
+            migrationBuilder.DropTable(
+                name: "Link");
+
+            migrationBuilder.DropTable(
+                name: "MangaAltTitle");
+
+            migrationBuilder.CreateTable(
+                name: "AltTitles",
+                columns: table => new
+                {
+                    AltTitleId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
+                    Language = table.Column<string>(type: "character varying(8)", maxLength: 8, nullable: false),
+                    MangaId = table.Column<string>(type: "character varying(64)", nullable: false),
+                    Title = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false)
+                },
+                constraints: table =>
+                {
+                    table.PrimaryKey("PK_AltTitles", x => x.AltTitleId);
+                    table.ForeignKey(
+                        name: "FK_AltTitles_Mangas_MangaId",
+                        column: x => x.MangaId,
+                        principalTable: "Mangas",
+                        principalColumn: "MangaId",
+                        onDelete: ReferentialAction.Cascade);
+                });
+
+            migrationBuilder.CreateTable(
+                name: "Links",
+                columns: table => new
+                {
+                    LinkId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
+                    LinkProvider = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
+                    LinkUrl = table.Column<string>(type: "character varying(2048)", maxLength: 2048, nullable: false),
+                    MangaId = table.Column<string>(type: "character varying(64)", nullable: false)
+                },
+                constraints: table =>
+                {
+                    table.PrimaryKey("PK_Links", x => x.LinkId);
+                    table.ForeignKey(
+                        name: "FK_Links_Mangas_MangaId",
+                        column: x => x.MangaId,
+                        principalTable: "Mangas",
+                        principalColumn: "MangaId",
+                        onDelete: ReferentialAction.Cascade);
+                });
+
+            migrationBuilder.CreateIndex(
+                name: "IX_AltTitles_MangaId",
+                table: "AltTitles",
+                column: "MangaId");
+
+            migrationBuilder.CreateIndex(
+                name: "IX_Links_MangaId",
+                table: "Links",
+                column: "MangaId");
+        }
+    }
+}
diff --git a/API/Migrations/20250316143014_dev-160325-Initial.Designer.cs b/API/Migrations/20250509035606_Initial-4.Designer.cs
similarity index 77%
rename from API/Migrations/20250316143014_dev-160325-Initial.Designer.cs
rename to API/Migrations/20250509035606_Initial-4.Designer.cs
index 24a7d03..07c02c3 100644
--- a/API/Migrations/20250316143014_dev-160325-Initial.Designer.cs
+++ b/API/Migrations/20250509035606_Initial-4.Designer.cs
@@ -13,8 +13,8 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
 namespace API.Migrations
 {
     [DbContext(typeof(PgsqlContext))]
-    [Migration("20250316143014_dev-160325-Initial")]
-    partial class dev160325Initial
+    [Migration("20250509035606_Initial-4")]
+    partial class Initial4
     {
         /// <inheritdoc />
         protected override void BuildTargetModel(ModelBuilder modelBuilder)
@@ -64,7 +64,6 @@ namespace API.Migrations
 
                     b.Property<string>("ParentMangaId")
                         .IsRequired()
-                        .HasMaxLength(64)
                         .HasColumnType("character varying(64)");
 
                     b.Property<string>("Title")
@@ -92,10 +91,6 @@ namespace API.Migrations
                         .HasMaxLength(64)
                         .HasColumnType("character varying(64)");
 
-                    b.PrimitiveCollection<string[]>("DependsOnJobsIds")
-                        .HasMaxLength(64)
-                        .HasColumnType("text[]");
-
                     b.Property<bool>("Enabled")
                         .HasColumnType("boolean");
 
@@ -106,6 +101,7 @@ namespace API.Migrations
                         .HasColumnType("timestamp with time zone");
 
                     b.Property<string>("ParentJobId")
+                        .IsRequired()
                         .HasMaxLength(64)
                         .HasColumnType("character varying(64)");
 
@@ -154,32 +150,6 @@ namespace API.Migrations
                     b.UseTphMappingStrategy();
                 });
 
-            modelBuilder.Entity("API.Schema.Link", b =>
-                {
-                    b.Property<string>("LinkId")
-                        .HasMaxLength(64)
-                        .HasColumnType("character varying(64)");
-
-                    b.Property<string>("LinkProvider")
-                        .IsRequired()
-                        .HasMaxLength(64)
-                        .HasColumnType("character varying(64)");
-
-                    b.Property<string>("LinkUrl")
-                        .IsRequired()
-                        .HasMaxLength(2048)
-                        .HasColumnType("character varying(2048)");
-
-                    b.Property<string>("MangaId")
-                        .HasColumnType("character varying(64)");
-
-                    b.HasKey("LinkId");
-
-                    b.HasIndex("MangaId");
-
-                    b.ToTable("Links");
-                });
-
             modelBuilder.Entity("API.Schema.LocalLibrary", b =>
                 {
                     b.Property<string>("LocalLibraryId")
@@ -208,11 +178,13 @@ namespace API.Migrations
                         .HasColumnType("character varying(64)");
 
                     b.Property<string>("CoverFileNameInCache")
-                        .HasColumnType("text");
+                        .HasMaxLength(512)
+                        .HasColumnType("character varying(512)");
 
                     b.Property<string>("CoverUrl")
                         .IsRequired()
-                        .HasColumnType("text");
+                        .HasMaxLength(512)
+                        .HasColumnType("character varying(512)");
 
                     b.Property<string>("Description")
                         .IsRequired()
@@ -220,32 +192,32 @@ namespace API.Migrations
 
                     b.Property<string>("DirectoryName")
                         .IsRequired()
-                        .HasMaxLength(256)
-                        .HasColumnType("character varying(256)");
+                        .HasMaxLength(1024)
+                        .HasColumnType("character varying(1024)");
 
                     b.Property<string>("IdOnConnectorSite")
-                        .IsRequired()
-                        .HasMaxLength(128)
-                        .HasColumnType("character varying(128)");
-
-                    b.Property<float>("IgnoreChapterBefore")
-                        .HasColumnType("real");
-
-                    b.Property<string>("LibraryLocalLibraryId")
-                        .HasColumnType("character varying(64)");
-
-                    b.Property<string>("MangaConnectorId")
-                        .IsRequired()
-                        .HasMaxLength(64)
-                        .HasColumnType("character varying(64)");
-
-                    b.Property<string>("Name")
                         .IsRequired()
                         .HasMaxLength(256)
                         .HasColumnType("character varying(256)");
 
-                    b.Property<string>("OriginalLanguage")
+                    b.Property<float>("IgnoreChaptersBefore")
+                        .HasColumnType("real");
+
+                    b.Property<string>("LibraryId")
+                        .HasMaxLength(64)
+                        .HasColumnType("character varying(64)");
+
+                    b.Property<string>("MangaConnectorName")
                         .IsRequired()
+                        .HasMaxLength(32)
+                        .HasColumnType("character varying(32)");
+
+                    b.Property<string>("Name")
+                        .IsRequired()
+                        .HasMaxLength(512)
+                        .HasColumnType("character varying(512)");
+
+                    b.Property<string>("OriginalLanguage")
                         .HasMaxLength(8)
                         .HasColumnType("character varying(8)");
 
@@ -254,47 +226,21 @@ namespace API.Migrations
 
                     b.Property<string>("WebsiteUrl")
                         .IsRequired()
-                        .HasMaxLength(256)
-                        .HasColumnType("character varying(256)");
+                        .HasMaxLength(512)
+                        .HasColumnType("character varying(512)");
 
                     b.Property<long>("Year")
                         .HasColumnType("bigint");
 
                     b.HasKey("MangaId");
 
-                    b.HasIndex("LibraryLocalLibraryId");
+                    b.HasIndex("LibraryId");
 
-                    b.HasIndex("MangaConnectorId");
+                    b.HasIndex("MangaConnectorName");
 
                     b.ToTable("Mangas");
                 });
 
-            modelBuilder.Entity("API.Schema.MangaAltTitle", b =>
-                {
-                    b.Property<string>("AltTitleId")
-                        .HasMaxLength(64)
-                        .HasColumnType("character varying(64)");
-
-                    b.Property<string>("Language")
-                        .IsRequired()
-                        .HasMaxLength(8)
-                        .HasColumnType("character varying(8)");
-
-                    b.Property<string>("MangaId")
-                        .HasColumnType("character varying(64)");
-
-                    b.Property<string>("Title")
-                        .IsRequired()
-                        .HasMaxLength(256)
-                        .HasColumnType("character varying(256)");
-
-                    b.HasKey("AltTitleId");
-
-                    b.HasIndex("MangaId");
-
-                    b.ToTable("AltTitles");
-                });
-
             modelBuilder.Entity("API.Schema.MangaConnectors.MangaConnector", b =>
                 {
                     b.Property<string>("Name")
@@ -396,19 +342,19 @@ namespace API.Migrations
                     b.ToTable("NotificationConnectors");
                 });
 
-            modelBuilder.Entity("AuthorManga", b =>
+            modelBuilder.Entity("AuthorToManga", b =>
                 {
-                    b.Property<string>("AuthorsAuthorId")
+                    b.Property<string>("AuthorIds")
                         .HasColumnType("character varying(64)");
 
-                    b.Property<string>("MangaId")
+                    b.Property<string>("MangaIds")
                         .HasColumnType("character varying(64)");
 
-                    b.HasKey("AuthorsAuthorId", "MangaId");
+                    b.HasKey("AuthorIds", "MangaIds");
 
-                    b.HasIndex("MangaId");
+                    b.HasIndex("MangaIds");
 
-                    b.ToTable("AuthorManga");
+                    b.ToTable("AuthorToManga");
                 });
 
             modelBuilder.Entity("JobJob", b =>
@@ -426,19 +372,19 @@ namespace API.Migrations
                     b.ToTable("JobJob");
                 });
 
-            modelBuilder.Entity("MangaMangaTag", b =>
+            modelBuilder.Entity("MangaTagToManga", b =>
                 {
-                    b.Property<string>("MangaId")
+                    b.Property<string>("MangaTagIds")
                         .HasColumnType("character varying(64)");
 
-                    b.Property<string>("MangaTagsTag")
+                    b.Property<string>("MangaIds")
                         .HasColumnType("character varying(64)");
 
-                    b.HasKey("MangaId", "MangaTagsTag");
+                    b.HasKey("MangaTagIds", "MangaIds");
 
-                    b.HasIndex("MangaTagsTag");
+                    b.HasIndex("MangaIds");
 
-                    b.ToTable("MangaMangaTag");
+                    b.ToTable("MangaTagToManga");
                 });
 
             modelBuilder.Entity("API.Schema.Jobs.DownloadAvailableChaptersJob", b =>
@@ -506,10 +452,42 @@ namespace API.Migrations
                     b.HasDiscriminator().HasValue((byte)3);
                 });
 
+            modelBuilder.Entity("API.Schema.Jobs.MoveMangaLibraryJob", b =>
+                {
+                    b.HasBaseType("API.Schema.Jobs.Job");
+
+                    b.Property<string>("MangaId")
+                        .IsRequired()
+                        .HasMaxLength(64)
+                        .HasColumnType("character varying(64)");
+
+                    b.Property<string>("ToLibraryId")
+                        .IsRequired()
+                        .HasMaxLength(64)
+                        .HasColumnType("character varying(64)");
+
+                    b.HasIndex("MangaId");
+
+                    b.HasIndex("ToLibraryId");
+
+                    b.ToTable("Jobs", t =>
+                        {
+                            t.Property("MangaId")
+                                .HasColumnName("MoveMangaLibraryJob_MangaId");
+                        });
+
+                    b.HasDiscriminator().HasValue((byte)7);
+                });
+
             modelBuilder.Entity("API.Schema.Jobs.RetrieveChaptersJob", b =>
                 {
                     b.HasBaseType("API.Schema.Jobs.Job");
 
+                    b.Property<string>("Language")
+                        .IsRequired()
+                        .HasMaxLength(8)
+                        .HasColumnType("character varying(8)");
+
                     b.Property<string>("MangaId")
                         .IsRequired()
                         .HasMaxLength(64)
@@ -546,26 +524,6 @@ namespace API.Migrations
                     b.HasDiscriminator().HasValue((byte)6);
                 });
 
-            modelBuilder.Entity("API.Schema.Jobs.UpdateMetadataJob", b =>
-                {
-                    b.HasBaseType("API.Schema.Jobs.Job");
-
-                    b.Property<string>("MangaId")
-                        .IsRequired()
-                        .HasMaxLength(64)
-                        .HasColumnType("character varying(64)");
-
-                    b.HasIndex("MangaId");
-
-                    b.ToTable("Jobs", t =>
-                        {
-                            t.Property("MangaId")
-                                .HasColumnName("UpdateMetadataJob_MangaId");
-                        });
-
-                    b.HasDiscriminator().HasValue((byte)2);
-                });
-
             modelBuilder.Entity("API.Schema.LibraryConnectors.Kavita", b =>
                 {
                     b.HasBaseType("API.Schema.LibraryConnectors.LibraryConnector");
@@ -580,18 +538,11 @@ namespace API.Migrations
                     b.HasDiscriminator().HasValue((byte)0);
                 });
 
-            modelBuilder.Entity("API.Schema.MangaConnectors.AsuraToon", b =>
+            modelBuilder.Entity("API.Schema.MangaConnectors.Global", b =>
                 {
                     b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
 
-                    b.HasDiscriminator().HasValue("AsuraToon");
-                });
-
-            modelBuilder.Entity("API.Schema.MangaConnectors.Bato", b =>
-                {
-                    b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
-
-                    b.HasDiscriminator().HasValue("Bato");
+                    b.HasDiscriminator().HasValue("Global");
                 });
 
             modelBuilder.Entity("API.Schema.MangaConnectors.MangaDex", b =>
@@ -601,52 +552,10 @@ namespace API.Migrations
                     b.HasDiscriminator().HasValue("MangaDex");
                 });
 
-            modelBuilder.Entity("API.Schema.MangaConnectors.MangaHere", b =>
-                {
-                    b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
-
-                    b.HasDiscriminator().HasValue("MangaHere");
-                });
-
-            modelBuilder.Entity("API.Schema.MangaConnectors.MangaKatana", b =>
-                {
-                    b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
-
-                    b.HasDiscriminator().HasValue("MangaKatana");
-                });
-
-            modelBuilder.Entity("API.Schema.MangaConnectors.Manganato", b =>
-                {
-                    b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
-
-                    b.HasDiscriminator().HasValue("Manganato");
-                });
-
-            modelBuilder.Entity("API.Schema.MangaConnectors.Mangaworld", b =>
-                {
-                    b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
-
-                    b.HasDiscriminator().HasValue("Mangaworld");
-                });
-
-            modelBuilder.Entity("API.Schema.MangaConnectors.ManhuaPlus", b =>
-                {
-                    b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
-
-                    b.HasDiscriminator().HasValue("ManhuaPlus");
-                });
-
-            modelBuilder.Entity("API.Schema.MangaConnectors.Weebcentral", b =>
-                {
-                    b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
-
-                    b.HasDiscriminator().HasValue("Weebcentral");
-                });
-
             modelBuilder.Entity("API.Schema.Chapter", b =>
                 {
                     b.HasOne("API.Schema.Manga", "ParentManga")
-                        .WithMany()
+                        .WithMany("Chapters")
                         .HasForeignKey("ParentMangaId")
                         .OnDelete(DeleteBehavior.Cascade)
                         .IsRequired();
@@ -664,51 +573,99 @@ namespace API.Migrations
                     b.Navigation("ParentJob");
                 });
 
-            modelBuilder.Entity("API.Schema.Link", b =>
-                {
-                    b.HasOne("API.Schema.Manga", null)
-                        .WithMany("Links")
-                        .HasForeignKey("MangaId")
-                        .OnDelete(DeleteBehavior.Cascade);
-                });
-
             modelBuilder.Entity("API.Schema.Manga", b =>
                 {
                     b.HasOne("API.Schema.LocalLibrary", "Library")
                         .WithMany()
-                        .HasForeignKey("LibraryLocalLibraryId")
-                        .OnDelete(DeleteBehavior.Cascade);
+                        .HasForeignKey("LibraryId")
+                        .OnDelete(DeleteBehavior.SetNull);
 
                     b.HasOne("API.Schema.MangaConnectors.MangaConnector", "MangaConnector")
                         .WithMany()
-                        .HasForeignKey("MangaConnectorId")
+                        .HasForeignKey("MangaConnectorName")
                         .OnDelete(DeleteBehavior.Cascade)
                         .IsRequired();
 
+                    b.OwnsMany("API.Schema.Link", "Links", b1 =>
+                        {
+                            b1.Property<string>("LinkId")
+                                .HasMaxLength(64)
+                                .HasColumnType("character varying(64)");
+
+                            b1.Property<string>("LinkProvider")
+                                .IsRequired()
+                                .HasMaxLength(64)
+                                .HasColumnType("character varying(64)");
+
+                            b1.Property<string>("LinkUrl")
+                                .IsRequired()
+                                .HasMaxLength(2048)
+                                .HasColumnType("character varying(2048)");
+
+                            b1.Property<string>("MangaId")
+                                .IsRequired()
+                                .HasColumnType("character varying(64)");
+
+                            b1.HasKey("LinkId");
+
+                            b1.HasIndex("MangaId");
+
+                            b1.ToTable("Link");
+
+                            b1.WithOwner()
+                                .HasForeignKey("MangaId");
+                        });
+
+                    b.OwnsMany("API.Schema.MangaAltTitle", "AltTitles", b1 =>
+                        {
+                            b1.Property<string>("AltTitleId")
+                                .HasMaxLength(64)
+                                .HasColumnType("character varying(64)");
+
+                            b1.Property<string>("Language")
+                                .IsRequired()
+                                .HasMaxLength(8)
+                                .HasColumnType("character varying(8)");
+
+                            b1.Property<string>("MangaId")
+                                .IsRequired()
+                                .HasColumnType("character varying(64)");
+
+                            b1.Property<string>("Title")
+                                .IsRequired()
+                                .HasMaxLength(256)
+                                .HasColumnType("character varying(256)");
+
+                            b1.HasKey("AltTitleId");
+
+                            b1.HasIndex("MangaId");
+
+                            b1.ToTable("MangaAltTitle");
+
+                            b1.WithOwner()
+                                .HasForeignKey("MangaId");
+                        });
+
+                    b.Navigation("AltTitles");
+
                     b.Navigation("Library");
 
+                    b.Navigation("Links");
+
                     b.Navigation("MangaConnector");
                 });
 
-            modelBuilder.Entity("API.Schema.MangaAltTitle", b =>
-                {
-                    b.HasOne("API.Schema.Manga", null)
-                        .WithMany("AltTitles")
-                        .HasForeignKey("MangaId")
-                        .OnDelete(DeleteBehavior.Cascade);
-                });
-
-            modelBuilder.Entity("AuthorManga", b =>
+            modelBuilder.Entity("AuthorToManga", b =>
                 {
                     b.HasOne("API.Schema.Author", null)
                         .WithMany()
-                        .HasForeignKey("AuthorsAuthorId")
+                        .HasForeignKey("AuthorIds")
                         .OnDelete(DeleteBehavior.Cascade)
                         .IsRequired();
 
                     b.HasOne("API.Schema.Manga", null)
                         .WithMany()
-                        .HasForeignKey("MangaId")
+                        .HasForeignKey("MangaIds")
                         .OnDelete(DeleteBehavior.Cascade)
                         .IsRequired();
                 });
@@ -728,17 +685,17 @@ namespace API.Migrations
                         .IsRequired();
                 });
 
-            modelBuilder.Entity("MangaMangaTag", b =>
+            modelBuilder.Entity("MangaTagToManga", b =>
                 {
                     b.HasOne("API.Schema.Manga", null)
                         .WithMany()
-                        .HasForeignKey("MangaId")
+                        .HasForeignKey("MangaIds")
                         .OnDelete(DeleteBehavior.Cascade)
                         .IsRequired();
 
                     b.HasOne("API.Schema.MangaTag", null)
                         .WithMany()
-                        .HasForeignKey("MangaTagsTag")
+                        .HasForeignKey("MangaTagIds")
                         .OnDelete(DeleteBehavior.Cascade)
                         .IsRequired();
                 });
@@ -776,6 +733,25 @@ namespace API.Migrations
                     b.Navigation("Chapter");
                 });
 
+            modelBuilder.Entity("API.Schema.Jobs.MoveMangaLibraryJob", b =>
+                {
+                    b.HasOne("API.Schema.Manga", "Manga")
+                        .WithMany()
+                        .HasForeignKey("MangaId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.HasOne("API.Schema.LocalLibrary", "ToLibrary")
+                        .WithMany()
+                        .HasForeignKey("ToLibraryId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("Manga");
+
+                    b.Navigation("ToLibrary");
+                });
+
             modelBuilder.Entity("API.Schema.Jobs.RetrieveChaptersJob", b =>
                 {
                     b.HasOne("API.Schema.Manga", "Manga")
@@ -798,22 +774,9 @@ namespace API.Migrations
                     b.Navigation("Manga");
                 });
 
-            modelBuilder.Entity("API.Schema.Jobs.UpdateMetadataJob", b =>
-                {
-                    b.HasOne("API.Schema.Manga", "Manga")
-                        .WithMany()
-                        .HasForeignKey("MangaId")
-                        .OnDelete(DeleteBehavior.Cascade)
-                        .IsRequired();
-
-                    b.Navigation("Manga");
-                });
-
             modelBuilder.Entity("API.Schema.Manga", b =>
                 {
-                    b.Navigation("AltTitles");
-
-                    b.Navigation("Links");
+                    b.Navigation("Chapters");
                 });
 #pragma warning restore 612, 618
         }
diff --git a/API/Migrations/20250401001439_dev-010425-1.cs b/API/Migrations/20250509035606_Initial-4.cs
similarity index 62%
rename from API/Migrations/20250401001439_dev-010425-1.cs
rename to API/Migrations/20250509035606_Initial-4.cs
index c5d27f5..f81421f 100644
--- a/API/Migrations/20250401001439_dev-010425-1.cs
+++ b/API/Migrations/20250509035606_Initial-4.cs
@@ -5,35 +5,35 @@
 namespace API.Migrations
 {
     /// <inheritdoc />
-    public partial class dev0104251 : Migration
+    public partial class Initial4 : Migration
     {
         /// <inheritdoc />
         protected override void Up(MigrationBuilder migrationBuilder)
         {
             migrationBuilder.AlterColumn<string>(
-                name: "OriginalLanguage",
+                name: "LibraryId",
                 table: "Mangas",
-                type: "character varying(8)",
-                maxLength: 8,
+                type: "character varying(64)",
+                maxLength: 64,
                 nullable: true,
                 oldClrType: typeof(string),
-                oldType: "character varying(8)",
-                oldMaxLength: 8);
+                oldType: "character varying(64)",
+                oldMaxLength: 64);
         }
 
         /// <inheritdoc />
         protected override void Down(MigrationBuilder migrationBuilder)
         {
             migrationBuilder.AlterColumn<string>(
-                name: "OriginalLanguage",
+                name: "LibraryId",
                 table: "Mangas",
-                type: "character varying(8)",
-                maxLength: 8,
+                type: "character varying(64)",
+                maxLength: 64,
                 nullable: false,
                 defaultValue: "",
                 oldClrType: typeof(string),
-                oldType: "character varying(8)",
-                oldMaxLength: 8,
+                oldType: "character varying(64)",
+                oldMaxLength: 64,
                 oldNullable: true);
         }
     }
diff --git a/API/Migrations/20250509035754_Initial-5.Designer.cs b/API/Migrations/20250509035754_Initial-5.Designer.cs
new file mode 100644
index 0000000..22b2b57
--- /dev/null
+++ b/API/Migrations/20250509035754_Initial-5.Designer.cs
@@ -0,0 +1,783 @@
+// <auto-generated />
+using System;
+using System.Collections.Generic;
+using API.Schema;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+
+#nullable disable
+
+namespace API.Migrations
+{
+    [DbContext(typeof(PgsqlContext))]
+    [Migration("20250509035754_Initial-5")]
+    partial class Initial5
+    {
+        /// <inheritdoc />
+        protected override void BuildTargetModel(ModelBuilder modelBuilder)
+        {
+#pragma warning disable 612, 618
+            modelBuilder
+                .HasAnnotation("ProductVersion", "9.0.3")
+                .HasAnnotation("Relational:MaxIdentifierLength", 63);
+
+            NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "hstore");
+            NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
+
+            modelBuilder.Entity("API.Schema.Author", b =>
+                {
+                    b.Property<string>("AuthorId")
+                        .HasMaxLength(64)
+                        .HasColumnType("character varying(64)");
+
+                    b.Property<string>("AuthorName")
+                        .IsRequired()
+                        .HasMaxLength(128)
+                        .HasColumnType("character varying(128)");
+
+                    b.HasKey("AuthorId");
+
+                    b.ToTable("Authors");
+                });
+
+            modelBuilder.Entity("API.Schema.Chapter", b =>
+                {
+                    b.Property<string>("ChapterId")
+                        .HasMaxLength(64)
+                        .HasColumnType("character varying(64)");
+
+                    b.Property<string>("ChapterNumber")
+                        .IsRequired()
+                        .HasMaxLength(10)
+                        .HasColumnType("character varying(10)");
+
+                    b.Property<bool>("Downloaded")
+                        .HasColumnType("boolean");
+
+                    b.Property<string>("FileName")
+                        .IsRequired()
+                        .HasMaxLength(256)
+                        .HasColumnType("character varying(256)");
+
+                    b.Property<string>("ParentMangaId")
+                        .IsRequired()
+                        .HasColumnType("character varying(64)");
+
+                    b.Property<string>("Title")
+                        .HasMaxLength(256)
+                        .HasColumnType("character varying(256)");
+
+                    b.Property<string>("Url")
+                        .IsRequired()
+                        .HasMaxLength(2048)
+                        .HasColumnType("character varying(2048)");
+
+                    b.Property<int?>("VolumeNumber")
+                        .HasColumnType("integer");
+
+                    b.HasKey("ChapterId");
+
+                    b.HasIndex("ParentMangaId");
+
+                    b.ToTable("Chapters");
+                });
+
+            modelBuilder.Entity("API.Schema.Jobs.Job", b =>
+                {
+                    b.Property<string>("JobId")
+                        .HasMaxLength(64)
+                        .HasColumnType("character varying(64)");
+
+                    b.Property<bool>("Enabled")
+                        .HasColumnType("boolean");
+
+                    b.Property<byte>("JobType")
+                        .HasColumnType("smallint");
+
+                    b.Property<DateTime>("LastExecution")
+                        .HasColumnType("timestamp with time zone");
+
+                    b.Property<string>("ParentJobId")
+                        .HasMaxLength(64)
+                        .HasColumnType("character varying(64)");
+
+                    b.Property<decimal>("RecurrenceMs")
+                        .HasColumnType("numeric(20,0)");
+
+                    b.Property<byte>("state")
+                        .HasColumnType("smallint");
+
+                    b.HasKey("JobId");
+
+                    b.HasIndex("ParentJobId");
+
+                    b.ToTable("Jobs");
+
+                    b.HasDiscriminator<byte>("JobType");
+
+                    b.UseTphMappingStrategy();
+                });
+
+            modelBuilder.Entity("API.Schema.LibraryConnectors.LibraryConnector", b =>
+                {
+                    b.Property<string>("LibraryConnectorId")
+                        .HasMaxLength(64)
+                        .HasColumnType("character varying(64)");
+
+                    b.Property<string>("Auth")
+                        .IsRequired()
+                        .HasMaxLength(256)
+                        .HasColumnType("character varying(256)");
+
+                    b.Property<string>("BaseUrl")
+                        .IsRequired()
+                        .HasMaxLength(256)
+                        .HasColumnType("character varying(256)");
+
+                    b.Property<byte>("LibraryType")
+                        .HasColumnType("smallint");
+
+                    b.HasKey("LibraryConnectorId");
+
+                    b.ToTable("LibraryConnectors");
+
+                    b.HasDiscriminator<byte>("LibraryType");
+
+                    b.UseTphMappingStrategy();
+                });
+
+            modelBuilder.Entity("API.Schema.LocalLibrary", b =>
+                {
+                    b.Property<string>("LocalLibraryId")
+                        .HasMaxLength(64)
+                        .HasColumnType("character varying(64)");
+
+                    b.Property<string>("BasePath")
+                        .IsRequired()
+                        .HasMaxLength(256)
+                        .HasColumnType("character varying(256)");
+
+                    b.Property<string>("LibraryName")
+                        .IsRequired()
+                        .HasMaxLength(512)
+                        .HasColumnType("character varying(512)");
+
+                    b.HasKey("LocalLibraryId");
+
+                    b.ToTable("LocalLibraries");
+                });
+
+            modelBuilder.Entity("API.Schema.Manga", b =>
+                {
+                    b.Property<string>("MangaId")
+                        .HasMaxLength(64)
+                        .HasColumnType("character varying(64)");
+
+                    b.Property<string>("CoverFileNameInCache")
+                        .HasMaxLength(512)
+                        .HasColumnType("character varying(512)");
+
+                    b.Property<string>("CoverUrl")
+                        .IsRequired()
+                        .HasMaxLength(512)
+                        .HasColumnType("character varying(512)");
+
+                    b.Property<string>("Description")
+                        .IsRequired()
+                        .HasColumnType("text");
+
+                    b.Property<string>("DirectoryName")
+                        .IsRequired()
+                        .HasMaxLength(1024)
+                        .HasColumnType("character varying(1024)");
+
+                    b.Property<string>("IdOnConnectorSite")
+                        .IsRequired()
+                        .HasMaxLength(256)
+                        .HasColumnType("character varying(256)");
+
+                    b.Property<float>("IgnoreChaptersBefore")
+                        .HasColumnType("real");
+
+                    b.Property<string>("LibraryId")
+                        .HasMaxLength(64)
+                        .HasColumnType("character varying(64)");
+
+                    b.Property<string>("MangaConnectorName")
+                        .IsRequired()
+                        .HasMaxLength(32)
+                        .HasColumnType("character varying(32)");
+
+                    b.Property<string>("Name")
+                        .IsRequired()
+                        .HasMaxLength(512)
+                        .HasColumnType("character varying(512)");
+
+                    b.Property<string>("OriginalLanguage")
+                        .HasMaxLength(8)
+                        .HasColumnType("character varying(8)");
+
+                    b.Property<byte>("ReleaseStatus")
+                        .HasColumnType("smallint");
+
+                    b.Property<string>("WebsiteUrl")
+                        .IsRequired()
+                        .HasMaxLength(512)
+                        .HasColumnType("character varying(512)");
+
+                    b.Property<long>("Year")
+                        .HasColumnType("bigint");
+
+                    b.HasKey("MangaId");
+
+                    b.HasIndex("LibraryId");
+
+                    b.HasIndex("MangaConnectorName");
+
+                    b.ToTable("Mangas");
+                });
+
+            modelBuilder.Entity("API.Schema.MangaConnectors.MangaConnector", b =>
+                {
+                    b.Property<string>("Name")
+                        .HasMaxLength(32)
+                        .HasColumnType("character varying(32)");
+
+                    b.PrimitiveCollection<string[]>("BaseUris")
+                        .IsRequired()
+                        .HasMaxLength(256)
+                        .HasColumnType("text[]");
+
+                    b.Property<bool>("Enabled")
+                        .HasColumnType("boolean");
+
+                    b.Property<string>("IconUrl")
+                        .IsRequired()
+                        .HasMaxLength(2048)
+                        .HasColumnType("character varying(2048)");
+
+                    b.PrimitiveCollection<string[]>("SupportedLanguages")
+                        .IsRequired()
+                        .HasMaxLength(8)
+                        .HasColumnType("text[]");
+
+                    b.HasKey("Name");
+
+                    b.ToTable("MangaConnectors");
+
+                    b.HasDiscriminator<string>("Name").HasValue("MangaConnector");
+
+                    b.UseTphMappingStrategy();
+                });
+
+            modelBuilder.Entity("API.Schema.MangaTag", b =>
+                {
+                    b.Property<string>("Tag")
+                        .HasMaxLength(64)
+                        .HasColumnType("character varying(64)");
+
+                    b.HasKey("Tag");
+
+                    b.ToTable("Tags");
+                });
+
+            modelBuilder.Entity("API.Schema.Notification", b =>
+                {
+                    b.Property<string>("NotificationId")
+                        .HasMaxLength(64)
+                        .HasColumnType("character varying(64)");
+
+                    b.Property<DateTime>("Date")
+                        .HasColumnType("timestamp with time zone");
+
+                    b.Property<string>("Message")
+                        .IsRequired()
+                        .HasMaxLength(512)
+                        .HasColumnType("character varying(512)");
+
+                    b.Property<string>("Title")
+                        .IsRequired()
+                        .HasMaxLength(128)
+                        .HasColumnType("character varying(128)");
+
+                    b.Property<byte>("Urgency")
+                        .HasColumnType("smallint");
+
+                    b.HasKey("NotificationId");
+
+                    b.ToTable("Notifications");
+                });
+
+            modelBuilder.Entity("API.Schema.NotificationConnectors.NotificationConnector", b =>
+                {
+                    b.Property<string>("Name")
+                        .HasMaxLength(64)
+                        .HasColumnType("character varying(64)");
+
+                    b.Property<string>("Body")
+                        .IsRequired()
+                        .HasMaxLength(4096)
+                        .HasColumnType("character varying(4096)");
+
+                    b.Property<Dictionary<string, string>>("Headers")
+                        .IsRequired()
+                        .HasColumnType("hstore");
+
+                    b.Property<string>("HttpMethod")
+                        .IsRequired()
+                        .HasMaxLength(8)
+                        .HasColumnType("character varying(8)");
+
+                    b.Property<string>("Url")
+                        .IsRequired()
+                        .HasMaxLength(2048)
+                        .HasColumnType("character varying(2048)");
+
+                    b.HasKey("Name");
+
+                    b.ToTable("NotificationConnectors");
+                });
+
+            modelBuilder.Entity("AuthorToManga", b =>
+                {
+                    b.Property<string>("AuthorIds")
+                        .HasColumnType("character varying(64)");
+
+                    b.Property<string>("MangaIds")
+                        .HasColumnType("character varying(64)");
+
+                    b.HasKey("AuthorIds", "MangaIds");
+
+                    b.HasIndex("MangaIds");
+
+                    b.ToTable("AuthorToManga");
+                });
+
+            modelBuilder.Entity("JobJob", b =>
+                {
+                    b.Property<string>("DependsOnJobsJobId")
+                        .HasColumnType("character varying(64)");
+
+                    b.Property<string>("JobId")
+                        .HasColumnType("character varying(64)");
+
+                    b.HasKey("DependsOnJobsJobId", "JobId");
+
+                    b.HasIndex("JobId");
+
+                    b.ToTable("JobJob");
+                });
+
+            modelBuilder.Entity("MangaTagToManga", b =>
+                {
+                    b.Property<string>("MangaTagIds")
+                        .HasColumnType("character varying(64)");
+
+                    b.Property<string>("MangaIds")
+                        .HasColumnType("character varying(64)");
+
+                    b.HasKey("MangaTagIds", "MangaIds");
+
+                    b.HasIndex("MangaIds");
+
+                    b.ToTable("MangaTagToManga");
+                });
+
+            modelBuilder.Entity("API.Schema.Jobs.DownloadAvailableChaptersJob", b =>
+                {
+                    b.HasBaseType("API.Schema.Jobs.Job");
+
+                    b.Property<string>("MangaId")
+                        .IsRequired()
+                        .HasMaxLength(64)
+                        .HasColumnType("character varying(64)");
+
+                    b.HasIndex("MangaId");
+
+                    b.ToTable("Jobs", t =>
+                        {
+                            t.Property("MangaId")
+                                .HasColumnName("DownloadAvailableChaptersJob_MangaId");
+                        });
+
+                    b.HasDiscriminator().HasValue((byte)1);
+                });
+
+            modelBuilder.Entity("API.Schema.Jobs.DownloadMangaCoverJob", b =>
+                {
+                    b.HasBaseType("API.Schema.Jobs.Job");
+
+                    b.Property<string>("MangaId")
+                        .IsRequired()
+                        .HasMaxLength(64)
+                        .HasColumnType("character varying(64)");
+
+                    b.HasIndex("MangaId");
+
+                    b.HasDiscriminator().HasValue((byte)4);
+                });
+
+            modelBuilder.Entity("API.Schema.Jobs.DownloadSingleChapterJob", b =>
+                {
+                    b.HasBaseType("API.Schema.Jobs.Job");
+
+                    b.Property<string>("ChapterId")
+                        .IsRequired()
+                        .HasMaxLength(64)
+                        .HasColumnType("character varying(64)");
+
+                    b.HasIndex("ChapterId");
+
+                    b.HasDiscriminator().HasValue((byte)0);
+                });
+
+            modelBuilder.Entity("API.Schema.Jobs.MoveFileOrFolderJob", b =>
+                {
+                    b.HasBaseType("API.Schema.Jobs.Job");
+
+                    b.Property<string>("FromLocation")
+                        .IsRequired()
+                        .HasMaxLength(256)
+                        .HasColumnType("character varying(256)");
+
+                    b.Property<string>("ToLocation")
+                        .IsRequired()
+                        .HasMaxLength(256)
+                        .HasColumnType("character varying(256)");
+
+                    b.HasDiscriminator().HasValue((byte)3);
+                });
+
+            modelBuilder.Entity("API.Schema.Jobs.MoveMangaLibraryJob", b =>
+                {
+                    b.HasBaseType("API.Schema.Jobs.Job");
+
+                    b.Property<string>("MangaId")
+                        .IsRequired()
+                        .HasMaxLength(64)
+                        .HasColumnType("character varying(64)");
+
+                    b.Property<string>("ToLibraryId")
+                        .IsRequired()
+                        .HasMaxLength(64)
+                        .HasColumnType("character varying(64)");
+
+                    b.HasIndex("MangaId");
+
+                    b.HasIndex("ToLibraryId");
+
+                    b.ToTable("Jobs", t =>
+                        {
+                            t.Property("MangaId")
+                                .HasColumnName("MoveMangaLibraryJob_MangaId");
+                        });
+
+                    b.HasDiscriminator().HasValue((byte)7);
+                });
+
+            modelBuilder.Entity("API.Schema.Jobs.RetrieveChaptersJob", b =>
+                {
+                    b.HasBaseType("API.Schema.Jobs.Job");
+
+                    b.Property<string>("Language")
+                        .IsRequired()
+                        .HasMaxLength(8)
+                        .HasColumnType("character varying(8)");
+
+                    b.Property<string>("MangaId")
+                        .IsRequired()
+                        .HasMaxLength(64)
+                        .HasColumnType("character varying(64)");
+
+                    b.HasIndex("MangaId");
+
+                    b.ToTable("Jobs", t =>
+                        {
+                            t.Property("MangaId")
+                                .HasColumnName("RetrieveChaptersJob_MangaId");
+                        });
+
+                    b.HasDiscriminator().HasValue((byte)5);
+                });
+
+            modelBuilder.Entity("API.Schema.Jobs.UpdateFilesDownloadedJob", b =>
+                {
+                    b.HasBaseType("API.Schema.Jobs.Job");
+
+                    b.Property<string>("MangaId")
+                        .IsRequired()
+                        .HasMaxLength(64)
+                        .HasColumnType("character varying(64)");
+
+                    b.HasIndex("MangaId");
+
+                    b.ToTable("Jobs", t =>
+                        {
+                            t.Property("MangaId")
+                                .HasColumnName("UpdateFilesDownloadedJob_MangaId");
+                        });
+
+                    b.HasDiscriminator().HasValue((byte)6);
+                });
+
+            modelBuilder.Entity("API.Schema.LibraryConnectors.Kavita", b =>
+                {
+                    b.HasBaseType("API.Schema.LibraryConnectors.LibraryConnector");
+
+                    b.HasDiscriminator().HasValue((byte)1);
+                });
+
+            modelBuilder.Entity("API.Schema.LibraryConnectors.Komga", b =>
+                {
+                    b.HasBaseType("API.Schema.LibraryConnectors.LibraryConnector");
+
+                    b.HasDiscriminator().HasValue((byte)0);
+                });
+
+            modelBuilder.Entity("API.Schema.MangaConnectors.Global", b =>
+                {
+                    b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
+
+                    b.HasDiscriminator().HasValue("Global");
+                });
+
+            modelBuilder.Entity("API.Schema.MangaConnectors.MangaDex", b =>
+                {
+                    b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
+
+                    b.HasDiscriminator().HasValue("MangaDex");
+                });
+
+            modelBuilder.Entity("API.Schema.Chapter", b =>
+                {
+                    b.HasOne("API.Schema.Manga", "ParentManga")
+                        .WithMany("Chapters")
+                        .HasForeignKey("ParentMangaId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("ParentManga");
+                });
+
+            modelBuilder.Entity("API.Schema.Jobs.Job", b =>
+                {
+                    b.HasOne("API.Schema.Jobs.Job", "ParentJob")
+                        .WithMany()
+                        .HasForeignKey("ParentJobId")
+                        .OnDelete(DeleteBehavior.Cascade);
+
+                    b.Navigation("ParentJob");
+                });
+
+            modelBuilder.Entity("API.Schema.Manga", b =>
+                {
+                    b.HasOne("API.Schema.LocalLibrary", "Library")
+                        .WithMany()
+                        .HasForeignKey("LibraryId")
+                        .OnDelete(DeleteBehavior.SetNull);
+
+                    b.HasOne("API.Schema.MangaConnectors.MangaConnector", "MangaConnector")
+                        .WithMany()
+                        .HasForeignKey("MangaConnectorName")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.OwnsMany("API.Schema.Link", "Links", b1 =>
+                        {
+                            b1.Property<string>("LinkId")
+                                .HasMaxLength(64)
+                                .HasColumnType("character varying(64)");
+
+                            b1.Property<string>("LinkProvider")
+                                .IsRequired()
+                                .HasMaxLength(64)
+                                .HasColumnType("character varying(64)");
+
+                            b1.Property<string>("LinkUrl")
+                                .IsRequired()
+                                .HasMaxLength(2048)
+                                .HasColumnType("character varying(2048)");
+
+                            b1.Property<string>("MangaId")
+                                .IsRequired()
+                                .HasColumnType("character varying(64)");
+
+                            b1.HasKey("LinkId");
+
+                            b1.HasIndex("MangaId");
+
+                            b1.ToTable("Link");
+
+                            b1.WithOwner()
+                                .HasForeignKey("MangaId");
+                        });
+
+                    b.OwnsMany("API.Schema.MangaAltTitle", "AltTitles", b1 =>
+                        {
+                            b1.Property<string>("AltTitleId")
+                                .HasMaxLength(64)
+                                .HasColumnType("character varying(64)");
+
+                            b1.Property<string>("Language")
+                                .IsRequired()
+                                .HasMaxLength(8)
+                                .HasColumnType("character varying(8)");
+
+                            b1.Property<string>("MangaId")
+                                .IsRequired()
+                                .HasColumnType("character varying(64)");
+
+                            b1.Property<string>("Title")
+                                .IsRequired()
+                                .HasMaxLength(256)
+                                .HasColumnType("character varying(256)");
+
+                            b1.HasKey("AltTitleId");
+
+                            b1.HasIndex("MangaId");
+
+                            b1.ToTable("MangaAltTitle");
+
+                            b1.WithOwner()
+                                .HasForeignKey("MangaId");
+                        });
+
+                    b.Navigation("AltTitles");
+
+                    b.Navigation("Library");
+
+                    b.Navigation("Links");
+
+                    b.Navigation("MangaConnector");
+                });
+
+            modelBuilder.Entity("AuthorToManga", b =>
+                {
+                    b.HasOne("API.Schema.Author", null)
+                        .WithMany()
+                        .HasForeignKey("AuthorIds")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.HasOne("API.Schema.Manga", null)
+                        .WithMany()
+                        .HasForeignKey("MangaIds")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+                });
+
+            modelBuilder.Entity("JobJob", b =>
+                {
+                    b.HasOne("API.Schema.Jobs.Job", null)
+                        .WithMany()
+                        .HasForeignKey("DependsOnJobsJobId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.HasOne("API.Schema.Jobs.Job", null)
+                        .WithMany()
+                        .HasForeignKey("JobId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+                });
+
+            modelBuilder.Entity("MangaTagToManga", b =>
+                {
+                    b.HasOne("API.Schema.Manga", null)
+                        .WithMany()
+                        .HasForeignKey("MangaIds")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.HasOne("API.Schema.MangaTag", null)
+                        .WithMany()
+                        .HasForeignKey("MangaTagIds")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+                });
+
+            modelBuilder.Entity("API.Schema.Jobs.DownloadAvailableChaptersJob", b =>
+                {
+                    b.HasOne("API.Schema.Manga", "Manga")
+                        .WithMany()
+                        .HasForeignKey("MangaId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("Manga");
+                });
+
+            modelBuilder.Entity("API.Schema.Jobs.DownloadMangaCoverJob", b =>
+                {
+                    b.HasOne("API.Schema.Manga", "Manga")
+                        .WithMany()
+                        .HasForeignKey("MangaId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("Manga");
+                });
+
+            modelBuilder.Entity("API.Schema.Jobs.DownloadSingleChapterJob", b =>
+                {
+                    b.HasOne("API.Schema.Chapter", "Chapter")
+                        .WithMany()
+                        .HasForeignKey("ChapterId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("Chapter");
+                });
+
+            modelBuilder.Entity("API.Schema.Jobs.MoveMangaLibraryJob", b =>
+                {
+                    b.HasOne("API.Schema.Manga", "Manga")
+                        .WithMany()
+                        .HasForeignKey("MangaId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.HasOne("API.Schema.LocalLibrary", "ToLibrary")
+                        .WithMany()
+                        .HasForeignKey("ToLibraryId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("Manga");
+
+                    b.Navigation("ToLibrary");
+                });
+
+            modelBuilder.Entity("API.Schema.Jobs.RetrieveChaptersJob", b =>
+                {
+                    b.HasOne("API.Schema.Manga", "Manga")
+                        .WithMany()
+                        .HasForeignKey("MangaId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("Manga");
+                });
+
+            modelBuilder.Entity("API.Schema.Jobs.UpdateFilesDownloadedJob", b =>
+                {
+                    b.HasOne("API.Schema.Manga", "Manga")
+                        .WithMany()
+                        .HasForeignKey("MangaId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("Manga");
+                });
+
+            modelBuilder.Entity("API.Schema.Manga", b =>
+                {
+                    b.Navigation("Chapters");
+                });
+#pragma warning restore 612, 618
+        }
+    }
+}
diff --git a/API/Migrations/20250509035754_Initial-5.cs b/API/Migrations/20250509035754_Initial-5.cs
new file mode 100644
index 0000000..aadc22d
--- /dev/null
+++ b/API/Migrations/20250509035754_Initial-5.cs
@@ -0,0 +1,40 @@
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace API.Migrations
+{
+    /// <inheritdoc />
+    public partial class Initial5 : Migration
+    {
+        /// <inheritdoc />
+        protected override void Up(MigrationBuilder migrationBuilder)
+        {
+            migrationBuilder.AlterColumn<string>(
+                name: "ParentJobId",
+                table: "Jobs",
+                type: "character varying(64)",
+                maxLength: 64,
+                nullable: true,
+                oldClrType: typeof(string),
+                oldType: "character varying(64)",
+                oldMaxLength: 64);
+        }
+
+        /// <inheritdoc />
+        protected override void Down(MigrationBuilder migrationBuilder)
+        {
+            migrationBuilder.AlterColumn<string>(
+                name: "ParentJobId",
+                table: "Jobs",
+                type: "character varying(64)",
+                maxLength: 64,
+                nullable: false,
+                defaultValue: "",
+                oldClrType: typeof(string),
+                oldType: "character varying(64)",
+                oldMaxLength: 64,
+                oldNullable: true);
+        }
+    }
+}
diff --git a/API/Migrations/PgsqlContextModelSnapshot.cs b/API/Migrations/PgsqlContextModelSnapshot.cs
index c290382..a17d8c2 100644
--- a/API/Migrations/PgsqlContextModelSnapshot.cs
+++ b/API/Migrations/PgsqlContextModelSnapshot.cs
@@ -61,7 +61,6 @@ namespace API.Migrations
 
                     b.Property<string>("ParentMangaId")
                         .IsRequired()
-                        .HasMaxLength(64)
                         .HasColumnType("character varying(64)");
 
                     b.Property<string>("Title")
@@ -89,10 +88,6 @@ namespace API.Migrations
                         .HasMaxLength(64)
                         .HasColumnType("character varying(64)");
 
-                    b.PrimitiveCollection<string[]>("DependsOnJobsIds")
-                        .HasMaxLength(64)
-                        .HasColumnType("text[]");
-
                     b.Property<bool>("Enabled")
                         .HasColumnType("boolean");
 
@@ -151,32 +146,6 @@ namespace API.Migrations
                     b.UseTphMappingStrategy();
                 });
 
-            modelBuilder.Entity("API.Schema.Link", b =>
-                {
-                    b.Property<string>("LinkId")
-                        .HasMaxLength(64)
-                        .HasColumnType("character varying(64)");
-
-                    b.Property<string>("LinkProvider")
-                        .IsRequired()
-                        .HasMaxLength(64)
-                        .HasColumnType("character varying(64)");
-
-                    b.Property<string>("LinkUrl")
-                        .IsRequired()
-                        .HasMaxLength(2048)
-                        .HasColumnType("character varying(2048)");
-
-                    b.Property<string>("MangaId")
-                        .HasColumnType("character varying(64)");
-
-                    b.HasKey("LinkId");
-
-                    b.HasIndex("MangaId");
-
-                    b.ToTable("Links");
-                });
-
             modelBuilder.Entity("API.Schema.LocalLibrary", b =>
                 {
                     b.Property<string>("LocalLibraryId")
@@ -205,11 +174,13 @@ namespace API.Migrations
                         .HasColumnType("character varying(64)");
 
                     b.Property<string>("CoverFileNameInCache")
-                        .HasColumnType("text");
+                        .HasMaxLength(512)
+                        .HasColumnType("character varying(512)");
 
                     b.Property<string>("CoverUrl")
                         .IsRequired()
-                        .HasColumnType("text");
+                        .HasMaxLength(512)
+                        .HasColumnType("character varying(512)");
 
                     b.Property<string>("Description")
                         .IsRequired()
@@ -225,17 +196,18 @@ namespace API.Migrations
                         .HasMaxLength(256)
                         .HasColumnType("character varying(256)");
 
-                    b.Property<float>("IgnoreChapterBefore")
+                    b.Property<float>("IgnoreChaptersBefore")
                         .HasColumnType("real");
 
-                    b.Property<string>("LibraryLocalLibraryId")
-                        .HasColumnType("character varying(64)");
-
-                    b.Property<string>("MangaConnectorId")
-                        .IsRequired()
+                    b.Property<string>("LibraryId")
                         .HasMaxLength(64)
                         .HasColumnType("character varying(64)");
 
+                    b.Property<string>("MangaConnectorName")
+                        .IsRequired()
+                        .HasMaxLength(32)
+                        .HasColumnType("character varying(32)");
+
                     b.Property<string>("Name")
                         .IsRequired()
                         .HasMaxLength(512)
@@ -258,39 +230,13 @@ namespace API.Migrations
 
                     b.HasKey("MangaId");
 
-                    b.HasIndex("LibraryLocalLibraryId");
+                    b.HasIndex("LibraryId");
 
-                    b.HasIndex("MangaConnectorId");
+                    b.HasIndex("MangaConnectorName");
 
                     b.ToTable("Mangas");
                 });
 
-            modelBuilder.Entity("API.Schema.MangaAltTitle", b =>
-                {
-                    b.Property<string>("AltTitleId")
-                        .HasMaxLength(64)
-                        .HasColumnType("character varying(64)");
-
-                    b.Property<string>("Language")
-                        .IsRequired()
-                        .HasMaxLength(8)
-                        .HasColumnType("character varying(8)");
-
-                    b.Property<string>("MangaId")
-                        .HasColumnType("character varying(64)");
-
-                    b.Property<string>("Title")
-                        .IsRequired()
-                        .HasMaxLength(256)
-                        .HasColumnType("character varying(256)");
-
-                    b.HasKey("AltTitleId");
-
-                    b.HasIndex("MangaId");
-
-                    b.ToTable("AltTitles");
-                });
-
             modelBuilder.Entity("API.Schema.MangaConnectors.MangaConnector", b =>
                 {
                     b.Property<string>("Name")
@@ -392,19 +338,19 @@ namespace API.Migrations
                     b.ToTable("NotificationConnectors");
                 });
 
-            modelBuilder.Entity("AuthorManga", b =>
+            modelBuilder.Entity("AuthorToManga", b =>
                 {
-                    b.Property<string>("AuthorsAuthorId")
+                    b.Property<string>("AuthorIds")
                         .HasColumnType("character varying(64)");
 
-                    b.Property<string>("MangaId")
+                    b.Property<string>("MangaIds")
                         .HasColumnType("character varying(64)");
 
-                    b.HasKey("AuthorsAuthorId", "MangaId");
+                    b.HasKey("AuthorIds", "MangaIds");
 
-                    b.HasIndex("MangaId");
+                    b.HasIndex("MangaIds");
 
-                    b.ToTable("AuthorManga");
+                    b.ToTable("AuthorToManga");
                 });
 
             modelBuilder.Entity("JobJob", b =>
@@ -422,19 +368,19 @@ namespace API.Migrations
                     b.ToTable("JobJob");
                 });
 
-            modelBuilder.Entity("MangaMangaTag", b =>
+            modelBuilder.Entity("MangaTagToManga", b =>
                 {
-                    b.Property<string>("MangaId")
+                    b.Property<string>("MangaTagIds")
                         .HasColumnType("character varying(64)");
 
-                    b.Property<string>("MangaTagsTag")
+                    b.Property<string>("MangaIds")
                         .HasColumnType("character varying(64)");
 
-                    b.HasKey("MangaId", "MangaTagsTag");
+                    b.HasKey("MangaTagIds", "MangaIds");
 
-                    b.HasIndex("MangaTagsTag");
+                    b.HasIndex("MangaIds");
 
-                    b.ToTable("MangaMangaTag");
+                    b.ToTable("MangaTagToManga");
                 });
 
             modelBuilder.Entity("API.Schema.Jobs.DownloadAvailableChaptersJob", b =>
@@ -446,6 +392,8 @@ namespace API.Migrations
                         .HasMaxLength(64)
                         .HasColumnType("character varying(64)");
 
+                    b.HasIndex("MangaId");
+
                     b.ToTable("Jobs", t =>
                         {
                             t.Property("MangaId")
@@ -464,6 +412,8 @@ namespace API.Migrations
                         .HasMaxLength(64)
                         .HasColumnType("character varying(64)");
 
+                    b.HasIndex("MangaId");
+
                     b.HasDiscriminator().HasValue((byte)4);
                 });
 
@@ -476,6 +426,8 @@ namespace API.Migrations
                         .HasMaxLength(64)
                         .HasColumnType("character varying(64)");
 
+                    b.HasIndex("ChapterId");
+
                     b.HasDiscriminator().HasValue((byte)0);
                 });
 
@@ -496,7 +448,7 @@ namespace API.Migrations
                     b.HasDiscriminator().HasValue((byte)3);
                 });
 
-            modelBuilder.Entity("API.Schema.Jobs.RetrieveChaptersJob", b =>
+            modelBuilder.Entity("API.Schema.Jobs.MoveMangaLibraryJob", b =>
                 {
                     b.HasBaseType("API.Schema.Jobs.Job");
 
@@ -505,6 +457,40 @@ namespace API.Migrations
                         .HasMaxLength(64)
                         .HasColumnType("character varying(64)");
 
+                    b.Property<string>("ToLibraryId")
+                        .IsRequired()
+                        .HasMaxLength(64)
+                        .HasColumnType("character varying(64)");
+
+                    b.HasIndex("MangaId");
+
+                    b.HasIndex("ToLibraryId");
+
+                    b.ToTable("Jobs", t =>
+                        {
+                            t.Property("MangaId")
+                                .HasColumnName("MoveMangaLibraryJob_MangaId");
+                        });
+
+                    b.HasDiscriminator().HasValue((byte)7);
+                });
+
+            modelBuilder.Entity("API.Schema.Jobs.RetrieveChaptersJob", b =>
+                {
+                    b.HasBaseType("API.Schema.Jobs.Job");
+
+                    b.Property<string>("Language")
+                        .IsRequired()
+                        .HasMaxLength(8)
+                        .HasColumnType("character varying(8)");
+
+                    b.Property<string>("MangaId")
+                        .IsRequired()
+                        .HasMaxLength(64)
+                        .HasColumnType("character varying(64)");
+
+                    b.HasIndex("MangaId");
+
                     b.ToTable("Jobs", t =>
                         {
                             t.Property("MangaId")
@@ -523,6 +509,8 @@ namespace API.Migrations
                         .HasMaxLength(64)
                         .HasColumnType("character varying(64)");
 
+                    b.HasIndex("MangaId");
+
                     b.ToTable("Jobs", t =>
                         {
                             t.Property("MangaId")
@@ -532,26 +520,6 @@ namespace API.Migrations
                     b.HasDiscriminator().HasValue((byte)6);
                 });
 
-            modelBuilder.Entity("API.Schema.Jobs.UpdateMetadataJob", b =>
-                {
-                    b.HasBaseType("API.Schema.Jobs.Job");
-
-                    b.Property<string>("MangaId")
-                        .IsRequired()
-                        .HasMaxLength(64)
-                        .HasColumnType("character varying(64)");
-
-                    b.HasIndex("MangaId");
-
-                    b.ToTable("Jobs", t =>
-                        {
-                            t.Property("MangaId")
-                                .HasColumnName("UpdateMetadataJob_MangaId");
-                        });
-
-                    b.HasDiscriminator().HasValue((byte)2);
-                });
-
             modelBuilder.Entity("API.Schema.LibraryConnectors.Kavita", b =>
                 {
                     b.HasBaseType("API.Schema.LibraryConnectors.LibraryConnector");
@@ -566,20 +534,6 @@ namespace API.Migrations
                     b.HasDiscriminator().HasValue((byte)0);
                 });
 
-            modelBuilder.Entity("API.Schema.MangaConnectors.AsuraToon", b =>
-                {
-                    b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
-
-                    b.HasDiscriminator().HasValue("AsuraToon");
-                });
-
-            modelBuilder.Entity("API.Schema.MangaConnectors.Bato", b =>
-                {
-                    b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
-
-                    b.HasDiscriminator().HasValue("Bato");
-                });
-
             modelBuilder.Entity("API.Schema.MangaConnectors.Global", b =>
                 {
                     b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
@@ -594,52 +548,10 @@ namespace API.Migrations
                     b.HasDiscriminator().HasValue("MangaDex");
                 });
 
-            modelBuilder.Entity("API.Schema.MangaConnectors.MangaHere", b =>
-                {
-                    b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
-
-                    b.HasDiscriminator().HasValue("MangaHere");
-                });
-
-            modelBuilder.Entity("API.Schema.MangaConnectors.MangaKatana", b =>
-                {
-                    b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
-
-                    b.HasDiscriminator().HasValue("MangaKatana");
-                });
-
-            modelBuilder.Entity("API.Schema.MangaConnectors.Manganato", b =>
-                {
-                    b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
-
-                    b.HasDiscriminator().HasValue("Manganato");
-                });
-
-            modelBuilder.Entity("API.Schema.MangaConnectors.Mangaworld", b =>
-                {
-                    b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
-
-                    b.HasDiscriminator().HasValue("Mangaworld");
-                });
-
-            modelBuilder.Entity("API.Schema.MangaConnectors.ManhuaPlus", b =>
-                {
-                    b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
-
-                    b.HasDiscriminator().HasValue("ManhuaPlus");
-                });
-
-            modelBuilder.Entity("API.Schema.MangaConnectors.Weebcentral", b =>
-                {
-                    b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
-
-                    b.HasDiscriminator().HasValue("Weebcentral");
-                });
-
             modelBuilder.Entity("API.Schema.Chapter", b =>
                 {
                     b.HasOne("API.Schema.Manga", "ParentManga")
-                        .WithMany()
+                        .WithMany("Chapters")
                         .HasForeignKey("ParentMangaId")
                         .OnDelete(DeleteBehavior.Cascade)
                         .IsRequired();
@@ -657,51 +569,99 @@ namespace API.Migrations
                     b.Navigation("ParentJob");
                 });
 
-            modelBuilder.Entity("API.Schema.Link", b =>
-                {
-                    b.HasOne("API.Schema.Manga", null)
-                        .WithMany("Links")
-                        .HasForeignKey("MangaId")
-                        .OnDelete(DeleteBehavior.Cascade);
-                });
-
             modelBuilder.Entity("API.Schema.Manga", b =>
                 {
                     b.HasOne("API.Schema.LocalLibrary", "Library")
                         .WithMany()
-                        .HasForeignKey("LibraryLocalLibraryId")
-                        .OnDelete(DeleteBehavior.Restrict);
+                        .HasForeignKey("LibraryId")
+                        .OnDelete(DeleteBehavior.SetNull);
 
                     b.HasOne("API.Schema.MangaConnectors.MangaConnector", "MangaConnector")
                         .WithMany()
-                        .HasForeignKey("MangaConnectorId")
+                        .HasForeignKey("MangaConnectorName")
                         .OnDelete(DeleteBehavior.Cascade)
                         .IsRequired();
 
+                    b.OwnsMany("API.Schema.Link", "Links", b1 =>
+                        {
+                            b1.Property<string>("LinkId")
+                                .HasMaxLength(64)
+                                .HasColumnType("character varying(64)");
+
+                            b1.Property<string>("LinkProvider")
+                                .IsRequired()
+                                .HasMaxLength(64)
+                                .HasColumnType("character varying(64)");
+
+                            b1.Property<string>("LinkUrl")
+                                .IsRequired()
+                                .HasMaxLength(2048)
+                                .HasColumnType("character varying(2048)");
+
+                            b1.Property<string>("MangaId")
+                                .IsRequired()
+                                .HasColumnType("character varying(64)");
+
+                            b1.HasKey("LinkId");
+
+                            b1.HasIndex("MangaId");
+
+                            b1.ToTable("Link");
+
+                            b1.WithOwner()
+                                .HasForeignKey("MangaId");
+                        });
+
+                    b.OwnsMany("API.Schema.MangaAltTitle", "AltTitles", b1 =>
+                        {
+                            b1.Property<string>("AltTitleId")
+                                .HasMaxLength(64)
+                                .HasColumnType("character varying(64)");
+
+                            b1.Property<string>("Language")
+                                .IsRequired()
+                                .HasMaxLength(8)
+                                .HasColumnType("character varying(8)");
+
+                            b1.Property<string>("MangaId")
+                                .IsRequired()
+                                .HasColumnType("character varying(64)");
+
+                            b1.Property<string>("Title")
+                                .IsRequired()
+                                .HasMaxLength(256)
+                                .HasColumnType("character varying(256)");
+
+                            b1.HasKey("AltTitleId");
+
+                            b1.HasIndex("MangaId");
+
+                            b1.ToTable("MangaAltTitle");
+
+                            b1.WithOwner()
+                                .HasForeignKey("MangaId");
+                        });
+
+                    b.Navigation("AltTitles");
+
                     b.Navigation("Library");
 
+                    b.Navigation("Links");
+
                     b.Navigation("MangaConnector");
                 });
 
-            modelBuilder.Entity("API.Schema.MangaAltTitle", b =>
-                {
-                    b.HasOne("API.Schema.Manga", null)
-                        .WithMany("AltTitles")
-                        .HasForeignKey("MangaId")
-                        .OnDelete(DeleteBehavior.Cascade);
-                });
-
-            modelBuilder.Entity("AuthorManga", b =>
+            modelBuilder.Entity("AuthorToManga", b =>
                 {
                     b.HasOne("API.Schema.Author", null)
                         .WithMany()
-                        .HasForeignKey("AuthorsAuthorId")
+                        .HasForeignKey("AuthorIds")
                         .OnDelete(DeleteBehavior.Cascade)
                         .IsRequired();
 
                     b.HasOne("API.Schema.Manga", null)
                         .WithMany()
-                        .HasForeignKey("MangaId")
+                        .HasForeignKey("MangaIds")
                         .OnDelete(DeleteBehavior.Cascade)
                         .IsRequired();
                 });
@@ -721,22 +681,85 @@ namespace API.Migrations
                         .IsRequired();
                 });
 
-            modelBuilder.Entity("MangaMangaTag", b =>
+            modelBuilder.Entity("MangaTagToManga", b =>
                 {
                     b.HasOne("API.Schema.Manga", null)
                         .WithMany()
-                        .HasForeignKey("MangaId")
+                        .HasForeignKey("MangaIds")
                         .OnDelete(DeleteBehavior.Cascade)
                         .IsRequired();
 
                     b.HasOne("API.Schema.MangaTag", null)
                         .WithMany()
-                        .HasForeignKey("MangaTagsTag")
+                        .HasForeignKey("MangaTagIds")
                         .OnDelete(DeleteBehavior.Cascade)
                         .IsRequired();
                 });
 
-            modelBuilder.Entity("API.Schema.Jobs.UpdateMetadataJob", b =>
+            modelBuilder.Entity("API.Schema.Jobs.DownloadAvailableChaptersJob", b =>
+                {
+                    b.HasOne("API.Schema.Manga", "Manga")
+                        .WithMany()
+                        .HasForeignKey("MangaId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("Manga");
+                });
+
+            modelBuilder.Entity("API.Schema.Jobs.DownloadMangaCoverJob", b =>
+                {
+                    b.HasOne("API.Schema.Manga", "Manga")
+                        .WithMany()
+                        .HasForeignKey("MangaId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("Manga");
+                });
+
+            modelBuilder.Entity("API.Schema.Jobs.DownloadSingleChapterJob", b =>
+                {
+                    b.HasOne("API.Schema.Chapter", "Chapter")
+                        .WithMany()
+                        .HasForeignKey("ChapterId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("Chapter");
+                });
+
+            modelBuilder.Entity("API.Schema.Jobs.MoveMangaLibraryJob", b =>
+                {
+                    b.HasOne("API.Schema.Manga", "Manga")
+                        .WithMany()
+                        .HasForeignKey("MangaId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.HasOne("API.Schema.LocalLibrary", "ToLibrary")
+                        .WithMany()
+                        .HasForeignKey("ToLibraryId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("Manga");
+
+                    b.Navigation("ToLibrary");
+                });
+
+            modelBuilder.Entity("API.Schema.Jobs.RetrieveChaptersJob", b =>
+                {
+                    b.HasOne("API.Schema.Manga", "Manga")
+                        .WithMany()
+                        .HasForeignKey("MangaId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("Manga");
+                });
+
+            modelBuilder.Entity("API.Schema.Jobs.UpdateFilesDownloadedJob", b =>
                 {
                     b.HasOne("API.Schema.Manga", "Manga")
                         .WithMany()
@@ -749,9 +772,7 @@ namespace API.Migrations
 
             modelBuilder.Entity("API.Schema.Manga", b =>
                 {
-                    b.Navigation("AltTitles");
-
-                    b.Navigation("Links");
+                    b.Navigation("Chapters");
                 });
 #pragma warning restore 612, 618
         }
diff --git a/API/Program.cs b/API/Program.cs
index 3e3af0b..0670a65 100644
--- a/API/Program.cs
+++ b/API/Program.cs
@@ -7,6 +7,7 @@ using API.Schema.MangaConnectors;
 using Asp.Versioning;
 using Asp.Versioning.Builder;
 using Asp.Versioning.Conventions;
+using log4net;
 using Microsoft.EntityFrameworkCore;
 using Newtonsoft.Json;
 using Newtonsoft.Json.Converters;
@@ -69,6 +70,7 @@ builder.Services.AddControllers().AddNewtonsoftJson(opts =>
     opts.SerializerSettings.Converters.Add(new StringEnumConverter());
     opts.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore;
 });
+builder.Services.AddScoped<ILog>(opts => LogManager.GetLogger("API"));
 
 builder.WebHost.UseUrls("http://*:6531");
 
@@ -109,26 +111,18 @@ using (var scope = app.Services.CreateScope())
     
     MangaConnector[] connectors =
         [
-            new AsuraToon(),
-            new Bato(),
             new MangaDex(),
-            new MangaHere(),
-            new MangaKatana(),
-            new Mangaworld(),
-            new ManhuaPlus(),
-            new Weebcentral(),
-            //new Manganato(),
             new Global(scope.ServiceProvider.GetService<PgsqlContext>()!)
         ];
     MangaConnector[] newConnectors = connectors.Where(c => !context.MangaConnectors.Contains(c)).ToArray();
     context.MangaConnectors.AddRange(newConnectors);
 
-    context.Jobs.AddRange(context.Mangas.AsEnumerable().Select(m => new UpdateFilesDownloadedJob(0, m.MangaId)));
+    context.Jobs.AddRange(context.Mangas.AsEnumerable().Select(m => new UpdateFilesDownloadedJob(m, 0)));
     
     context.Jobs.RemoveRange(context.Jobs.Where(j => j.state == JobState.Completed && j.RecurrenceMs < 1));
 
     if (!context.LocalLibraries.Any())
-        context.LocalLibraries.Add(new LocalLibrary(TrangaSettings.downloadLocation, "Default Library"));
+        context.LocalLibraries.Add(new LocalLibrary(TrangaSettings.downloadLocation, "Default ToLibrary"));
     
     string[] emojis = { "(•‿•)", "(づ \u25d5‿\u25d5 )づ", "( \u02d8\u25bd\u02d8)っ\u2668", "=\uff3e\u25cf \u22cf \u25cf\uff3e=", "(ΦωΦ)", "(\u272a\u3268\u272a)", "( ノ・o・ )ノ", "(〜^\u2207^ )〜", "~(\u2267ω\u2266)~","૮ \u00b4• ﻌ \u00b4• ა", "(\u02c3ᆺ\u02c2)", "(=\ud83d\udf66 \u0f1d \ud83d\udf66=)"};
     context.Notifications.Add(new Notification("Tranga Started", emojis[Random.Shared.Next(0, emojis.Length - 1)], NotificationUrgency.High));
diff --git a/API/Schema/Chapter.cs b/API/Schema/Chapter.cs
index b844c87..b56990a 100644
--- a/API/Schema/Chapter.cs
+++ b/API/Schema/Chapter.cs
@@ -3,7 +3,6 @@ using System.ComponentModel.DataAnnotations.Schema;
 using System.Text;
 using System.Text.RegularExpressions;
 using System.Xml.Linq;
-using API.Schema.Jobs;
 using Microsoft.EntityFrameworkCore;
 using Newtonsoft.Json;
 
@@ -12,51 +11,50 @@ namespace API.Schema;
 [PrimaryKey("ChapterId")]
 public class Chapter : IComparable<Chapter>
 {
-    public Chapter(Manga parentManga, string url, string chapterNumber, int? volumeNumber = null, string? title = null)
-        : this(parentManga.MangaId, url, chapterNumber, volumeNumber, title)
-    {
-        ParentManga = parentManga;
-        FileName = GetArchiveFilePath(parentManga.Name);
-    }
+    [StringLength(64)] [Required] public string ChapterId { get; init; }
 
-    public Chapter(string parentMangaId, string url, string chapterNumber,
-        int? volumeNumber = null, string? title = null)
-    {
-        ChapterId = TokenGen.CreateToken(typeof(Chapter), parentMangaId, (volumeNumber ?? 0).ToString(), chapterNumber);
-        ParentMangaId = parentMangaId;
-        Url = url;
-        ChapterNumber = chapterNumber;
-        VolumeNumber = volumeNumber;
-        Title = title;
-    }
+    public string ParentMangaId { get; init; }
+    [JsonIgnore] public Manga ParentManga { get; init; } = null!;
 
-    [StringLength(64)]
-    [Required]
-    public string ChapterId { get; init; }
     public int? VolumeNumber { get; private set; }
-    [StringLength(10)]
-    [Required]
-    public string ChapterNumber { get; private set; }
+    [StringLength(10)] [Required] public string ChapterNumber { get; private set; }
 
-    [StringLength(2048)]
-    [Required]
-    [Url]
-    public string Url { get; internal set; }
-    [StringLength(256)]
-    public string? Title { get; private set; }
-    [StringLength(256)]
-    [Required]
-    public string FileName { get; private set; }
-    [JsonIgnore]
-    [NotMapped]
-    public string? FullArchiveFilePath => ParentManga is { } m ? Path.Join(m.FullDirectoryPath, FileName) : null;
-    [Required]
-    public bool Downloaded { get; internal set; } = false;
-    [Required]
-    [StringLength(64)]
-    public string ParentMangaId { get; internal set; }
-    [JsonIgnore]
-    public Manga? ParentManga { get; init; }
+    [StringLength(2048)] [Required] [Url] public string Url { get; internal set; }
+
+    [StringLength(256)] public string? Title { get; private set; }
+
+    [StringLength(256)] [Required] public string FileName { get; private set; }
+
+    [Required] public bool Downloaded { get; internal set; }
+    [JsonIgnore] [NotMapped] public string FullArchiveFilePath => Path.Join(ParentManga.FullDirectoryPath, FileName);
+
+    public Chapter(Manga parentManga, string url, string chapterNumber, int? volumeNumber = null, string? title = null)
+    {
+        this.ChapterId = TokenGen.CreateToken(typeof(Chapter), parentManga.MangaId, chapterNumber);
+        this.ParentMangaId = parentManga.MangaId;
+        this.ParentManga = parentManga;
+        this.VolumeNumber = volumeNumber;
+        this.ChapterNumber = chapterNumber;
+        this.Url = url;
+        this.Title = title;
+        this.FileName = GetArchiveFilePath();
+        this.Downloaded = false;
+    }
+
+    /// <summary>
+    /// EF ONLY!!!
+    /// </summary>
+    internal Chapter(string chapterId, string parentMangaId, int? volumeNumber, string chapterNumber, string url, string? title, string fileName, bool downloaded)
+    {
+        this.ChapterId = chapterId;
+        this.ParentMangaId = parentMangaId;
+        this.VolumeNumber = volumeNumber;
+        this.ChapterNumber = chapterNumber;
+        this.Url = url;
+        this.Title = title;
+        this.FileName = fileName;
+        this.Downloaded = downloaded;
+    }
 
     public int CompareTo(Chapter? other)
     {
@@ -70,43 +68,11 @@ public class Chapter : IComparable<Chapter>
         };
     }
 
-    public MoveFileOrFolderJob? UpdateChapterNumber(string chapterNumber)
-    {
-        ChapterNumber = chapterNumber;
-        return UpdateArchiveFileName();
-    }
-
-    public MoveFileOrFolderJob? UpdateVolumeNumber(int? volumeNumber)
-    {
-        VolumeNumber = volumeNumber;
-        return UpdateArchiveFileName();
-    }
-
-    public MoveFileOrFolderJob? UpdateTitle(string? title)
-    {
-        Title = title;
-        return UpdateArchiveFileName();
-    }
-
-    internal MoveFileOrFolderJob? UpdateArchiveFileName()
-    {
-        string? oldPath = FullArchiveFilePath;
-        if (oldPath is null)
-            return null;
-        string newPath = GetArchiveFilePath();
-        FileName = newPath;
-        return Downloaded ? new MoveFileOrFolderJob(oldPath, newPath) : null;
-    }
-
     /// <summary>
     /// Checks the filesystem if an archive at the ArchiveFilePath exists
     /// </summary>
     /// <returns>True if archive exists on disk</returns>
-    public bool IsDownloaded()
-    {
-        string path = GetArchiveFilePath();
-        return File.Exists(path);
-    }
+    public bool CheckDownloaded() => File.Exists(FullArchiveFilePath);
 
     /// Placeholders:
     /// %M Manga Name
@@ -119,7 +85,7 @@ public class Chapter : IComparable<Chapter>
     /// %Y Year (Manga)
     private static readonly Regex NullableRex = new(@"\?([a-zA-Z])\(([^\)]*)\)|(.+?)");
     private static readonly Regex ReplaceRexx = new(@"%([a-zA-Z])|(.+?)");
-    private string GetArchiveFilePath(string? parentMangaName = null)
+    private string GetArchiveFilePath()
     {
         string archiveNamingScheme = TrangaSettings.chapterNamingScheme;
         StringBuilder stringBuilder = new();
@@ -134,13 +100,13 @@ public class Chapter : IComparable<Chapter>
             char placeholder = nullable.Groups[1].Value[0];
             bool isNull = placeholder switch
             {
-                'M' => ParentManga?.Name is null && parentMangaName is null,
+                'M' => ParentManga?.Name is null,
                 'V' => VolumeNumber is null,
                 'C' => ChapterNumber is null,
                 'T' => Title is null,
                 'A' => ParentManga?.Authors?.FirstOrDefault()?.AuthorName is null,
                 'I' => ChapterId is null,
-                'i' => ParentMangaId is null,
+                'i' => ParentManga?.MangaId is null,
                 'Y' => ParentManga?.Year is null,
                 _ => true
             };
@@ -162,13 +128,13 @@ public class Chapter : IComparable<Chapter>
             char placeholder = replace.Groups[1].Value[0];
             string? value = placeholder switch
             {
-                'M' => ParentManga?.Name ?? parentMangaName,
+                'M' => ParentManga?.Name,
                 'V' => VolumeNumber?.ToString(),
                 'C' => ChapterNumber,
                 'T' => Title,
                 'A' => ParentManga?.Authors?.FirstOrDefault()?.AuthorName,
                 'I' => ChapterId,
-                'i' => ParentMangaId,
+                'i' => ParentManga?.MangaId,
                 'Y' => ParentManga?.Year.ToString(),
                 _ => null
             };
diff --git a/API/Schema/Jobs/DownloadAvailableChaptersJob.cs b/API/Schema/Jobs/DownloadAvailableChaptersJob.cs
index e3ae719..60358ba 100644
--- a/API/Schema/Jobs/DownloadAvailableChaptersJob.cs
+++ b/API/Schema/Jobs/DownloadAvailableChaptersJob.cs
@@ -1,17 +1,31 @@
 using System.ComponentModel.DataAnnotations;
+using Newtonsoft.Json;
 
 namespace API.Schema.Jobs;
 
-public class DownloadAvailableChaptersJob(ulong recurrenceMs, string mangaId, string? parentJobId = null, ICollection<string>? dependsOnJobsIds = null)
-    : Job(TokenGen.CreateToken(typeof(DownloadAvailableChaptersJob)), JobType.DownloadAvailableChaptersJob, recurrenceMs, parentJobId, dependsOnJobsIds)
+public class DownloadAvailableChaptersJob : Job
 {
-    [StringLength(64)]
-    [Required]
-    public string MangaId { get; init; } = mangaId;
+    [StringLength(64)] [Required] public string MangaId { get; init; }
+    [JsonIgnore] public Manga Manga { get; init; } = null!;
+    
+    public DownloadAvailableChaptersJob(Manga manga, ulong recurrenceMs, Job? parentJob = null, ICollection<Job>? dependsOnJobs = null)
+        : base(TokenGen.CreateToken(typeof(DownloadAvailableChaptersJob)), JobType.DownloadAvailableChaptersJob, recurrenceMs, parentJob, dependsOnJobs)
+    {
+        this.MangaId = manga.MangaId;
+        this.Manga = manga;
+    }
+    
+    /// <summary>
+    /// EF ONLY!!!
+    /// </summary>
+    public DownloadAvailableChaptersJob(string mangaId, ulong recurrenceMs, string? parentJobId = null)
+        : base(TokenGen.CreateToken(typeof(DownloadAvailableChaptersJob)), JobType.DownloadAvailableChaptersJob, recurrenceMs, parentJobId)
+    {
+        this.MangaId = mangaId;
+    }
     
     protected override IEnumerable<Job> RunInternal(PgsqlContext context)
     {
-        return context.Chapters.Where(c => c.ParentMangaId == MangaId).AsEnumerable()
-            .Select(chapter => new DownloadSingleChapterJob(chapter.ChapterId, this.JobId));
+        return Manga.Chapters.Select(chapter => new DownloadSingleChapterJob(chapter, this));
     }
 }
\ No newline at end of file
diff --git a/API/Schema/Jobs/DownloadMangaCoverJob.cs b/API/Schema/Jobs/DownloadMangaCoverJob.cs
index bcf2c1c..533fe70 100644
--- a/API/Schema/Jobs/DownloadMangaCoverJob.cs
+++ b/API/Schema/Jobs/DownloadMangaCoverJob.cs
@@ -1,26 +1,41 @@
 using System.ComponentModel.DataAnnotations;
+using Microsoft.EntityFrameworkCore;
+using Newtonsoft.Json;
 
 namespace API.Schema.Jobs;
 
-public class DownloadMangaCoverJob(string mangaId, string? parentJobId = null, ICollection<string>? dependsOnJobsIds = null)
-    : Job(TokenGen.CreateToken(typeof(DownloadMangaCoverJob)), JobType.DownloadMangaCoverJob, 0, parentJobId, dependsOnJobsIds)
+public class DownloadMangaCoverJob : Job
 {
-    [StringLength(64)]
-    [Required]
-    public string MangaId { get; init; } = mangaId;
+    [StringLength(64)] [Required] public string MangaId { get; init; }
+    [JsonIgnore] public Manga Manga { get; init; } = null!;
+
+    public DownloadMangaCoverJob(Manga manga, Job? parentJob = null, ICollection<Job>? dependsOnJobs = null)
+        : base(TokenGen.CreateToken(typeof(DownloadMangaCoverJob)), JobType.DownloadMangaCoverJob, 0, parentJob, dependsOnJobs)
+    {
+        this.MangaId = manga.MangaId;
+        this.Manga = manga;
+    }
+    
+    /// <summary>
+    /// EF ONLY!!!
+    /// </summary>
+    public DownloadMangaCoverJob(string mangaId, string? parentJobId = null)
+        : base(TokenGen.CreateToken(typeof(DownloadMangaCoverJob)), JobType.DownloadMangaCoverJob, 0, parentJobId)
+    {
+        this.MangaId = mangaId;
+    }
     
     protected override IEnumerable<Job> RunInternal(PgsqlContext context)
     {
-        Manga? manga = context.Mangas.Find(this.MangaId);
-        if (manga is null)
+        try
         {
-            Log.Error($"Manga {this.MangaId} not found.");
-            return [];
+            Manga.CoverFileNameInCache = Manga.MangaConnector.SaveCoverImageToCache(Manga);
+            context.SaveChanges();
+        }
+        catch (DbUpdateException e)
+        {
+            Log.Error(e);
         }
-        
-        manga.CoverFileNameInCache = manga.SaveCoverImageToCache();
-        context.SaveChanges();
-        Log.Info($"Saved cover for Manga {this.MangaId} to cache at {manga.CoverFileNameInCache}.");
         return [];
     }
 }
\ No newline at end of file
diff --git a/API/Schema/Jobs/DownloadSingleChapterJob.cs b/API/Schema/Jobs/DownloadSingleChapterJob.cs
index 417f5e8..94fabaa 100644
--- a/API/Schema/Jobs/DownloadSingleChapterJob.cs
+++ b/API/Schema/Jobs/DownloadSingleChapterJob.cs
@@ -2,7 +2,7 @@
 using System.IO.Compression;
 using System.Runtime.InteropServices;
 using API.MangaDownloadClients;
-using API.Schema.MangaConnectors;
+using Newtonsoft.Json;
 using SixLabors.ImageSharp;
 using SixLabors.ImageSharp.Formats.Jpeg;
 using SixLabors.ImageSharp.Processing;
@@ -11,45 +11,37 @@ using static System.IO.UnixFileMode;
 
 namespace API.Schema.Jobs;
 
-public class DownloadSingleChapterJob(string chapterId, string? parentJobId = null, ICollection<string>? dependsOnJobsIds = null)
-    : Job(TokenGen.CreateToken(typeof(DownloadSingleChapterJob)), JobType.DownloadSingleChapterJob, 0, parentJobId, dependsOnJobsIds)
+public class DownloadSingleChapterJob : Job
 {
-    [StringLength(64)]
-    [Required]
-    public string ChapterId { get; init; } = chapterId;
+    [StringLength(64)] [Required] public string ChapterId { get; init; }
+
+    [JsonIgnore] public Chapter Chapter { get; init; } = null!;
+
+    public DownloadSingleChapterJob(Chapter chapter, Job? parentJob = null, ICollection<Job>? dependsOnJobs = null)
+        : base(TokenGen.CreateToken(typeof(DownloadSingleChapterJob)), JobType.DownloadSingleChapterJob, 0, parentJob, dependsOnJobs)
+    {
+        this.ChapterId = chapter.ChapterId;
+        this.Chapter = chapter;
+    }
+    
+    /// <summary>
+    /// EF ONLY!!!
+    /// </summary>
+    public DownloadSingleChapterJob(string chapterId, string? parentJobId = null)
+        : base(TokenGen.CreateToken(typeof(DownloadSingleChapterJob)), JobType.DownloadSingleChapterJob, 0)
+    {
+        this.ChapterId = chapterId;
+    }
     
     protected override IEnumerable<Job> RunInternal(PgsqlContext context)
     {
-        Chapter? chapter = context.Chapters.Find(ChapterId);
-        if (chapter is null)
-        {
-            Log.Error("Chapter is null.");
-            return [];
-        }
-        Manga? manga = context.Mangas.Find(chapter.ParentMangaId) ?? chapter.ParentManga;
-        if (manga is null)
-        {
-            Log.Error("Manga is null.");
-            return [];
-        }
-        MangaConnector? connector = context.MangaConnectors.Find(manga.MangaConnectorId) ?? manga.MangaConnector;
-        if (connector is null)
-        {
-            Log.Error("Connector is null.");
-            return [];
-        }
-        string[] imageUrls = connector.GetChapterImageUrls(chapter);
+        string[] imageUrls = Chapter.ParentManga.MangaConnector.GetChapterImageUrls(Chapter);
         if (imageUrls.Length < 1)
         {
             Log.Info($"No imageUrls for chapter {ChapterId}");
             return [];
         }
-        string? saveArchiveFilePath = chapter.FullArchiveFilePath;
-        if (saveArchiveFilePath is null)
-        {
-            Log.Error("saveArchiveFilePath is null.");
-            return [];
-        }
+        string saveArchiveFilePath = Chapter.FullArchiveFilePath;
         
         //Check if Publication Directory already exists
         string directoryPath = Path.GetDirectoryName(saveArchiveFilePath)!;
@@ -88,10 +80,10 @@ public class DownloadSingleChapterJob(string chapterId, string? parentJobId = nu
             }
         }
         
-        CopyCoverFromCacheToDownloadLocation(manga);
+        CopyCoverFromCacheToDownloadLocation(Chapter.ParentManga);
         
         Log.Debug($"Creating ComicInfo.xml {ChapterId}");
-        File.WriteAllText(Path.Join(tempFolder, "ComicInfo.xml"), chapter.GetComicInfoXmlString());
+        File.WriteAllText(Path.Join(tempFolder, "ComicInfo.xml"), Chapter.GetComicInfoXmlString());
         
         Log.Debug($"Packaging images to archive {ChapterId}");
         //ZIP-it and ship-it
@@ -100,10 +92,10 @@ public class DownloadSingleChapterJob(string chapterId, string? parentJobId = nu
             File.SetUnixFileMode(saveArchiveFilePath, UserRead | UserWrite | UserExecute | GroupRead | GroupWrite | GroupExecute | OtherRead | OtherExecute);
         Directory.Delete(tempFolder, true); //Cleanup
         
-        chapter.Downloaded = true;
+        Chapter.Downloaded = true;
         context.SaveChanges();
 
-        return [new UpdateFilesDownloadedJob(0, manga.MangaId, this.JobId)];
+        return [new UpdateFilesDownloadedJob(Chapter.ParentManga, 0, this)];
     }
     
     private void ProcessImage(string imagePath)
@@ -138,7 +130,7 @@ public class DownloadSingleChapterJob(string chapterId, string? parentJobId = nu
         }
 
         Log.Info($"Copying cover to {publicationFolder}");
-        string? fileInCache = manga.CoverFileNameInCache ?? manga.SaveCoverImageToCache();
+        string? fileInCache = manga.CoverFileNameInCache ?? manga.MangaConnector.SaveCoverImageToCache(manga);
         if (fileInCache is null)
         {
             Log.Error($"File {fileInCache} does not exist");
diff --git a/API/Schema/Jobs/Job.cs b/API/Schema/Jobs/Job.cs
index 0c35ace..07fb42a 100644
--- a/API/Schema/Jobs/Job.cs
+++ b/API/Schema/Jobs/Job.cs
@@ -12,49 +12,50 @@ public abstract class Job
     [StringLength(64)]
     [Required]
     public string JobId { get; init; }
-    [StringLength(64)]
-    public string? ParentJobId { get; init; }
-    [JsonIgnore]
-    public Job? ParentJob { get; init; }
-    [StringLength(64)]
-    public ICollection<string>? DependsOnJobsIds { get; init; }
-    [JsonIgnore]
-    public ICollection<Job>? DependsOnJobs { get; init; }
-    
-    [Required]
-    public JobType JobType { get; init; }
-    [Required]
-    public ulong RecurrenceMs { get; set; }
-    [Required]
-    public DateTime LastExecution { get; internal set; } = DateTime.UnixEpoch;
-    
-    [NotMapped]
-    [Required]
-    public DateTime NextExecution => LastExecution.AddMilliseconds(RecurrenceMs);
-    [Required]
-    public JobState state { get; internal set; } = JobState.Waiting;
-    [Required]
-    public bool Enabled { get; internal set; } = true;
-    
-    [NotMapped]
-    [JsonIgnore]
-    protected ILog Log { get; init; }
 
-    public Job(string jobId, JobType jobType, ulong recurrenceMs, Job? parentJob = null, ICollection<Job>? dependsOnJobs = null)
-        : this(jobId, jobType, recurrenceMs, parentJob?.JobId, dependsOnJobs?.Select(j => j.JobId).ToList())
+    [StringLength(64)] public string? ParentJobId { get; init; }
+    [JsonIgnore] public Job? ParentJob { get; init; }
+    [JsonIgnore] public ICollection<Job> DependsOnJobs { get; init; }
+
+    [Required] public JobType JobType { get; init; }
+
+    [Required] public ulong RecurrenceMs { get; set; }
+
+    [Required] public DateTime LastExecution { get; internal set; } = DateTime.UnixEpoch;
+
+    [NotMapped] [Required] public DateTime NextExecution => LastExecution.AddMilliseconds(RecurrenceMs);
+    [Required] public JobState state { get; internal set; } = JobState.FirstExecution;
+    [Required] public bool Enabled { get; internal set; } = true;
+
+    [JsonIgnore] [NotMapped] internal bool IsCompleted => state is >= (JobState)128 and < (JobState)192;
+    [JsonIgnore] [NotMapped] internal bool DependenciesFulfilled => DependsOnJobs.All(j => j.IsCompleted);
+
+    [NotMapped] [JsonIgnore] protected ILog Log { get; init; }
+
+    protected Job(string jobId, JobType jobType, ulong recurrenceMs, Job? parentJob = null, ICollection<Job>? dependsOnJobs = null)
     {
+        this.JobId = jobId;
+        this.JobType = jobType;
+        this.RecurrenceMs = recurrenceMs;
+        this.ParentJobId = parentJob?.JobId;
         this.ParentJob = parentJob;
-        this.DependsOnJobs = dependsOnJobs;
+        this.DependsOnJobs = dependsOnJobs ?? [];
+        
+        this.Log = LogManager.GetLogger(this.GetType());
     }
 
-    public Job(string jobId, JobType jobType, ulong recurrenceMs, string? parentJobId = null, ICollection<string>? dependsOnJobsIds = null)
+    /// <summary>
+    /// EF ONLY!!!
+    /// </summary>
+    protected Job(string jobId, JobType jobType, ulong recurrenceMs, string? parentJobId)
     {
-        Log = LogManager.GetLogger(GetType());
-        JobId = jobId;
-        ParentJobId = parentJobId;
-        DependsOnJobsIds = dependsOnJobsIds;
-        JobType = jobType;
-        RecurrenceMs = recurrenceMs;
+        this.JobId = jobId;
+        this.JobType = jobType;
+        this.RecurrenceMs = recurrenceMs;
+        this.ParentJobId = parentJobId;
+        this.DependsOnJobs = [];
+        
+        this.Log = LogManager.GetLogger(this.GetType());
     }
 
     public IEnumerable<Job> Run(IServiceProvider serviceProvider)
diff --git a/API/Schema/Jobs/JobState.cs b/API/Schema/Jobs/JobState.cs
index 062376e..12e8919 100644
--- a/API/Schema/Jobs/JobState.cs
+++ b/API/Schema/Jobs/JobState.cs
@@ -3,11 +3,12 @@
 public enum JobState : byte
 {
     //Values 0-63 Preparation Stages
-    Waiting = 0, 
+    FirstExecution = 0, 
     //64-127 Running Stages
     Running = 64,
     //128-191 Completion Stages
     Completed = 128,
+    CompletedWaiting = 159,
     //192-255 Error stages
     Failed = 192
 }
\ No newline at end of file
diff --git a/API/Schema/Jobs/MoveFileOrFolderJob.cs b/API/Schema/Jobs/MoveFileOrFolderJob.cs
index 0857ea8..9b62db2 100644
--- a/API/Schema/Jobs/MoveFileOrFolderJob.cs
+++ b/API/Schema/Jobs/MoveFileOrFolderJob.cs
@@ -2,15 +2,31 @@
 
 namespace API.Schema.Jobs;
 
-public class MoveFileOrFolderJob(string fromLocation, string toLocation, string? parentJobId = null, ICollection<string>? dependsOnJobsIds = null)
-    : Job(TokenGen.CreateToken(typeof(MoveFileOrFolderJob)), JobType.MoveFileOrFolderJob, 0, parentJobId, dependsOnJobsIds)
+public class MoveFileOrFolderJob : Job
 {
     [StringLength(256)]
     [Required]
-    public string FromLocation { get; init; } = fromLocation;
+    public string FromLocation { get; init; }
     [StringLength(256)]
     [Required]
-    public string ToLocation { get; init; } = toLocation;
+    public string ToLocation { get; init; }
+    
+    public MoveFileOrFolderJob(string fromLocation, string toLocation, Job? parentJob = null, ICollection<Job>? dependsOnJobs = null)
+        : base(TokenGen.CreateToken(typeof(MoveFileOrFolderJob)), JobType.MoveFileOrFolderJob, 0, parentJob, dependsOnJobs)
+    {
+        this.FromLocation = fromLocation;
+        this.ToLocation = toLocation;
+    }
+    
+    /// <summary>
+    /// EF ONLY!!!
+    /// </summary>
+    public MoveFileOrFolderJob(string jobId, string fromLocation, string toLocation, string? parentJobId = null)
+        : base(jobId, JobType.MoveFileOrFolderJob, 0, parentJobId)
+    {
+        this.FromLocation = fromLocation;
+        this.ToLocation = toLocation;
+    }
     
     protected override IEnumerable<Job> RunInternal(PgsqlContext context)
     {
diff --git a/API/Schema/Jobs/MoveMangaLibraryJob.cs b/API/Schema/Jobs/MoveMangaLibraryJob.cs
index 210bb28..13cd201 100644
--- a/API/Schema/Jobs/MoveMangaLibraryJob.cs
+++ b/API/Schema/Jobs/MoveMangaLibraryJob.cs
@@ -1,35 +1,39 @@
 using System.ComponentModel.DataAnnotations;
 using Microsoft.EntityFrameworkCore;
+using Newtonsoft.Json;
 
 namespace API.Schema.Jobs;
 
-public class MoveMangaLibraryJob(string mangaId, string toLibraryId, string? parentJobId = null, ICollection<string>? dependsOnJobsIds = null)
-    : Job(TokenGen.CreateToken(typeof(MoveMangaLibraryJob)), JobType.MoveMangaLibraryJob, 0, parentJobId, dependsOnJobsIds)
+public class MoveMangaLibraryJob : Job
 {
-    [StringLength(64)]
-    [Required]
-    public string MangaId { get; init; } = mangaId;
-    [StringLength(64)]
-    [Required]
-    public string ToLibraryId { get; init; } = toLibraryId;
+    [StringLength(64)] [Required] public string MangaId { get; init; }
+    [JsonIgnore] public Manga Manga { get; init; } = null!;
+    [StringLength(64)] [Required] public string ToLibraryId { get; init; }
+    public LocalLibrary ToLibrary { get; init; } = null!;
+    
+    public MoveMangaLibraryJob(Manga manga, LocalLibrary toLibrary, Job? parentJob = null, ICollection<Job>? dependsOnJobs = null)
+        : base(TokenGen.CreateToken(typeof(MoveMangaLibraryJob)), JobType.MoveMangaLibraryJob, 0, parentJob, dependsOnJobs)
+    {
+        this.MangaId = manga.MangaId;
+        this.Manga = manga;
+        this.ToLibraryId = toLibrary.LocalLibraryId;
+        this.ToLibrary = toLibrary;
+    }
+    
+    /// <summary>
+    /// EF ONLY!!!
+    /// </summary>
+    public MoveMangaLibraryJob(string mangaId, string toLibraryId, string? parentJobId = null)
+        : base(TokenGen.CreateToken(typeof(MoveMangaLibraryJob)), JobType.MoveMangaLibraryJob, 0, parentJobId)
+    {
+        this.MangaId = mangaId;
+        this.ToLibraryId = toLibraryId;
+    }
     
     protected override IEnumerable<Job> RunInternal(PgsqlContext context)
     {
-        Manga? manga = context.Mangas.Find(MangaId);
-        if (manga is null)
-        {
-            Log.Error("Manga not found");
-            return [];
-        }
-        LocalLibrary? library = context.LocalLibraries.Find(ToLibraryId);
-        if (library is null)
-        {
-            Log.Error("LocalLibrary not found");
-            return [];
-        }
-        Chapter[] chapters = context.Chapters.Where(c => c.ParentMangaId == MangaId).ToArray();
-        Dictionary<Chapter, string> oldPath = chapters.ToDictionary(c => c, c => c.FullArchiveFilePath!);
-        manga.Library = library;
+        Dictionary<Chapter, string> oldPath = Manga.Chapters.ToDictionary(c => c, c => c.FullArchiveFilePath);
+        Manga.Library = ToLibrary;
         try
         {
             context.SaveChanges();
@@ -40,6 +44,6 @@ public class MoveMangaLibraryJob(string mangaId, string toLibraryId, string? par
             return [];
         }
 
-        return chapters.Select(c => new MoveFileOrFolderJob(oldPath[c], c.FullArchiveFilePath!));
+        return Manga.Chapters.Select(c => new MoveFileOrFolderJob(oldPath[c], c.FullArchiveFilePath));
     }
 }
\ No newline at end of file
diff --git a/API/Schema/Jobs/RetrieveChaptersJob.cs b/API/Schema/Jobs/RetrieveChaptersJob.cs
index c0eb38d..e4f7257 100644
--- a/API/Schema/Jobs/RetrieveChaptersJob.cs
+++ b/API/Schema/Jobs/RetrieveChaptersJob.cs
@@ -1,40 +1,43 @@
 using System.ComponentModel.DataAnnotations;
 using API.Schema.MangaConnectors;
 using Microsoft.EntityFrameworkCore;
+using Newtonsoft.Json;
 
 namespace API.Schema.Jobs;
 
-public class RetrieveChaptersJob(ulong recurrenceMs, string mangaId, string? parentJobId = null, ICollection<string>? dependsOnJobsIds = null)
-    : Job(TokenGen.CreateToken(typeof(RetrieveChaptersJob)), JobType.RetrieveChaptersJob, recurrenceMs, parentJobId, dependsOnJobsIds)
+public class RetrieveChaptersJob : Job
 {
-    [StringLength(64)]
-    [Required]
-    public string MangaId { get; init; } = mangaId;
+    [StringLength(64)] [Required] public string MangaId { get; init; }
+    [JsonIgnore] public Manga Manga { get; init; } = null!;
+    [StringLength(8)] [Required] public string Language { get; private set; }
+    
+    public RetrieveChaptersJob(Manga manga, string language, ulong recurrenceMs, Job? parentJob = null, ICollection<Job>? dependsOnJobs = null)
+        : base(TokenGen.CreateToken(typeof(RetrieveChaptersJob)), JobType.RetrieveChaptersJob, recurrenceMs, parentJob, dependsOnJobs)
+    {
+        this.MangaId = manga.MangaId;
+        this.Manga = manga;
+        this.Language = language;
+    }
+    
+    /// <summary>
+    /// EF ONLY!!!
+    /// </summary>
+    public RetrieveChaptersJob(string mangaId, string language, ulong recurrenceMs, string? parentJobId = null)
+        : base(TokenGen.CreateToken(typeof(RetrieveChaptersJob)), JobType.RetrieveChaptersJob, recurrenceMs, parentJobId)
+    {
+        this.MangaId = mangaId;
+        this.Language = language;
+    }
     
     protected override IEnumerable<Job> RunInternal(PgsqlContext context)
     {
-        Manga? manga = context.Mangas.Find(MangaId);
-        if (manga is null)
-        {
-            Log.Error("Manga is null.");
-            return [];
-        }
-        MangaConnector? connector = manga.MangaConnector ?? context.MangaConnectors.Find(manga.MangaConnectorId);
-        if (connector is null)
-        {
-            Log.Error("Connector is null.");
-            return [];
-        }
         // This gets all chapters that are not downloaded
-        Chapter[] allNewChapters = connector.GetNewChapters(manga).DistinctBy(c => c.ChapterId).ToArray();
-        Log.Info($"{allNewChapters.Length} new chapters.");
+        Chapter[] allChapters = Manga.MangaConnector.GetChapters(Manga, Language);
+        Chapter[] newChapters = allChapters.Where(chapter => context.Chapters.Contains(chapter) == false).ToArray();
+        Log.Info($"{newChapters.Length} new chapters.");
 
         try
         {
-            // This filters out chapters that are not downloaded but already exist in the DB
-            string[] chapterIds = context.Chapters.Where(chapter => chapter.ParentMangaId == manga.MangaId)
-                .Select(chapter => chapter.ChapterId).ToArray();
-            Chapter[] newChapters = allNewChapters.Where(chapter => !chapterIds.Contains(chapter.ChapterId)).ToArray();
             context.Chapters.AddRange(newChapters);
             context.SaveChanges();
         }
diff --git a/API/Schema/Jobs/UpdateFilesDownloadedJob.cs b/API/Schema/Jobs/UpdateFilesDownloadedJob.cs
index a68c3b6..3e151bf 100644
--- a/API/Schema/Jobs/UpdateFilesDownloadedJob.cs
+++ b/API/Schema/Jobs/UpdateFilesDownloadedJob.cs
@@ -1,22 +1,43 @@
 using System.ComponentModel.DataAnnotations;
+using Microsoft.EntityFrameworkCore;
 using Newtonsoft.Json;
 
 namespace API.Schema.Jobs;
 
-public class UpdateFilesDownloadedJob(ulong recurrenceMs, string mangaId, string? parentJobId = null, ICollection<string>? dependsOnJobsIds = null)
-    : Job(TokenGen.CreateToken(typeof(UpdateFilesDownloadedJob)), JobType.UpdateFilesDownloadedJob, recurrenceMs, parentJobId, dependsOnJobsIds)
+public class UpdateFilesDownloadedJob : Job
 {
-    [StringLength(64)]
-    [Required]
-    public string MangaId { get; init; } = mangaId;
+    [StringLength(64)] [Required] public string MangaId { get; init; }
+    [JsonIgnore] public Manga Manga { get; init; } = null!;
+    
+    public UpdateFilesDownloadedJob(Manga manga, ulong recurrenceMs, Job? parentJob = null, ICollection<Job>? dependsOnJobs = null)
+        : base(TokenGen.CreateToken(typeof(UpdateFilesDownloadedJob)), JobType.UpdateFilesDownloadedJob, recurrenceMs, parentJob, dependsOnJobs)
+    {
+        this.MangaId = manga.MangaId;
+        this.Manga = manga;
+    }
+    
+    /// <summary>
+    /// EF ONLY!!!
+    /// </summary>
+    public UpdateFilesDownloadedJob(string mangaId, ulong recurrenceMs, string? parentJobId = null)
+        : base(TokenGen.CreateToken(typeof(UpdateFilesDownloadedJob)), JobType.UpdateFilesDownloadedJob, recurrenceMs, parentJobId)
+    {
+        this.MangaId = mangaId;
+    }
     
     protected override IEnumerable<Job> RunInternal(PgsqlContext context)
     {
-        IQueryable<Chapter> chapters = context.Chapters.Where(c => c.ParentMangaId == MangaId);
-        foreach (Chapter chapter in chapters)
-            chapter.Downloaded = chapter.IsDownloaded();
+        foreach (Chapter chapter in Manga.Chapters)
+            chapter.Downloaded = chapter.CheckDownloaded();
 
-        context.SaveChanges();
+        try
+        {
+            context.SaveChanges();
+        }
+        catch (DbUpdateException e)
+        {
+            Log.Error(e);
+        }
         return [];
     }
 }
\ No newline at end of file
diff --git a/API/Schema/Jobs/UpdateMetadataJob.cs b/API/Schema/Jobs/UpdateMetadataJob.cs
deleted file mode 100644
index 6fe238e..0000000
--- a/API/Schema/Jobs/UpdateMetadataJob.cs
+++ /dev/null
@@ -1,27 +0,0 @@
-using System.ComponentModel.DataAnnotations;
-using Newtonsoft.Json;
-
-namespace API.Schema.Jobs;
-
-public class UpdateMetadataJob(ulong recurrenceMs, string mangaId, string? parentJobId = null, ICollection<string>? dependsOnJobsIds = null)
-    : Job(TokenGen.CreateToken(typeof(UpdateMetadataJob)), JobType.UpdateMetaDataJob, recurrenceMs, parentJobId, dependsOnJobsIds)
-{
-    [StringLength(64)]
-    [Required]
-    public string MangaId { get; init; } = mangaId;
-    
-    [JsonIgnore]
-    public virtual Manga? Manga { get; init; }
-    
-    /// <summary>
-    /// Updates all data related to Manga.
-    /// Retrieves data from Mangaconnector
-    /// Updates Chapter-info
-    /// </summary>
-    /// <param name="context"></param>
-    protected override IEnumerable<Job> RunInternal(PgsqlContext context)
-    {
-        Log.Warn("NOT IMPLEMENTED.");
-        return [];//TODO
-    }
-}
\ No newline at end of file
diff --git a/API/Schema/LibraryConnectors/Kavita.cs b/API/Schema/LibraryConnectors/Kavita.cs
index f6a68e6..9a674b9 100644
--- a/API/Schema/LibraryConnectors/Kavita.cs
+++ b/API/Schema/LibraryConnectors/Kavita.cs
@@ -53,13 +53,13 @@ public class Kavita : LibraryConnector
     protected override void UpdateLibraryInternal()
     {
         foreach (KavitaLibrary lib in GetLibraries())
-            NetClient.MakePost($"{BaseUrl}/api/Library/scan?libraryId={lib.id}", "Bearer", Auth);
+            NetClient.MakePost($"{BaseUrl}/api/ToLibrary/scan?libraryId={lib.id}", "Bearer", Auth);
     }
 
     internal override bool Test()
     {
         foreach (KavitaLibrary lib in GetLibraries())
-            if (NetClient.MakePost($"{BaseUrl}/api/Library/scan?libraryId={lib.id}", "Bearer", Auth))
+            if (NetClient.MakePost($"{BaseUrl}/api/ToLibrary/scan?libraryId={lib.id}", "Bearer", Auth))
                 return true;
         return false;
     }
@@ -70,7 +70,7 @@ public class Kavita : LibraryConnector
     /// <returns>Array of KavitaLibrary</returns>
     private IEnumerable<KavitaLibrary> GetLibraries()
     {
-        Stream data = NetClient.MakeRequest($"{BaseUrl}/api/Library/libraries", "Bearer", Auth);
+        Stream data = NetClient.MakeRequest($"{BaseUrl}/api/ToLibrary/libraries", "Bearer", Auth);
         if (data == Stream.Null)
         {
             Log.Info("No libraries found");
diff --git a/API/Schema/Manga.cs b/API/Schema/Manga.cs
index f635dfe..f3951cb 100644
--- a/API/Schema/Manga.cs
+++ b/API/Schema/Manga.cs
@@ -1,12 +1,7 @@
 using System.ComponentModel.DataAnnotations;
 using System.ComponentModel.DataAnnotations.Schema;
-using System.Diagnostics.CodeAnalysis;
-using System.Net;
-using System.Runtime.CompilerServices;
 using System.Runtime.InteropServices;
-using System.Text.RegularExpressions;
-using API.MangaDownloadClients;
-using API.Schema.Jobs;
+using System.Text;
 using API.Schema.MangaConnectors;
 using Microsoft.EntityFrameworkCore;
 using Newtonsoft.Json;
@@ -20,166 +15,129 @@ public class Manga
     [StringLength(64)]
     [Required]
     public string MangaId { get; init; }
-    [StringLength(256)]
-    [Required]
-    public string IdOnConnectorSite { get; init; }
-    [StringLength(512)]
-    [Required]
-    public string Name { get; internal set; }
-    [Required]
-    public string Description { get; internal set; }
-    [Url]
-    [StringLength(512)]
-    [Required]
-    public string WebsiteUrl { get; internal set; }
-    [JsonIgnore]
-    [Url]
-    public string CoverUrl { get; internal set; }
-    [JsonIgnore]
-    public string? CoverFileNameInCache { get; internal set; }
-    [Required]
-    public uint Year { get; internal set; }
-    [StringLength(8)]
-    public string? OriginalLanguage { get; internal set; }
-    [Required]
-    public MangaReleaseStatus ReleaseStatus { get; internal set; }
-    [StringLength(1024)]
-    [Required]
-    public string DirectoryName { get; private set; }
-    public LocalLibrary? Library { get; internal set; }
-    [JsonIgnore]
-    [NotMapped]
-    public string LibraryPath => Library is null ? TrangaSettings.downloadLocation : Library.BasePath;
-    [JsonIgnore]
-    [NotMapped]
-    public string FullDirectoryPath => Path.Join(LibraryPath, DirectoryName);
-    [Required]
-    public float IgnoreChapterBefore { get; internal set; }
-    [StringLength(64)]
-    [Required]
-    public string MangaConnectorId { get; private set; }
-    [JsonIgnore] public MangaConnector? MangaConnector { get; private set; }
+    [StringLength(256)] [Required] public string IdOnConnectorSite { get; init; }
+    [StringLength(512)] [Required] public string Name { get; internal set; }
+    [Required] public string Description { get; internal set; }
+    [Url] [StringLength(512)] [Required] public string WebsiteUrl { get; internal init; }
+    [JsonIgnore] [Url] [StringLength(512)] public string CoverUrl { get; internal set; }
+    [Required] public MangaReleaseStatus ReleaseStatus { get; internal set; }
     
-    [JsonIgnore] public ICollection<Author>? Authors { get; internal set; }
-    [NotMapped]
-    [StringLength(64)]
-    [Required]
-    public IEnumerable<string> AuthorIds => Authors?.Select(a => a.AuthorId) ?? [];
+    [StringLength(64)] 
+    public string? LibraryId { get; init; }
+    [JsonIgnore] public LocalLibrary? Library { get; internal set; }
     
-    [JsonIgnore] public ICollection<MangaTag>? MangaTags { get; internal set; }
-    [NotMapped]
-    [StringLength(64)]
+    [StringLength(32)]
     [Required]
-    public IEnumerable<string> Tags => MangaTags?.Select(t => t.Tag) ?? [];
-    
-    
-    [JsonIgnore] public ICollection<Link>? Links { get; internal set; }
-    [NotMapped]
-    [StringLength(64)]
-    [Required]
-    public IEnumerable<string> LinkIds => Links?.Select(l => l.LinkId) ?? [];
-    
-    [JsonIgnore] public ICollection<MangaAltTitle>? AltTitles { get; internal set; }
-    [NotMapped]
-    [StringLength(64)]
-    [Required]
-    public IEnumerable<string> AltTitleIds => AltTitles?.Select(a => a.AltTitleId) ?? [];
+    public string MangaConnectorName { get; init; }
+    [JsonIgnore] public MangaConnector MangaConnector { get; init; } = null!;
 
-    public Manga(string idOnConnectorSite, string name, string description, string websiteUrl, string coverUrl,
-        string? coverFileNameInCache, uint year, string? originalLanguage, MangaReleaseStatus releaseStatus,
-        float ignoreChapterBefore, MangaConnector mangaConnector, ICollection<Author> authors,
-        ICollection<MangaTag> mangaTags, ICollection<Link> links, ICollection<MangaAltTitle> altTitles,
-        LocalLibrary? library = null)
-        : this(idOnConnectorSite, name, description, websiteUrl, coverUrl, coverFileNameInCache, year, originalLanguage,
-            releaseStatus, ignoreChapterBefore, mangaConnector.Name)
+    public ICollection<Author> Authors { get; internal set; }= null!;
+    public ICollection<MangaTag> MangaTags { get; internal set; }= null!;
+    public ICollection<Link> Links { get; internal set; }= null!;
+    public ICollection<MangaAltTitle> AltTitles { get; internal set; } = null!;
+    [Required] public float IgnoreChaptersBefore { get; internal set; }
+    [StringLength(1024)] [Required] public string DirectoryName { get; private set; }
+
+    [JsonIgnore] [StringLength(512)] public string? CoverFileNameInCache { get; internal set; } = null;
+    [Required] public uint? Year { get; internal init; }
+    [StringLength(8)] public string? OriginalLanguage { get; internal init; }
+
+    [JsonIgnore]
+    [NotMapped]
+    public string? FullDirectoryPath => Library is not null ? Path.Join(Library.BasePath, DirectoryName) : null;
+    
+    [JsonIgnore] public ICollection<Chapter> Chapters { get; internal set; } = [];
+
+    public Manga(string idOnConnector, string name, string description, string websiteUrl, string coverUrl, MangaReleaseStatus releaseStatus,
+        MangaConnector mangaConnector, ICollection<Author> authors, ICollection<MangaTag> mangaTags, ICollection<Link> links, ICollection<MangaAltTitle> altTitles,
+        LocalLibrary? library = null, float ignoreChaptersBefore = 0f, uint? year = null, string? originalLanguage = null)
     {
+        this.MangaId = TokenGen.CreateToken(typeof(Manga), mangaConnector.Name, idOnConnector);
+        this.IdOnConnectorSite = idOnConnector;
+        this.Name = name;
+        this.Description = description;
+        this.WebsiteUrl = websiteUrl;
+        this.CoverUrl = coverUrl;
+        this.ReleaseStatus = releaseStatus;
+        this.LibraryId = library?.LocalLibraryId;
+        this.Library = library;
+        this.MangaConnectorName = mangaConnector.Name;
+        this.MangaConnector = mangaConnector;
         this.Authors = authors;
         this.MangaTags = mangaTags;
         this.Links = links;
         this.AltTitles = altTitles;
-        this.Library = library;
+        this.IgnoreChaptersBefore = ignoreChaptersBefore;
+        this.DirectoryName = CleanDirectoryName(name);
+        this.Year = year;
+        this.OriginalLanguage = originalLanguage;
+    }
+
+    /// <summary>
+    /// EF ONLY!!!
+    /// </summary>
+    public Manga(string mangaId, string idOnConnectorSite, string name, string description, string websiteUrl, string coverUrl, MangaReleaseStatus releaseStatus,
+        string mangaConnectorName, string directoryName, float ignoreChaptersBefore, string? libraryId, uint? year, string? originalLanguage)
+    {
+        this.MangaId = mangaId;
+        this.IdOnConnectorSite = idOnConnectorSite;
+        this.Name = name;
+        this.Description = description;
+        this.WebsiteUrl = websiteUrl;
+        this.CoverUrl = coverUrl;
+        this.ReleaseStatus = releaseStatus;
+        this.MangaConnectorName = mangaConnectorName;
+        this.DirectoryName = directoryName;
+        this.LibraryId = libraryId;
+        this.IgnoreChaptersBefore = ignoreChaptersBefore;
+        this.Year = year;
+        this.OriginalLanguage = originalLanguage;
     }
     
-    public Manga(string idOnConnectorSite, string name, string description, string websiteUrl, string coverUrl,
-        string? coverFileNameInCache, uint year, string? originalLanguage, MangaReleaseStatus releaseStatus,
-        float ignoreChapterBefore, string mangaConnectorId)
-    {
-        MangaId = TokenGen.CreateToken(typeof(Manga), mangaConnectorId, idOnConnectorSite);
-        IdOnConnectorSite = idOnConnectorSite;
-        Name = name;
-        Description = description;
-        WebsiteUrl = websiteUrl;
-        CoverUrl = coverUrl;
-        CoverFileNameInCache = coverFileNameInCache;
-        Year = year;
-        OriginalLanguage = originalLanguage;
-        ReleaseStatus = releaseStatus;
-        IgnoreChapterBefore = ignoreChapterBefore;
-        MangaConnectorId = mangaConnectorId;
-        DirectoryName = BuildFolderName(name);
-    }
-
-    public MoveFileOrFolderJob UpdateFolderName(string downloadLocation, string newName)
-    {
-        string oldName = this.DirectoryName;
-        this.DirectoryName = newName;
-        return new MoveFileOrFolderJob(Path.Join(downloadLocation, oldName), Path.Join(downloadLocation, this.DirectoryName));
-    }
-
-    internal void UpdateWithInfo(Manga other)
-    {
-        this.Name = other.Name;
-        this.Year = other.Year;
-        this.Description = other.Description;
-        this.CoverUrl = other.CoverUrl;
-        this.OriginalLanguage = other.OriginalLanguage;
-        this.Authors = other.Authors;
-        this.Links = other.Links;
-        this.MangaTags = other.MangaTags;
-        this.AltTitles = other.AltTitles;
-        this.ReleaseStatus = other.ReleaseStatus;
-    }
-
-    private static string BuildFolderName(string mangaName)
-    {
-        return mangaName;
-    }
-    
-    internal string? SaveCoverImageToCache(int retries = 3)
-    {
-        if(retries < 0)
-            return null;
-        
-        Regex urlRex = new (@"https?:\/\/((?:[a-zA-Z0-9-]+\.)+[a-zA-Z0-9]+)\/(?:.+\/)*(.+\.([a-zA-Z]+))");
-        //https?:\/\/[a-zA-Z0-9-]+\.([a-zA-Z0-9-]+\.[a-zA-Z0-9]+)\/(?:.+\/)*(.+\.([a-zA-Z]+)) for only second level domains
-        Match match = urlRex.Match(CoverUrl);
-        string filename = $"{match.Groups[1].Value}-{MangaId}.{match.Groups[3].Value}";
-        string saveImagePath = Path.Join(TrangaSettings.coverImageCache, filename);
-
-        if (File.Exists(saveImagePath))
-            return saveImagePath;
-        
-        RequestResult coverResult = new HttpDownloadClient().MakeRequest(CoverUrl, RequestType.MangaCover, $"https://{match.Groups[1].Value}");
-        if (coverResult.statusCode is < HttpStatusCode.OK or >= HttpStatusCode.Ambiguous)
-            return SaveCoverImageToCache(--retries);
-            
-        using MemoryStream ms = new();
-        coverResult.result.CopyTo(ms);
-        Directory.CreateDirectory(TrangaSettings.coverImageCache);
-        File.WriteAllBytes(saveImagePath, ms.ToArray());
-        
-        return saveImagePath;
-    }
     
     public string CreatePublicationFolder()
     {
-        string publicationFolder = Path.Join(LibraryPath, this.DirectoryName);
+        string publicationFolder = FullDirectoryPath;
         if(!Directory.Exists(publicationFolder))
             Directory.CreateDirectory(publicationFolder);
         if(RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
             File.SetUnixFileMode(publicationFolder, GroupRead | GroupWrite | GroupExecute | OtherRead | OtherWrite | OtherExecute | UserRead | UserWrite | UserExecute);
         return publicationFolder;
     }
-    
-    //TODO onchanges create job to update metadata files in archives, etc.
+
+    //https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file
+    //less than 32 is control *forbidden*
+    //34 is " *forbidden*
+    //42 is * *forbidden*
+    //47 is / *forbidden*
+    //58 is : *forbidden*
+    //60 is < *forbidden*
+    //62 is > *forbidden*
+    //63 is ? *forbidden*
+    //92 is \ *forbidden*
+    //124 is | *forbidden*
+    //127 is delete *forbidden*
+    //Below 127 all except *******
+    private static readonly int[] ForbiddenCharsBelow127 = [34, 42, 47, 58, 60, 62, 63, 92, 124, 127];
+    //Above 127 none except *******
+    private static readonly int[] IncludeCharsAbove127 = [128, 138, 142];
+    //128 is € include
+    //138 is Š include
+    //142 is Ž include
+    //152 through 255 looks fine except 157, 172, 173, 175 *******
+    private static readonly int[] ForbiddenCharsAbove152 = [157, 172, 173, 175];
+    private static string CleanDirectoryName(string name)
+    {
+        StringBuilder sb = new ();
+        foreach (char c in name)
+        {
+            if (c > 32 && c < 127 && ForbiddenCharsBelow127.Contains(c) == false)
+                sb.Append(c);
+            else if (c > 127 && c < 152 && IncludeCharsAbove127.Contains(c))
+                sb.Append(c);
+            else if(c >= 152 && c <= 255 && ForbiddenCharsAbove152.Contains(c) == false)
+                sb.Append(c);
+        }
+        return sb.ToString();
+    }
 }
\ No newline at end of file
diff --git a/API/Schema/MangaAltTitle.cs b/API/Schema/MangaAltTitle.cs
index 04e41f3..86248cf 100644
--- a/API/Schema/MangaAltTitle.cs
+++ b/API/Schema/MangaAltTitle.cs
@@ -1,5 +1,4 @@
 using System.ComponentModel.DataAnnotations;
-using System.ComponentModel.DataAnnotations.Schema;
 using Microsoft.EntityFrameworkCore;
 
 namespace API.Schema;
diff --git a/API/Schema/MangaConnectors/AsuraToon.cs b/API/Schema/MangaConnectors/AsuraToon.cs
deleted file mode 100644
index 8437bbb..0000000
--- a/API/Schema/MangaConnectors/AsuraToon.cs
+++ /dev/null
@@ -1,191 +0,0 @@
-using System.Text.RegularExpressions;
-using API.MangaDownloadClients;
-using HtmlAgilityPack;
-using log4net;
-
-namespace API.Schema.MangaConnectors;
-
-public class AsuraToon : MangaConnector
-{
-	
-	public AsuraToon() : base("AsuraToon", ["en"], ["asuracomic.net"], "https://asuracomic.net/images/logo.webp")
-	{
-		this.downloadClient = new ChromiumDownloadClient();
-	}
-
-	public override (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)[] GetManga(string publicationTitle = "")
-	{
-		string sanitizedTitle = string.Join(' ', Regex.Matches(publicationTitle, "[A-z]*").Where(m => m.Value.Length > 0)).ToLower();
-		string requestUrl = $"https://asuracomic.net/series?name={sanitizedTitle}";
-		RequestResult requestResult =
-			downloadClient.MakeRequest(requestUrl, RequestType.Default);
-		if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
-			return [];
-
-		if (requestResult.htmlDocument is null)
-		{
-			return [];
-		}
-			
-		(Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)[] publications = ParsePublicationsFromHtml(requestResult.htmlDocument);
-		return publications;
-	}
-
-	public override (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)? GetMangaFromId(string publicationId)
-	{
-		return GetMangaFromUrl($"https://asuracomic.net/series/{publicationId}");
-	}
-
-	public override (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)? GetMangaFromUrl(string url)
-	{
-		RequestResult requestResult = downloadClient.MakeRequest(url, RequestType.MangaInfo);
-		if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
-			return null;
-		if (requestResult.htmlDocument is null)
-		{
-			return null;
-		}
-		return ParseSinglePublicationFromHtml(requestResult.htmlDocument, url.Split('/')[^1], url);
-	}
-
-	private (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)[] ParsePublicationsFromHtml(HtmlDocument document)
-	{
-		HtmlNodeCollection mangaList = document.DocumentNode.SelectNodes("//a[starts-with(@href,'series')]");
-		if (mangaList is null || mangaList.Count < 1)
-			return [];
-
-		IEnumerable<string> urls = mangaList.Select(a => $"https://asuracomic.net/{a.GetAttributeValue("href", "")}");
-		
-		List<(Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)> ret = new();
-		foreach (string url in urls)
-		{
-			(Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)? manga = GetMangaFromUrl(url);
-			if (manga is { } x)
-				ret.Add(x);
-		}
-
-		return ret.ToArray();
-	}
-
-	private (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?) ParseSinglePublicationFromHtml(HtmlDocument document, string publicationId, string websiteUrl)
-	{
-		string? originalLanguage = null;
-		Dictionary<string, string> altTitles = new(), links = new();
-
-		HtmlNodeCollection genreNodes = document.DocumentNode.SelectNodes("//h3[text()='Genres']/../div/button");
-		string[] tags = genreNodes.Select(b => b.InnerText).ToArray();
-		List<MangaTag> mangaTags = tags.Select(t => new MangaTag(t)).ToList();
-		
-		HtmlNode statusNode = document.DocumentNode.SelectSingleNode("//h3[text()='Status']/../h3[2]");
-		MangaReleaseStatus releaseStatus = statusNode.InnerText.ToLower() switch
-		{
-			"ongoing" => MangaReleaseStatus.Continuing,
-			"hiatus" => MangaReleaseStatus.OnHiatus,
-			"completed" => MangaReleaseStatus.Completed,
-			"dropped" => MangaReleaseStatus.Cancelled,
-			"season end" => MangaReleaseStatus.Continuing,
-			"coming soon" => MangaReleaseStatus.Unreleased,
-			_ => MangaReleaseStatus.Unreleased
-		};
-		
-		HtmlNode coverNode = 
-			document.DocumentNode.SelectSingleNode("//img[@alt='poster']");
-		string coverUrl = coverNode.GetAttributeValue("src", "");
-		
-		HtmlNode titleNode = 
-			document.DocumentNode.SelectSingleNode("//title");
-		string sortName = Regex.Match(titleNode.InnerText, @"(.*) - Asura Scans").Groups[1].Value;
-		
-		HtmlNode descriptionNode =
-			document.DocumentNode.SelectSingleNode("//h3[starts-with(text(),'Synopsis')]/../span");
-		string description = descriptionNode?.InnerText??"";
-		
-		HtmlNodeCollection authorNodes = document.DocumentNode.SelectNodes("//h3[text()='Author']/../h3[not(text()='Author' or text()='_')]");
-		HtmlNodeCollection artistNodes = document.DocumentNode.SelectNodes("//h3[text()='Artist']/../h3[not(text()='Artist' or text()='_')]");
-		IEnumerable<string> authorNames = authorNodes is null ? [] : authorNodes.Select(a => a.InnerText);
-		IEnumerable<string> artistNames = artistNodes is null ? [] : artistNodes.Select(a => a.InnerText);
-		List<string> authorStrings = authorNames.Concat(artistNames).ToList();
-		List<Author> authors = authorStrings.Select(author => new Author(author)).ToList();
-
-		HtmlNode? firstChapterNode = document.DocumentNode.SelectSingleNode("//a[contains(@href, 'chapter/1')]/../following-sibling::h3");
-		uint year = uint.Parse(firstChapterNode?.InnerText.Split(' ')[^1] ?? "2000");
-
-		Manga manga = new (publicationId, sortName, description, websiteUrl, coverUrl, null, year,
-			originalLanguage, releaseStatus, -1,
-			this, 
-			authors, 
-			mangaTags, 
-			[],
-			[]);
-		
-		return (manga, authors, mangaTags, [], []);
-	}
-
-	public override Chapter[] GetChapters(Manga manga, string language="en")
-	{
-		string requestUrl = $"https://asuracomic.net/series/{manga.MangaId}";
-		// Leaving this in for verification if the page exists
-		RequestResult requestResult =
-			downloadClient.MakeRequest(requestUrl, RequestType.Default);
-		if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
-			return [];
-
-		//Return Chapters ordered by Chapter-Number
-		List<Chapter> chapters = ParseChaptersFromHtml(manga, requestUrl);
-		return chapters.Order().ToArray();
-	}
-
-	private List<Chapter> ParseChaptersFromHtml(Manga manga, string mangaUrl)
-	{
-		RequestResult result = downloadClient.MakeRequest(mangaUrl, RequestType.Default);
-		if ((int)result.statusCode < 200 || (int)result.statusCode >= 300 || result.htmlDocument is null)
-		{
-			return new List<Chapter>();
-		}
-
-		List<Chapter> ret = new();
-
-		HtmlNodeCollection chapterURLNodes = result.htmlDocument.DocumentNode.SelectNodes("//a[contains(@href, '/chapter/')]");
-		Regex infoRex = new(@"Chapter ([0-9]+)(.*)?");
-
-		foreach (HtmlNode chapterInfo in chapterURLNodes)
-		{
-			string chapterUrl = chapterInfo.GetAttributeValue("href", "");
-
-			Match match = infoRex.Match(chapterInfo.InnerText);
-			string chapterNumber = new(match.Groups[1].Value);
-			string? chapterName = match.Groups[2].Success && match.Groups[2].Length > 1 ? match.Groups[2].Value : null;
-			string url = $"https://asuracomic.net/series/{chapterUrl}";
-			try
-			{
-				ret.Add(new Chapter(manga, url, chapterNumber, null, chapterName));
-			}
-			catch (Exception e)
-			{
-			}
-		}
-		
-		return ret;
-	}
-
-	internal override string[] GetChapterImageUrls(Chapter chapter)
-	{
-		string requestUrl = chapter.Url;
-		// Leaving this in to check if the page exists
-		RequestResult requestResult =
-			downloadClient.MakeRequest(requestUrl, RequestType.Default);
-		if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300 || requestResult.htmlDocument is null)
-		{
-			return [];
-		}
-		string[] imageUrls = ParseImageUrlsFromHtml(requestResult.htmlDocument);
-		return imageUrls;
-	}
-
-	private string[] ParseImageUrlsFromHtml(HtmlDocument document)
-	{
-		HtmlNodeCollection images = document.DocumentNode.SelectNodes("//img[contains(@alt, 'chapter page')]");
-
-		return images.Select(i => i.GetAttributeValue("src", "")).ToArray();
-	}
-}
\ No newline at end of file
diff --git a/API/Schema/MangaConnectors/Bato.cs b/API/Schema/MangaConnectors/Bato.cs
deleted file mode 100644
index 0cff375..0000000
--- a/API/Schema/MangaConnectors/Bato.cs
+++ /dev/null
@@ -1,203 +0,0 @@
-using System.Net;
-using System.Text.RegularExpressions;
-using API.MangaDownloadClients;
-using HtmlAgilityPack;
-
-namespace API.Schema.MangaConnectors;
-
-public class Bato : MangaConnector
-{
-	
-	public Bato() : base("Bato", ["en"], ["bato.to"], "https://bato.to/amsta/img/batoto/favicon.ico")
-	{
-		this.downloadClient = new HttpDownloadClient();
-	}
-
-	public override (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)[] GetManga(string publicationTitle = "")
-	{
-		string sanitizedTitle = string.Join(' ', Regex.Matches(publicationTitle, "[A-z]*").Where(m => m.Value.Length > 0)).ToLower();
-		string requestUrl = $"https://bato.to/v3x-search?word={sanitizedTitle}&lang=en";
-		RequestResult requestResult =
-			downloadClient.MakeRequest(requestUrl, RequestType.Default);
-		if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
-			return [];
-
-		if (requestResult.htmlDocument is null)
-		{
-			return [];
-		}
-			
-		(Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)[] publications = ParsePublicationsFromHtml(requestResult.htmlDocument);
-		return publications;
-	}
-
-	public override (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)? GetMangaFromId(string publicationId)
-	{
-		return GetMangaFromUrl($"https://bato.to/title/{publicationId}");
-	}
-
-	public override (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)? GetMangaFromUrl(string url)
-	{
-		RequestResult requestResult = downloadClient.MakeRequest(url, RequestType.MangaInfo);
-		if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
-			return null;
-		if (requestResult.htmlDocument is null)
-		{
-			return null;
-		}
-		return ParseSinglePublicationFromHtml(requestResult.htmlDocument, url.Split('/')[^1], url);
-	}
-
-	private (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)[] ParsePublicationsFromHtml(HtmlDocument document)
-	{
-		HtmlNode mangaList = document.DocumentNode.SelectSingleNode("//div[@data-hk='0-0-2']");
-		if (!mangaList.ChildNodes.Any(node => node.Name == "div"))
-			return [];
-
-		List<string> urls = mangaList.ChildNodes
-			.Select(node => $"https://bato.to{node.Descendants("div").First().FirstChild.GetAttributeValue("href", "")}").ToList();
-		
-		HashSet<(Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)> ret = new();
-		foreach (string url in urls)
-		{
-			(Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)? manga = GetMangaFromUrl(url);
-			if (manga is { } x)
-				ret.Add(x);
-		}
-
-		return ret.ToArray();
-	}
-
-	private (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?) ParseSinglePublicationFromHtml(HtmlDocument document, string publicationId, string websiteUrl)
-	{
-		HtmlNode infoNode = document.DocumentNode.SelectSingleNode("/html/body/div/main/div[1]/div[2]");
-
-		string sortName = infoNode.Descendants("h3").First().InnerText;
-		string description = document.DocumentNode
-			.SelectSingleNode("//div[contains(concat(' ',normalize-space(@class),' '),'prose')]").InnerText;
-
-		string[] altTitlesList = infoNode.ChildNodes[1].ChildNodes[2].InnerText.Split('/');
-		int i = 0;
-		List<MangaAltTitle> altTitles = altTitlesList.Select(a => new MangaAltTitle(i++.ToString(), a)).ToList();
-
-		string coverUrl = document.DocumentNode.SelectNodes("//img")
-			.First(child => child.GetAttributeValue("data-hk", "") == "0-1-0").GetAttributeValue("src", "").Replace("&amp;", "&");
-
-		List<HtmlNode> genreNodes = document.DocumentNode.SelectSingleNode("//b[text()='Genres:']/..").SelectNodes("span").ToList();
-		string[] tags = genreNodes.Select(node => node.FirstChild.InnerText).ToArray();
-		List<MangaTag> mangaTags = tags.Select(s => new MangaTag(s)).ToList();
-
-		List<HtmlNode> authorsNodes = infoNode.ChildNodes[1].ChildNodes[3].Descendants("a").ToList();
-		List<string> authorNames = authorsNodes.Select(node => node.InnerText.Replace("amp;", "")).ToList();
-		List<Author> authors = authorNames.Select(n => new Author(n)).ToList();
-
-		HtmlNode? originalLanguageNode = document.DocumentNode.SelectSingleNode("//span[text()='Tr From']/..");
-		string originalLanguage = originalLanguageNode is not null ? originalLanguageNode.LastChild.InnerText : "";
-		
-		if (!uint.TryParse(
-			    document.DocumentNode.SelectSingleNode("//span[text()='Original Publication:']/..").LastChild.InnerText.Split('-')[0],
-			    out uint year))
-			year = (uint)DateTime.UtcNow.Year;
-
-		string status = document.DocumentNode.SelectSingleNode("//span[text()='Original Publication:']/..")
-			.ChildNodes[2].InnerText;
-		MangaReleaseStatus releaseStatus = MangaReleaseStatus.Unreleased;
-		switch (status.ToLower())
-		{
-			case "ongoing": releaseStatus = MangaReleaseStatus.Continuing; break;
-			case "completed": releaseStatus = MangaReleaseStatus.Completed; break;
-			case "hiatus": releaseStatus = MangaReleaseStatus.OnHiatus; break;
-			case "cancelled": releaseStatus = MangaReleaseStatus.Cancelled; break;
-			case "pending": releaseStatus = MangaReleaseStatus.Unreleased; break;
-		}
-
-		Manga manga = new (publicationId, sortName, description, websiteUrl, coverUrl, null, year,
-			originalLanguage, releaseStatus, -1,
-			this, 
-			authors, 
-			mangaTags, 
-			[],
-			altTitles);
-		
-		return (manga, authors, mangaTags, [], altTitles);
-	}
-
-	public override Chapter[] GetChapters(Manga manga, string language="en")
-	{
-		string requestUrl = $"https://bato.to/title/{manga.MangaId}";
-		// Leaving this in for verification if the page exists
-		RequestResult requestResult =
-			downloadClient.MakeRequest(requestUrl, RequestType.Default);
-		if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
-			return [];
-
-		//Return Chapters ordered by Chapter-Number
-		List<Chapter> chapters = ParseChaptersFromHtml(manga, requestUrl);
-		return chapters.Order().ToArray();
-	}
-
-	private List<Chapter> ParseChaptersFromHtml(Manga manga, string mangaUrl)
-	{
-		RequestResult result = downloadClient.MakeRequest(mangaUrl, RequestType.Default);
-		if ((int)result.statusCode < 200 || (int)result.statusCode >= 300 || result.htmlDocument is null)
-		{
-			return new List<Chapter>();
-		}
-
-		List<Chapter> ret = new();
-
-		HtmlNode chapterList =
-			result.htmlDocument.DocumentNode.SelectSingleNode("/html/body/div/main/div[3]/astro-island/div/div[2]/div/div/astro-slot");
-
-		Regex numberRex = new(@"\/title\/.+\/([0-9])+(?:-vol_([0-9]+))?-ch_([0-9\.]+)");
-
-		foreach (HtmlNode chapterInfo in chapterList.SelectNodes("div"))
-		{
-			HtmlNode infoNode = chapterInfo.FirstChild.FirstChild;
-			string chapterUrl = infoNode.GetAttributeValue("href", "");
-
-			Match match = numberRex.Match(chapterUrl);
-			string id = match.Groups[1].Value;
-			int? volumeNumber = match.Groups[2].Success ? int.Parse(match.Groups[2].Value) : null;
-			string chapterNumber = new(match.Groups[3].Value);
-			string url = $"https://bato.to{chapterUrl}?load=2";
-			try
-			{
-				ret.Add(new Chapter(manga, url, chapterNumber, volumeNumber, null));
-			}
-			catch (Exception e)
-			{
-			}
-		}
-		
-		return ret;
-	}
-
-	internal override string[] GetChapterImageUrls(Chapter chapter)
-	{
-		string requestUrl = chapter.Url;
-		// Leaving this in to check if the page exists
-		RequestResult requestResult =
-			downloadClient.MakeRequest(requestUrl, RequestType.Default);
-		if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300 || requestResult.htmlDocument is null)
-		{
-			return [];
-		}
-
-		string[] imageUrls = ParseImageUrlsFromHtml(requestResult.htmlDocument);
-		return imageUrls;
-	}
-
-	private string[] ParseImageUrlsFromHtml(HtmlDocument document)
-	{
-		HtmlNode images = document.DocumentNode.SelectNodes("//astro-island").First(node =>
-			node.GetAttributeValue("component-url", "").Contains("/_astro/ImageList."));
-
-		string weirdString = images.OuterHtml;
-		string weirdString2 = Regex.Match(weirdString, @"props=\""(.*)}\""").Groups[1].Value;
-		string[] urls = Regex.Matches(weirdString2, @"(https:\/\/[A-z\-0-9\.\?\&\;\=\/]+)\\")
-			.Select(match => match.Groups[1].Value.Replace("&amp;", "&")).ToArray();
-		
-		return urls;
-	}
-}
\ No newline at end of file
diff --git a/API/Schema/MangaConnectors/Global.cs b/API/Schema/MangaConnectors/Global.cs
index 52a89f3..c4f0b73 100644
--- a/API/Schema/MangaConnectors/Global.cs
+++ b/API/Schema/MangaConnectors/Global.cs
@@ -8,15 +8,14 @@ public class Global : MangaConnector
         this.context = context;
     }
 
-    public override (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)[] GetManga(string publicationTitle = "")
+    public override Manga[] SearchManga(string mangaSearchName)
     {
         //Get all enabled Connectors
         MangaConnector[] enabledConnectors = context.MangaConnectors.Where(c => c.Enabled && c.Name != "Global").ToArray();
         
         //Create Task for each MangaConnector to search simulatneously
-        Task<(Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)[]>[] tasks =
-            enabledConnectors.Select(c =>
-                new Task<(Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)[]>(() => c.GetManga(publicationTitle))).ToArray();
+        Task<Manga[]>[] tasks =
+            enabledConnectors.Select(c => new Task<Manga[]>(() => c.SearchManga(mangaSearchName))).ToArray();
         foreach (var task in tasks)
             task.Start();
         
@@ -27,29 +26,28 @@ public class Global : MangaConnector
         }while(tasks.Any(t => t.Status < TaskStatus.RanToCompletion));
         
         //Concatenate all results into one
-        (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)[] ret = 
-            tasks.Select(t => t.IsCompletedSuccessfully ? t.Result : []).ToArray().SelectMany(i => i).ToArray();
+        Manga[] ret = tasks.Select(t => t.IsCompletedSuccessfully ? t.Result : []).ToArray().SelectMany(i => i).ToArray();
         return ret;
     }
 
-    public override (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)? GetMangaFromUrl(string url)
+    public override Manga? GetMangaFromUrl(string url)
     {
-        MangaConnector? mc = context.MangaConnectors.ToArray().FirstOrDefault(c => c.ValidateUrl(url));
+        MangaConnector? mc = context.MangaConnectors.ToArray().FirstOrDefault(c => c.UrlMatchesConnector(url));
         return mc?.GetMangaFromUrl(url) ?? null;
     }
 
-    public override (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)? GetMangaFromId(string publicationId)
+    public override Manga? GetMangaFromId(string mangaIdOnSite)
     {
         return null;
     }
 
-    public override Chapter[] GetChapters(Manga manga, string language = "en")
+    public override Chapter[] GetChapters(Manga manga, string? language = null)
     {
-        return manga.MangaConnector?.GetChapters(manga) ?? [];
+        return manga.MangaConnector.GetChapters(manga, language);
     }
 
     internal override string[] GetChapterImageUrls(Chapter chapter)
     {
-        return chapter.ParentManga?.MangaConnector?.GetChapterImageUrls(chapter) ?? [];
+        return chapter.ParentManga.MangaConnector.GetChapterImageUrls(chapter);
     }
 }
\ No newline at end of file
diff --git a/API/Schema/MangaConnectors/MangaConnector.cs b/API/Schema/MangaConnectors/MangaConnector.cs
index 09d9856..a3f5e76 100644
--- a/API/Schema/MangaConnectors/MangaConnector.cs
+++ b/API/Schema/MangaConnectors/MangaConnector.cs
@@ -11,6 +11,14 @@ namespace API.Schema.MangaConnectors;
 [PrimaryKey("Name")]
 public abstract class MangaConnector(string name, string[] supportedLanguages, string[] baseUris, string iconUrl)
 {
+    [JsonIgnore]
+    [NotMapped]
+    internal DownloadClient downloadClient { get; init; } = null!;
+
+    [JsonIgnore]
+    [NotMapped]
+    protected ILog Log { get; init; } = LogManager.GetLogger(name);
+    
     [StringLength(32)]
     [Required]
     public string Name { get; init; } = name;
@@ -26,32 +34,41 @@ public abstract class MangaConnector(string name, string[] supportedLanguages, s
     [Required]
     public bool Enabled { get; internal set; } = true;
     
-    public abstract (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)[] GetManga(string publicationTitle = "");
+    public abstract Manga[] SearchManga(string mangaSearchName);
 
-    public abstract (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)? GetMangaFromUrl(string url);
+    public abstract Manga? GetMangaFromUrl(string url);
 
-    public abstract (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)? GetMangaFromId(string publicationId);
+    public abstract Manga? GetMangaFromId(string mangaIdOnSite);
     
-    public abstract Chapter[] GetChapters(Manga manga, string language="en");
-    
-    [JsonIgnore]
-    [NotMapped]
-    internal DownloadClient downloadClient { get; init; } = null!;
-
-    [JsonIgnore]
-    [NotMapped]
-    protected ILog Log { get; init; } = LogManager.GetLogger(name);
-    
-    public Chapter[] GetNewChapters(Manga manga)
-    {
-        Chapter[] allChapters = GetChapters(manga);
-        if (allChapters.Length < 1)
-            return [];
-        
-        return allChapters.Where(chapter => !chapter.IsDownloaded()).ToArray();
-    }
+    public abstract Chapter[] GetChapters(Manga manga, string? language = null);
 
     internal abstract string[] GetChapterImageUrls(Chapter chapter);
 
-    public bool ValidateUrl(string url) => BaseUris.Any(baseUri => Regex.IsMatch(url, "https?://" + baseUri + "/.*"));
+    public bool UrlMatchesConnector(string url) => BaseUris.Any(baseUri => Regex.IsMatch(url, "https?://" + baseUri + "/.*"));
+    
+    internal string? SaveCoverImageToCache(Manga manga, int retries = 3)
+    {
+        if(retries < 0)
+            return null;
+        
+        Regex urlRex = new (@"https?:\/\/((?:[a-zA-Z0-9-]+\.)+[a-zA-Z0-9]+)\/(?:.+\/)*(.+\.([a-zA-Z]+))");
+        //https?:\/\/[a-zA-Z0-9-]+\.([a-zA-Z0-9-]+\.[a-zA-Z0-9]+)\/(?:.+\/)*(.+\.([a-zA-Z]+)) for only second level domains
+        Match match = urlRex.Match(manga.CoverUrl);
+        string filename = $"{match.Groups[1].Value}-{manga.MangaId}.{match.Groups[3].Value}";
+        string saveImagePath = Path.Join(TrangaSettings.coverImageCache, filename);
+
+        if (File.Exists(saveImagePath))
+            return saveImagePath;
+        
+        RequestResult coverResult = downloadClient.MakeRequest(manga.CoverUrl, RequestType.MangaCover, $"https://{match.Groups[1].Value}");
+        if ((int)coverResult.statusCode < 200 || (int)coverResult.statusCode >= 300)
+            return SaveCoverImageToCache(manga, --retries);
+            
+        using MemoryStream ms = new();
+        coverResult.result.CopyTo(ms);
+        Directory.CreateDirectory(TrangaSettings.coverImageCache);
+        File.WriteAllBytes(saveImagePath, ms.ToArray());
+        
+        return saveImagePath;
+    }
 }
\ No newline at end of file
diff --git a/API/Schema/MangaConnectors/MangaDex.cs b/API/Schema/MangaConnectors/MangaDex.cs
index a7e8bd6..4557007 100644
--- a/API/Schema/MangaConnectors/MangaDex.cs
+++ b/API/Schema/MangaConnectors/MangaDex.cs
@@ -1,8 +1,6 @@
-using System.Net;
-using System.Text.Json.Nodes;
-using System.Text.RegularExpressions;
+using System.Text.RegularExpressions;
 using API.MangaDownloadClients;
-using JsonSerializer = System.Text.Json.JsonSerializer;
+using Newtonsoft.Json.Linq;
 
 namespace API.Schema.MangaConnectors;
 
@@ -11,313 +9,327 @@ public class MangaDex : MangaConnector
     //https://api.mangadex.org/docs/3-enumerations/#language-codes--localization
     //https://en.wikipedia.org/wiki/List_of_ISO_639_language_codes
     //https://gist.github.com/Josantonius/b455e315bc7f790d14b136d61d9ae469
-    public MangaDex() : base("MangaDex", ["en","pt","pt-br","it","de","ru","aa","ab","ae","af","ak","am","an","ar-ae","ar-bh","ar-dz","ar-eg","ar-iq","ar-jo","ar-kw","ar-lb","ar-ly","ar-ma","ar-om","ar-qa","ar-sa","ar-sy","ar-tn","ar-ye","ar","as","av","ay","az","ba","be","bg","bh","bi","bm","bn","bo","br","bs","ca","ce","ch","co","cr","cs","cu","cv","cy","da","de-at","de-ch","de-de","de-li","de-lu","div","dv","dz","ee","el","en-au","en-bz","en-ca","en-cb","en-gb","en-ie","en-jm","en-nz","en-ph","en-tt","en-us","en-za","en-zw","eo","es-ar","es-bo","es-cl","es-co","es-cr","es-do","es-ec","es-es","es-gt","es-hn","es-la","es-mx","es-ni","es-pa","es-pe","es-pr","es-py","es-sv","es-us","es-uy","es-ve","es","et","eu","fa","ff","fi","fj","fo","fr-be","fr-ca","fr-ch","fr-fr","fr-lu","fr-mc","fr","fy","ga","gd","gl","gn","gu","gv","ha","he","hi","ho","hr-ba","hr-hr","hr","ht","hu","hy","hz","ia","id","ie","ig","ii","ik","in","io","is","it-ch","it-it","iu","iw","ja","ja-ro","ji","jv","jw","ka","kg","ki","kj","kk","kl","km","kn","ko","ko-ro","kr","ks","ku","kv","kw","ky","kz","la","lb","lg","li","ln","lo","ls","lt","lu","lv","mg","mh","mi","mk","ml","mn","mo","mr","ms-bn","ms-my","ms","mt","my","na","nb","nd","ne","ng","nl-be","nl-nl","nl","nn","no","nr","ns","nv","ny","oc","oj","om","or","os","pa","pi","pl","ps","pt-pt","qu-bo","qu-ec","qu-pe","qu","rm","rn","ro","rw","sa","sb","sc","sd","se-fi","se-no","se-se","se","sg","sh","si","sk","sl","sm","sn","so","sq","sr-ba","sr-sp","sr","ss","st","su","sv-fi","sv-se","sv","sw","sx","syr","ta","te","tg","th","ti","tk","tl","tn","to","tr","ts","tt","tw","ty","ug","uk","ur","us","uz","ve","vi","vo","wa","wo","xh","yi","yo","za","zh-cn","zh-hk","zh-mo","zh-ro","zh-sg","zh-tw","zh","zu"], ["mangadex.org"], "https://mangadex.org/favicon.ico")
+    public MangaDex() : base("MangaDex", 
+        ["en","pt","pt-br","it","de","ru","aa","ab","ae","af","ak","am","an","ar-ae","ar-bh","ar-dz","ar-eg","ar-iq","ar-jo","ar-kw","ar-lb","ar-ly","ar-ma","ar-om","ar-qa","ar-sa","ar-sy","ar-tn","ar-ye","ar","as","av","ay","az","ba","be","bg","bh","bi","bm","bn","bo","br","bs","ca","ce","ch","co","cr","cs","cu","cv","cy","da","de-at","de-ch","de-de","de-li","de-lu","div","dv","dz","ee","el","en-au","en-bz","en-ca","en-cb","en-gb","en-ie","en-jm","en-nz","en-ph","en-tt","en-us","en-za","en-zw","eo","es-ar","es-bo","es-cl","es-co","es-cr","es-do","es-ec","es-es","es-gt","es-hn","es-la","es-mx","es-ni","es-pa","es-pe","es-pr","es-py","es-sv","es-us","es-uy","es-ve","es","et","eu","fa","ff","fi","fj","fo","fr-be","fr-ca","fr-ch","fr-fr","fr-lu","fr-mc","fr","fy","ga","gd","gl","gn","gu","gv","ha","he","hi","ho","hr-ba","hr-hr","hr","ht","hu","hy","hz","ia","id","ie","ig","ii","ik","in","io","is","it-ch","it-it","iu","iw","ja","ja-ro","ji","jv","jw","ka","kg","ki","kj","kk","kl","km","kn","ko","ko-ro","kr","ks","ku","kv","kw","ky","kz","la","lb","lg","li","ln","lo","ls","lt","lu","lv","mg","mh","mi","mk","ml","mn","mo","mr","ms-bn","ms-my","ms","mt","my","na","nb","nd","ne","ng","nl-be","nl-nl","nl","nn","no","nr","ns","nv","ny","oc","oj","om","or","os","pa","pi","pl","ps","pt-pt","qu-bo","qu-ec","qu-pe","qu","rm","rn","ro","rw","sa","sb","sc","sd","se-fi","se-no","se-se","se","sg","sh","si","sk","sl","sm","sn","so","sq","sr-ba","sr-sp","sr","ss","st","su","sv-fi","sv-se","sv","sw","sx","syr","ta","te","tg","th","ti","tk","tl","tn","to","tr","ts","tt","tw","ty","ug","uk","ur","us","uz","ve","vi","vo","wa","wo","xh","yi","yo","za","zh-cn","zh-hk","zh-mo","zh-ro","zh-sg","zh-tw","zh","zu"],
+        ["mangadex.org"], 
+        "https://mangadex.org/favicon.ico")
     {
         this.downloadClient = new HttpDownloadClient();
     }
 
-    public override (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)[] GetManga(string publicationTitle = "")
+    private const int Limit = 100;
+    public override Manga[] SearchManga(string mangaSearchName)
     {
-        const int limit = 100; //How many values we want returned at once
-        int offset = 0; //"Page"
-        int total = int.MaxValue; //How many total results are there, is updated on first request
-        HashSet<(Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)> retManga = new();
-        List<JsonNode> results = new();
+        Log.Info($"Searching Manga: {mangaSearchName}");
+        List<Manga> mangas = new ();
         
-        //Request all search-results
-        while (offset < total) //As long as we haven't requested all "Pages"
+        int offset = 0;
+        int total = int.MaxValue;
+        while(offset < total)
         {
-            //Request next Page
             string requestUrl =
-                $"https://api.mangadex.org/manga?limit={limit}&title={publicationTitle}&offset={offset}" +
-                $"&includes[]=manga&includes[]=cover_art&includes[]=author&includes[]=artist&includes[]=tag";
-            RequestResult requestResult = downloadClient.MakeRequest(requestUrl, RequestType.MangaInfo);
-            if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
-            {
-                Log.Info($"{requestResult.statusCode}: {requestUrl}");
-                break;
-            }
-            JsonObject? result = JsonSerializer.Deserialize<JsonObject>(requestResult.result);
-            
-            offset += limit;
-            if (result is null)
-            {
-                Log.Info($"result was null: {requestUrl}");
-                break;
-            }
-            
-            if(result.ContainsKey("total"))
-                total = result["total"]!.GetValue<int>(); //Update the total number of Publications
-            else continue;
+                $"https://api.mangadex.org/manga?limit={Limit}&offset={offset}&title={mangaSearchName}" +
+                $"&contentRating%5B%5D=safe&contentRating%5B%5D=suggestive&contentRating%5B%5D=erotica" +
+                $"&includes%5B%5D=manga&includes%5B%5D=cover_art&includes%5B%5D=author&includes%5B%5D=artist&includes%5B%5D=tag'";
+            offset += Limit;
 
-            if (result.ContainsKey("data"))
-                results.AddRange(result["data"]!.AsArray()!);//Manga-data-Array
+            RequestResult result = downloadClient.MakeRequest(requestUrl, RequestType.MangaDexFeed);
+            if ((int)result.statusCode < 200 || (int)result.statusCode >= 300)
+            {
+                Log.Error("Request failed");
+                return [];
+            }
+
+            using StreamReader sr = new (result.result);
+            JObject jObject = JObject.Parse(sr.ReadToEnd());
+
+            if (jObject.Value<string>("result") != "ok")
+            {
+                JArray? errors = jObject["errors"] as JArray;
+                Log.Error($"Request failed: {string.Join(',', errors?.Select(e => e.Value<string>("title")) ?? [])}");
+                return [];
+            }
+
+            total = jObject.Value<int>("total");
+            
+            JArray? data = jObject.Value<JArray>("data");
+            if (data is null)
+            {
+                Log.Error("Data was null");
+                return [];
+            }
+            
+            mangas.AddRange(data.Select(ParseMangaFromJToken));
         }
         
-        foreach (JsonNode mangaNode in results)
-        {
-            if(MangaFromJsonObject(mangaNode.AsObject()) is { } manga)
-                retManga.Add(manga); //Add Publication (Manga) to result
-        }
-        return retManga.ToArray();
+        Log.Info($"Search {mangaSearchName} yielded {mangas.Count} results.");
+        return mangas.ToArray();
     }
 
-    public override (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)? GetMangaFromId(string publicationId)
+    private static readonly Regex GetMangaIdFromUrl = new(@"https?:\/\/mangadex\.org\/title\/([a-z0-9-]+)\/?.*");
+    public override Manga? GetMangaFromUrl(string url)
     {
-        string url = $"https://api.mangadex.org/manga/{publicationId}" +
-                     $"?includes%5B%5D=manga&includes%5B%5D=cover_art&includes%5B%5D=author&includes%5B%5D=artist&includes%5B%5D=tag";
-        RequestResult requestResult = downloadClient.MakeRequest(url, RequestType.MangaInfo);
-        if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
+        Log.Info($"Getting Manga: {url}");
+        if (!UrlMatchesConnector(url))
         {
-            Log.Info($"{requestResult.statusCode}: {url}");
+            Log.Debug($"Url is not for Connector. {url}");
             return null;
         }
-        JsonObject? result = JsonSerializer.Deserialize<JsonObject>(requestResult.result);
-        if(result is not null)
-            return MangaFromJsonObject(result["data"]!.AsObject());
-        Log.Info($"result was null: {url}");
-        return null;
-    }
 
-    public override (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)? GetMangaFromUrl(string url)
-    {
-        Regex idRex = new (@"https:\/\/mangadex.org\/title\/([A-z0-9-]*)\/.*");
-        string id = idRex.Match(url).Groups[1].Value;
+        Match match = GetMangaIdFromUrl.Match(url);
+        if (!match.Success || !match.Groups[1].Success)
+        {
+            Log.Debug($"Url is not for Connector (Could not retrieve id). {url}");
+            return null;
+        }
+        string id = match.Groups[1].Value;
+
         return GetMangaFromId(id);
     }
 
-    private (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)? MangaFromJsonObject(JsonObject manga)
+    public override Manga? GetMangaFromId(string mangaIdOnSite)
     {
-        if (!manga.TryGetPropertyValue("id", out JsonNode? idNode))
+        Log.Info($"Getting Manga: {mangaIdOnSite}");
+        string requestUrl =
+            $"https://api.mangadex.org/manga/{mangaIdOnSite}" +
+            $"?includes%5B%5D=manga&includes%5B%5D=cover_art&includes%5B%5D=author&includes%5B%5D=artist&includes%5B%5D=tag'";
+        
+        RequestResult result = downloadClient.MakeRequest(requestUrl, RequestType.MangaDexFeed);
+        if ((int)result.statusCode < 200 || (int)result.statusCode >= 300)
         {
-            Log.Info("id was null");
+            Log.Error("Request failed");
             return null;
         }
-        string publicationId = idNode!.GetValue<string>();
-            
-        if (!manga.TryGetPropertyValue("attributes", out JsonNode? attributesNode))
-        {
-            Log.Info("attributes was null");
-            return null;
-        }
-        JsonObject attributes = attributesNode!.AsObject();
 
-        if (!attributes.TryGetPropertyValue("title", out JsonNode? titleNode))
-        {
-            Log.Info("title was null");
-            return null;
-        }
-        string sortName = titleNode!.AsObject().ContainsKey("en") switch
-        {
-            true => titleNode.AsObject()["en"]!.GetValue<string>(),
-            false => titleNode.AsObject().First().Value!.GetValue<string>()
-        };
-        
-        Dictionary<string, string> altTitlesDict = new();
-        if (attributes.TryGetPropertyValue("altTitles", out JsonNode? altTitlesNode))
-        {
-            foreach (JsonNode? altTitleNode in altTitlesNode!.AsArray())
-            {
-                JsonObject altTitleNodeObject = altTitleNode!.AsObject();
-                altTitlesDict.TryAdd(altTitleNodeObject.First().Key, altTitleNodeObject.First().Value!.GetValue<string>());
-            }
-        }
-        List<MangaAltTitle> altTitles = altTitlesDict.Select(t => new MangaAltTitle(t.Key, t.Value)).ToList();
+        using StreamReader sr = new (result.result);
+        JObject jObject = JObject.Parse(sr.ReadToEnd());
 
-        if (!attributes.TryGetPropertyValue("description", out JsonNode? descriptionNode))
+        if (jObject.Value<string>("result") != "ok")
         {
-            Log.Info("description was null");
-            return null;
-        }
-        string description = descriptionNode!.AsObject().ContainsKey("en") switch
-        {
-            true => descriptionNode.AsObject()["en"]!.GetValue<string>(),
-            false => descriptionNode.AsObject().FirstOrDefault().Value?.GetValue<string>() ?? ""
-        };
-
-        Dictionary<string, string> linksDict = new();
-        if (attributes.TryGetPropertyValue("links", out JsonNode? linksNode) && linksNode is not null)
-            foreach (KeyValuePair<string, JsonNode?> linkKv in linksNode!.AsObject())
-                linksDict.TryAdd(linkKv.Key, linkKv.Value.GetValue<string>());
-        List<Link> links = linksDict.Select(x => new Link(x.Key, x.Value)).ToList();
-
-        string? originalLanguage =
-            attributes.TryGetPropertyValue("originalLanguage", out JsonNode? originalLanguageNode) switch
-            {
-                true => originalLanguageNode?.GetValue<string>(),
-                false => null
-            };
-        
-        MangaReleaseStatus releaseStatus = MangaReleaseStatus.Unreleased;
-        if (attributes.TryGetPropertyValue("status", out JsonNode? statusNode))
-        {
-            releaseStatus = statusNode?.GetValue<string>().ToLower() switch
-            {
-                "ongoing" => MangaReleaseStatus.Continuing,
-                "completed" => MangaReleaseStatus.Completed,
-                "hiatus" => MangaReleaseStatus.OnHiatus,
-                "cancelled" => MangaReleaseStatus.Cancelled,
-                _ => MangaReleaseStatus.Unreleased
-            };
-        }
-
-        uint year = attributes.TryGetPropertyValue("year", out JsonNode? yearNode) switch
-        {
-            true => yearNode?.GetValue<uint>()??0,
-            false => 0
-        };
-        
-        HashSet<string> tags = new(128);
-        if (attributes.TryGetPropertyValue("tags", out JsonNode? tagsNode))
-            foreach (JsonNode? tagNode in tagsNode!.AsArray())
-                tags.Add(tagNode!["attributes"]!["name"]!["en"]!.GetValue<string>());
-        List<MangaTag> mangaTags = tags.Select(t => new MangaTag(t)).ToList();
-        
-        if (!manga.TryGetPropertyValue("relationships", out JsonNode? relationshipsNode))
-        {
-            Log.Info("relationships was null");
+            JArray? errors = jObject["errors"] as JArray;
+            Log.Error($"Request failed: {string.Join(',', errors?.Select(e => e.Value<string>("title")) ?? [])}");
             return null;
         }
         
-        JsonNode? coverNode = relationshipsNode!.AsArray()
-            .FirstOrDefault(rel => rel!["type"]!.GetValue<string>().Equals("cover_art"));
-        if (coverNode is null)
+        JObject? data = jObject["data"] as JObject;
+        if (data is null)
         {
-            Log.Info("coverNode was null");
+            Log.Error("Data was null");
             return null;
         }
-        string fileName = coverNode["attributes"]!["fileName"]!.GetValue<string>();
-        string coverUrl = $"https://uploads.mangadex.org/covers/{publicationId}/{fileName}";
-        
-        List<string> authorNames = new();
-        JsonNode?[] authorNodes = relationshipsNode.AsArray()
-            .Where(rel => rel!["type"]!.GetValue<string>().Equals("author") || rel!["type"]!.GetValue<string>().Equals("artist")).ToArray();
-        foreach (JsonNode? authorNode in authorNodes)
-        {
-            string authorName = authorNode!["attributes"]!["name"]!.GetValue<string>();
-            if(!authorNames.Contains(authorName))
-               authorNames.Add(authorName);
-        }
-        List<Author> authors = authorNames.Select(a => new Author(a)).ToList();
 
-        Manga pub = new (publicationId, sortName, description, $"https://mangadex.org/title/{publicationId}", coverUrl, null, year,
-            originalLanguage, releaseStatus, -1,
-            this, 
-            authors, 
-            mangaTags, 
-            links,
-            altTitles);
-		
-        return (pub, authors, mangaTags, links, altTitles);
+        Manga manga = ParseMangaFromJToken(data);
+        return manga;
     }
 
-    public override Chapter[] GetChapters(Manga manga, string language="en")
+    public override Chapter[] GetChapters(Manga manga, string? language = null)
     {
-        const int limit = 100; //How many values we want returned at once
-        int offset = 0; //"Page"
-        int total = int.MaxValue; //How many total results are there, is updated on first request
-        List<Chapter> chapters = new();
-        //As long as we haven't requested all "Pages"
-        while (offset < total)
+        Log.Info($"Getting Chapters: {manga.IdOnConnectorSite}");
+        List<Chapter> chapters = new ();
+        
+        int offset = 0;
+        int total = int.MaxValue;
+        while(offset < total)
         {
-            //Request next "Page"
-            string requestUrl = $"https://api.mangadex.org/manga/{manga.IdOnConnectorSite}/feed?limit={limit}&offset={offset}&translatedLanguage%5B%5D={language}" +
-                                $"&contentRating%5B%5D=safe&contentRating%5B%5D=suggestive&contentRating%5B%5D=erotica&contentRating%5B%5D=pornographic";
-            RequestResult requestResult = downloadClient.MakeRequest(requestUrl, RequestType.MangaDexFeed);
-            if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
-            {
-                Log.Info($"{requestResult.statusCode}: {requestUrl}");
-                break;
-            }
-            JsonObject? result = JsonSerializer.Deserialize<JsonObject>(requestResult.result);
-            
-            offset += limit;
-            if (result is null)
-            {
-                Log.Info($"result was null: {requestUrl}");
-                break;
-            }
-            
-            total = result["total"]!.GetValue<int>();
-            JsonArray chaptersInResult = result["data"]!.AsArray();
-            //Loop through all Chapters in result and extract information from JSON
-            foreach (JsonNode? jsonNode in chaptersInResult)
-            {
-                JsonObject chapter = (JsonObject)jsonNode!;
-                JsonObject attributes = chapter["attributes"]!.AsObject();
-                
-                string chapterId = chapter["id"]!.GetValue<string>();
-                string url = $"https://mangadex.org/chapter/{chapterId}";
-                
-                string? title = attributes.ContainsKey("title") && attributes["title"] is not null
-                    ? attributes["title"]!.GetValue<string>()
-                    : null;
-                
-                int? volume = attributes.ContainsKey("volume") && attributes["volume"] is not null
-                    ? int.Parse(attributes["volume"]!.GetValue<string>())
-                    : null;
-                
-                string? chapterNumStr = attributes.ContainsKey("chapter") && attributes["chapter"] is not null
-                    ? attributes["chapter"]!.GetValue<string>()
-                    : null;
-                
-                string chapterNumber = new(chapterNumStr);
-                
-                
-                if (attributes.ContainsKey("pages") && attributes["pages"] is not null &&
-                    attributes["pages"]!.GetValue<int>() < 1)
-                {
-                    Log.Info($"No pages: {chapterId}");
-                    continue;
-                }
-                
-                try
-                {
-                    Chapter newChapter = new(manga, url, chapterNumber, volume, title);
-                    if(!chapters.Contains(newChapter))
-                        chapters.Add(newChapter);
-                }
-                catch (Exception e)
-                {
-                    Log.Debug(e);
-                }
-            }
-        }
+            string requestUrl =
+                $"https://api.mangadex.org/manga/{manga.IdOnConnectorSite}/feed?limit={Limit}&offset={offset}&" +
+                $"translatedLanguage%5B%5D={language}&" +
+                $"contentRating%5B%5D=safe&contentRating%5B%5D=suggestive&contentRating%5B%5D=erotica&includeFutureUpdates=0&includes%5B%5D=";
+            offset += Limit;
 
-        //Return Chapters ordered by Chapter-Number
+            RequestResult result = downloadClient.MakeRequest(requestUrl, RequestType.MangaDexFeed);
+            if ((int)result.statusCode < 200 || (int)result.statusCode >= 300)
+            {
+                Log.Error("Request failed");
+                return [];
+            }
+
+            using StreamReader sr = new (result.result);
+            JObject jObject = JObject.Parse(sr.ReadToEnd());
+
+            if (jObject.Value<string>("result") != "ok")
+            {
+                JArray? errors = jObject["errors"] as JArray;
+                Log.Error($"Request failed: {string.Join(',', errors?.Select(e => e.Value<string>("title")) ?? [])}");
+                return [];
+            }
+
+            total = jObject.Value<int>("total");
+            
+            JArray? data = jObject.Value<JArray>("data");
+            if (data is null)
+            {
+                Log.Error("Data was null");
+                return [];
+            }
+            
+            chapters.AddRange(data.Select(d => ParseChapterFromJToken(manga, d)));
+        }
+        
+        Log.Info($"Request for chapters for {manga.Name} yielded {chapters.Count} results.");
         return chapters.ToArray();
     }
 
+    private static readonly Regex GetChapterIdFromUrl = new(@"https?:\/\/mangadex\.org\/chapter\/([a-z0-9-]+)\/?.*");
     internal override string[] GetChapterImageUrls(Chapter chapter)
-    {//Request URLs for Chapter-Images
-        Match m = Regex.Match(chapter.Url, @"https?:\/\/mangadex.org\/chapter\/([0-9\-a-z]+)");
-        if (!m.Success)
+    {
+        Log.Info($"Getting Chapter Image-Urls: {chapter.Url}");
+        if (!UrlMatchesConnector(chapter.Url))
         {
-            Log.Error($"Could not parse Chapter ID: {chapter.Url}");
+            Log.Debug($"Url is not for Connector. {chapter.Url}");
             return [];
         }
 
-        string url = $"https://api.mangadex.org/at-home/server/{m.Groups[1].Value}?forcePort443=false";
-        RequestResult requestResult =
-            downloadClient.MakeRequest(url, RequestType.MangaDexImage);
-        if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
+        Match match = GetChapterIdFromUrl.Match(chapter.Url);
+        if (!match.Success || !match.Groups[1].Success)
         {
-            Log.Info($"{requestResult.statusCode}: {url}");
+            Log.Debug($"Url is not for Connector (Could not retrieve id). {chapter.Url}");
             return [];
         }
-        JsonObject? result = JsonSerializer.Deserialize<JsonObject>(requestResult.result);
-        if (result is null)
+        
+        string id = match.Groups[1].Value;
+        string requestUrl = $"https://api.mangadex.org/at-home/server/{id}";
+        
+        RequestResult result = downloadClient.MakeRequest(requestUrl, RequestType.Default);
+        if ((int)result.statusCode < 200 || (int)result.statusCode >= 300)
         {
-            Log.Info($"Result was null: {url}");
+            Log.Error("Request failed");
             return [];
         }
-        string baseUrl = result["baseUrl"]!.GetValue<string>();
-        string hash = result["chapter"]!["hash"]!.GetValue<string>();
-        JsonArray imageFileNames = result["chapter"]!["data"]!.AsArray();
-        //Loop through all imageNames and construct urls (imageUrl)
-        List<string> imageUrls = new();
-        foreach (JsonNode? image in imageFileNames)
-            imageUrls.Add($"{baseUrl}/data/{hash}/{image!.GetValue<string>()}");
-        return imageUrls.ToArray();
+
+        using StreamReader sr = new (result.result);
+        JObject jObject = JObject.Parse(sr.ReadToEnd());
+        
+        if (jObject.Value<string>("result") != "ok")
+        {
+            JArray? errors = jObject["errors"] as JArray;
+            Log.Error($"Request failed: {string.Join(',', errors?.Select(e => e.Value<string>("title")) ?? [])}");
+            return [];
+        }
+        
+        string? baseUrl = jObject.Value<string>("baseUrl");
+        JToken? chapterToken = jObject["chapter"];
+        string? hash = chapterToken?.Value<string>("hash");
+        JArray? data = chapterToken?["data"] as JArray;
+
+        if (baseUrl is null || hash is null || data is null)
+        {
+            Log.Error("Data was null");
+            return [];
+        }
+
+        IEnumerable<string> urls = data.Select(t => $"{baseUrl}/data/{hash}/{t.Value<string>()}");
+        
+        return urls.ToArray();
+    }
+
+    private Manga ParseMangaFromJToken(JToken jToken)
+    {
+        string? id = jToken.Value<string>("id");
+        
+        JObject? attributes = jToken["attributes"] as JObject;
+        string? name = attributes?["title"]?.Value<string>("en");
+        string? description = attributes?["description"]?.Value<string>("en");
+        string? status = attributes?["status"]?.Value<string>();
+        uint? year = attributes?["year"]?.Value<uint>();
+        string? originalLanguage = attributes?["originalLanguage"]?.Value<string>();
+        JArray? altTitlesJArray = attributes?["altTitles"] as JArray;
+        JArray? tagsJArray = attributes?["tags"] as JArray;
+        
+        JArray? relationships = jToken["relationships"] as JArray;
+        string? coverFileName =
+            relationships?.FirstOrDefault(r => r["type"]?.Value<string>() == "cover_art")?["attributes"]?.Value<string>("fileName");
+
+        if (id is null || attributes is null || name is null || description is null || status is null ||
+            altTitlesJArray is null || tagsJArray is null || relationships is null || coverFileName is null)
+            throw new Exception("jToken was not in expected format");
+        
+        List<Link> links = attributes["links"]?
+            .ToObject<Dictionary<string,string>>()?
+            .Select(kv =>
+            {
+                //https://api.mangadex.org/docs/3-enumerations/#manga-links-data
+                string url = kv.Key switch
+                {
+                    "al" => $"https://anilist.co/manga/{kv.Value}",
+                    "ap" => $"https://www.anime-planet.com/manga/{kv.Value}",
+                    "bw" => $"https://bookwalker.jp/{kv.Value}",
+                    "mu" => $"https://www.mangaupdates.com/series.html?id={kv.Value}",
+                    "nu" => $"https://www.novelupdates.com/series/{kv.Value}",
+                    "mal" => $"https://myanimelist.net/manga/{kv.Value}",
+                    _ => kv.Value
+                };
+                string key = kv.Key switch
+                {
+                    "al" => "AniList",
+                    "ap" => "Anime Planet",
+                    "bw" => "BookWalker",
+                    "mu" => "Manga Updates",
+                    "nu" => "Novel Updates",
+                    "kt" => "Kitsu.io",
+                    "amz" => "Amazon",
+                    "ebj" => "eBookJapan",
+                    "mal" => "MyAnimeList",
+                    "cdj" => "CDJapan",
+                    _ => kv.Key
+                };
+                return new Link(key, url);
+            }).ToList()!;
+
+        List<MangaAltTitle> altTitles = altTitlesJArray
+            .Select(t =>
+            {
+                JObject? j = t as JObject;
+                JProperty? p = j?.Properties().First();
+                if (p is null)
+                    return null;
+                return new MangaAltTitle(p.Name, p.Value.ToString());
+            }).Where(x => x is not null).ToList()!;
+        
+        List<MangaTag> tags = tagsJArray
+            .Where(t => t.Value<string>("type") == "tag")
+            .Select(t => t["attributes"]?["name"]?.Value<string>("en"))
+            .Select(str => str is not null ? new MangaTag(str) : null)
+            .Where(x => x is not null).ToList()!;
+        
+        List<Author> authors = relationships
+            .Where(r => r["type"]?.Value<string>() == "author")
+            .Select(t => t["attributes"]?.Value<string>("name"))
+            .Select(str => str is not null ? new Author(str) : null)
+            .Where(x => x is not null).ToList()!;
+            
+        
+        MangaReleaseStatus releaseStatus = status switch
+        {
+            "completed" => MangaReleaseStatus.Completed,
+            "ongoing" => MangaReleaseStatus.Continuing,
+            "cancelled" => MangaReleaseStatus.Cancelled,
+            "hiatus" => MangaReleaseStatus.OnHiatus,
+            _ => MangaReleaseStatus.Unreleased
+        };
+        string websiteUrl = $"https://mangadex.org/title/{id}";
+        string coverUrl = $"https://uploads.mangadex.org/covers/{id}/{coverFileName}";
+
+        return new Manga(id, name, description, websiteUrl, coverUrl, releaseStatus, this,
+            authors, tags, links,altTitles,
+            null, 0f, year, originalLanguage);
+    }
+
+    private Chapter ParseChapterFromJToken(Manga parentManga, JToken jToken)
+    {
+        string? id = jToken.Value<string>("id");
+        JToken? attributes = jToken["attributes"];
+        string? chapter = attributes?.Value<string>("chapter");
+        string? volumeStr = attributes?.Value<string>("volume");
+        int? volume = null;
+        string? title = attributes?.Value<string>("title");
+        
+        if(id is null || chapter is null)
+            throw new Exception("jToken was not in expected format");
+        if(volumeStr is not null)
+            volume = int.Parse(volumeStr);
+        
+        string url = $"https://mangadex.org/chapter/{id}";
+        return new Chapter(parentManga, url, chapter, volume, title);
     }
 }
\ No newline at end of file
diff --git a/API/Schema/MangaConnectors/MangaHere.cs b/API/Schema/MangaConnectors/MangaHere.cs
deleted file mode 100644
index e9197c9..0000000
--- a/API/Schema/MangaConnectors/MangaHere.cs
+++ /dev/null
@@ -1,183 +0,0 @@
-using System.Text.RegularExpressions;
-using API.MangaDownloadClients;
-using HtmlAgilityPack;
-
-namespace API.Schema.MangaConnectors;
-
-public class MangaHere : MangaConnector
-{
-    public MangaHere() : base("MangaHere", ["en"], ["www.mangahere.cc"], "http://www.mangahere.cc/favicon.ico")
-    {
-        this.downloadClient = new ChromiumDownloadClient();
-    }
-
-    public override (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)[] GetManga(string publicationTitle = "")
-    {
-        string sanitizedTitle = string.Join('+', Regex.Matches(publicationTitle, "[A-z]*").Where(str => str.Length > 0)).ToLower();
-        string requestUrl = $"https://www.mangahere.cc/search?title={sanitizedTitle}";
-        RequestResult requestResult =
-            downloadClient.MakeRequest(requestUrl, RequestType.Default);
-        if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300 || requestResult.htmlDocument is null)
-            return [];
-        
-        (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)[] publications = ParsePublicationsFromHtml(requestResult.htmlDocument);
-        return publications;
-    }
-
-    private (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)[] ParsePublicationsFromHtml(HtmlDocument document)
-    {
-        if (document.DocumentNode.SelectNodes("//div[contains(concat(' ',normalize-space(@class),' '),' container ')]").Any(node => node.ChildNodes.Any(cNode => cNode.HasClass("search-keywords"))))
-            return [];
-        
-        List<string> urls = document.DocumentNode
-            .SelectNodes("//a[contains(@href, '/manga/') and not(contains(@href, '.html'))]")
-            .Select(thumb => $"https://www.mangahere.cc{thumb.GetAttributeValue("href", "")}").Distinct().ToList();
-
-        HashSet<(Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)> ret = new();
-        foreach (string url in urls)
-        {
-            (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)? manga = GetMangaFromUrl(url);
-            if (manga is { } x)
-                ret.Add(x);
-        }
-
-        return ret.ToArray();
-    }
-
-    public override (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)? GetMangaFromId(string publicationId)
-    {
-        return GetMangaFromUrl($"https://www.mangahere.cc/manga/{publicationId}");
-    }
-
-    public override (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)? GetMangaFromUrl(string url)
-    {
-        RequestResult requestResult =
-            downloadClient.MakeRequest(url, RequestType.MangaInfo);
-        if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300 || requestResult.htmlDocument is null)
-            return null;
-        
-        Regex idRex = new (@"https:\/\/www\.mangahere\.[a-z]{0,63}\/manga\/([0-9A-z\-]+).*");
-        string id = idRex.Match(url).Groups[1].Value;
-        return ParseSinglePublicationFromHtml(requestResult.htmlDocument, id, url);
-    }
-
-    private (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?) ParseSinglePublicationFromHtml(HtmlDocument document, string publicationId, string websiteUrl)
-    {
-        string originalLanguage = "", status = "";
-        Dictionary<string, string> altTitles = new(), links = new();
-        MangaReleaseStatus releaseStatus = MangaReleaseStatus.Unreleased;
-
-        //We dont get posters, because same origin bs HtmlNode posterNode = document.DocumentNode.SelectSingleNode("//img[contains(concat(' ',normalize-space(@class),' '),' detail-info-cover-img ')]");
-        string coverUrl = "http://static.mangahere.cc/v20230914/mangahere/images/nopicture.jpg";
-
-        HtmlNode titleNode = document.DocumentNode.SelectSingleNode("//span[contains(concat(' ',normalize-space(@class),' '),' detail-info-right-title-font ')]");
-        string sortName = titleNode.InnerText;
-        
-        List<string> authorNames = document.DocumentNode
-            .SelectNodes("//p[contains(concat(' ',normalize-space(@class),' '),' detail-info-right-say ')]/a")
-            .Select(node => node.InnerText)
-            .ToList();
-        List<Author> authors = authorNames.Select(n => new Author(n)).ToList();
-
-        HashSet<string> tags = document.DocumentNode
-            .SelectNodes("//p[contains(concat(' ',normalize-space(@class),' '),' detail-info-right-tag-list ')]/a")
-            .Select(node => node.InnerText)
-            .ToHashSet();
-        List<MangaTag> mangaTags = tags.Select(n => new MangaTag(n)).ToList();
-
-        status = document.DocumentNode.SelectSingleNode("//span[contains(concat(' ',normalize-space(@class),' '),' detail-info-right-title-tip ')]").InnerText;
-        switch (status.ToLower())
-        {
-            case "cancelled": releaseStatus = MangaReleaseStatus.Cancelled; break;
-            case "hiatus": releaseStatus = MangaReleaseStatus.OnHiatus; break;
-            case "discontinued": releaseStatus = MangaReleaseStatus.Cancelled; break;
-            case "complete": releaseStatus = MangaReleaseStatus.Completed; break;
-            case "ongoing": releaseStatus = MangaReleaseStatus.Continuing; break;
-        }
-
-        HtmlNode descriptionNode = document.DocumentNode
-            .SelectSingleNode("//p[contains(concat(' ',normalize-space(@class),' '),' fullcontent ')]");
-        string description = descriptionNode.InnerText;
-
-        Manga manga = new (publicationId, sortName, description, websiteUrl, coverUrl, null, 0,
-            originalLanguage, releaseStatus, -1,
-            this, 
-            authors, 
-            mangaTags, 
-            [],
-            []);
-		
-        return (manga, authors, mangaTags, [], []);
-    }
-
-    public override Chapter[] GetChapters(Manga manga, string language="en")
-    {
-        string requestUrl = $"https://www.mangahere.cc/manga/{manga.MangaId}";
-        RequestResult requestResult =
-            downloadClient.MakeRequest(requestUrl, RequestType.Default);
-        if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300 || requestResult.htmlDocument is null)
-            return Array.Empty<Chapter>();
-        
-        List<string> urls = requestResult.htmlDocument.DocumentNode.SelectNodes("//div[@id='list-1']/ul//li//a[contains(@href, '/manga/')]")
-            .Select(node => node.GetAttributeValue("href", "")).ToList();
-        Regex chapterRex = new(@".*\/manga\/[a-zA-Z0-9\-\._\~\!\$\&\'\(\)\*\+\,\;\=\:\@]+\/v([0-9(TBD)]+)\/c([0-9\.]+)\/.*");
-        
-        List<Chapter> chapters = new();
-        foreach (string url in urls)
-        {
-            Match rexMatch = chapterRex.Match(url);
-
-            int? volumeNumber = rexMatch.Groups[1].Value == "TBD" ? null : int.Parse(rexMatch.Groups[1].Value);
-            string chapterNumber = new(rexMatch.Groups[2].Value);
-            string fullUrl = $"https://www.mangahere.cc{url}";
-                
-            try
-            {
-                chapters.Add(new Chapter(manga, fullUrl, chapterNumber, volumeNumber, null));
-            }
-            catch (Exception e)
-            {
-            }
-        }
-        //Return Chapters ordered by Chapter-Number
-        return chapters.Order().ToArray();
-    }
-
-    internal override string[] GetChapterImageUrls(Chapter chapter)
-    {
-        List<string> imageUrls = new();
-
-        int downloaded = 1;
-        int images = 1;
-        string url = string.Join('/', chapter.Url.Split('/')[..^1]);
-        do
-        {
-            RequestResult requestResult =
-                downloadClient.MakeRequest($"{url}/{downloaded}.html", RequestType.Default);
-            if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300 || requestResult.htmlDocument is null)
-            {
-                return [];
-            }
-            
-            imageUrls.AddRange(ParseImageUrlsFromHtml(requestResult.htmlDocument));
-
-            images = requestResult.htmlDocument.DocumentNode
-                .SelectNodes("//a[contains(@href, '/manga/')]")
-                .MaxBy(node => node.GetAttributeValue("data-page", 0))!.GetAttributeValue("data-page", 0);
-        } while (downloaded++ <= images);
-        
-        return imageUrls.ToArray();
-    }
-
-    private string[] ParseImageUrlsFromHtml(HtmlDocument document)
-    {
-        return document.DocumentNode
-            .SelectNodes("//img[contains(concat(' ',normalize-space(@class),' '),' reader-main-img ')]")
-            .Select(node =>
-            {
-                string url = node.GetAttributeValue("src", "");
-                return url.StartsWith("//") ? $"https:{url}" : url;
-            })
-            .ToArray();
-    }
-}
\ No newline at end of file
diff --git a/API/Schema/MangaConnectors/MangaKatana.cs b/API/Schema/MangaConnectors/MangaKatana.cs
deleted file mode 100644
index bc7823a..0000000
--- a/API/Schema/MangaConnectors/MangaKatana.cs
+++ /dev/null
@@ -1,233 +0,0 @@
-using System.Text.RegularExpressions;
-using API.MangaDownloadClients;
-using HtmlAgilityPack;
-
-namespace API.Schema.MangaConnectors;
-
-public class MangaKatana : MangaConnector
-{
-	public MangaKatana() : base("MangaKatana", ["en"], ["mangakatana.com"], "https://mangakatana.com/static/img/fav.png")
-	{
-		this.downloadClient = new HttpDownloadClient();
-	}
-
-	public override (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)[] GetManga(string publicationTitle = "")
-	{
-		string sanitizedTitle = string.Join("%20", Regex.Matches(publicationTitle, "[A-z]*").Where(m => m.Value.Length > 0)).ToLower();
-		string requestUrl = $"https://mangakatana.com/?search={sanitizedTitle}&search_by=book_name";
-		RequestResult requestResult =
-			downloadClient.MakeRequest(requestUrl, RequestType.Default);
-		if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
-			return [];
-
-		// ReSharper disable once MergeIntoPattern
-		// If a single result is found, the user will be redirected to the results directly instead of a result page
-		if(requestResult.hasBeenRedirected
-		    && requestResult.redirectedToUrl is not null
-			&& requestResult.redirectedToUrl.Contains("mangakatana.com/manga"))
-		{
-			return new [] { ParseSinglePublicationFromHtml(requestResult.result, requestResult.redirectedToUrl.Split('/')[^1], requestResult.redirectedToUrl) };
-		}
-
-		(Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)[] publications = ParsePublicationsFromHtml(requestResult.result);
-		return publications;
-	}
-
-	public override (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)? GetMangaFromId(string publicationId)
-	{
-		return GetMangaFromUrl($"https://mangakatana.com/manga/{publicationId}");
-	}
-
-	public override (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)? GetMangaFromUrl(string url)
-	{
-		RequestResult requestResult =
-			downloadClient.MakeRequest(url, RequestType.MangaInfo);
-		if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
-			return null;
-		return ParseSinglePublicationFromHtml(requestResult.result, url.Split('/')[^1], url);
-	}
-
-	private (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)[] ParsePublicationsFromHtml(Stream html)
-	{
-		StreamReader reader = new(html);
-		string htmlString = reader.ReadToEnd();
-		HtmlDocument document = new();
-		document.LoadHtml(htmlString);
-		IEnumerable<HtmlNode> searchResults = document.DocumentNode.SelectNodes("//*[@id='book_list']/div");
-		if (searchResults is null || !searchResults.Any())
-			return [];
-		List<string> urls = new();
-		foreach (HtmlNode mangaResult in searchResults)
-		{
-			urls.Add(mangaResult.Descendants("a").First().GetAttributes()
-				.First(a => a.Name == "href").Value);
-		}
-
-		HashSet<(Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)> ret = new();
-		foreach (string url in urls)
-		{
-			(Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)? manga = GetMangaFromUrl(url);
-			if (manga is { } x)
-				ret.Add(x);
-		}
-
-		return ret.ToArray();
-	}
-
-	private (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?) ParseSinglePublicationFromHtml(Stream html, string publicationId, string websiteUrl)
-	{
-		StreamReader reader = new(html);
-		string htmlString = reader.ReadToEnd();
-		HtmlDocument document = new();
-		document.LoadHtml(htmlString);
-		Dictionary<string, string> altTitlesDict = new();
-		Dictionary<string, string>? links = null;
-		HashSet<string> tags = new();
-		string[] authorNames = [];
-		string originalLanguage = "";
-		MangaReleaseStatus releaseStatus = MangaReleaseStatus.Unreleased;
-
-		HtmlNode infoNode = document.DocumentNode.SelectSingleNode("//*[@id='single_book']");
-		string sortName = infoNode.Descendants("h1").First(n => n.HasClass("heading")).InnerText;
-		HtmlNode infoTable = infoNode.SelectSingleNode("//*[@id='single_book']/div[2]/div/ul");
-
-		foreach (HtmlNode row in infoTable.Descendants("li"))
-		{
-			string key = row.SelectNodes("div").First().InnerText.ToLower();
-			string value = row.SelectNodes("div").Last().InnerText;
-			string keySanitized = string.Concat(Regex.Matches(key, "[a-z]"));
-
-			switch (keySanitized)
-			{
-				case "altnames":
-					string[] alts = value.Split(" ; ");
-					for (int i = 0; i < alts.Length; i++)
-						altTitlesDict.Add(i.ToString(), alts[i]);
-					break;
-				case "authorsartists":
-					authorNames = value.Split(',');
-					break;
-				case "status":
-					switch (value.ToLower())
-					{
-						case "ongoing": releaseStatus = MangaReleaseStatus.Continuing; break;
-						case "completed": releaseStatus = MangaReleaseStatus.Completed; break;
-					}
-					break;
-				case "genres":
-					tags = row.SelectNodes("div").Last().Descendants("a").Select(a => a.InnerText).ToHashSet();
-					break;
-			}
-		}
-
-		string coverUrl = document.DocumentNode.SelectSingleNode("//*[@id='single_book']/div[1]/div").Descendants("img").First()
-			.GetAttributes().First(a => a.Name == "src").Value;
-
-		string description = document.DocumentNode.SelectSingleNode("//*[@id='single_book']/div[3]/p").InnerText;
-		while (description.StartsWith('\n'))
-			description = description.Substring(1);
-
-		uint year = (uint)DateTime.UtcNow.Year;
-		string yearString = infoTable.Descendants("div").First(d => d.HasClass("updateAt"))
-			.InnerText.Split('-')[^1];
-
-		if(yearString.Contains("ago") == false)
-		{
-			year = uint.Parse(yearString);
-		}
-		List<Author> authors = authorNames.Select(n => new Author(n)).ToList();
-		List<MangaTag> mangaTags = tags.Select(n => new MangaTag(n)).ToList();
-		List<MangaAltTitle> altTitles = altTitlesDict.Select(x => new MangaAltTitle(x.Key, x.Value)).ToList();
-
-		Manga manga = new (publicationId, sortName, description, websiteUrl, coverUrl, null, year,
-			originalLanguage, releaseStatus, -1,
-			this, 
-			authors, 
-			mangaTags, 
-			[],
-			altTitles);
-		
-		return (manga, authors, mangaTags, [], altTitles);
-	}
-
-	public override Chapter[] GetChapters(Manga manga, string language="en")
-	{
-		string requestUrl = $"https://mangakatana.com/manga/{manga.MangaId}";
-		// Leaving this in for verification if the page exists
-		RequestResult requestResult =
-			downloadClient.MakeRequest(requestUrl, RequestType.Default);
-		if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
-			return Array.Empty<Chapter>();
-
-		//Return Chapters ordered by Chapter-Number
-		List<Chapter> chapters = ParseChaptersFromHtml(manga, requestUrl);
-		return chapters.Order().ToArray();
-	}
-
-	private List<Chapter> ParseChaptersFromHtml(Manga manga, string mangaUrl)
-	{
-		// Using HtmlWeb will include the chapters since they are loaded with js 
-		HtmlWeb web = new();
-		HtmlDocument document = web.Load(mangaUrl);
-
-		List<Chapter> ret = new();
-
-		HtmlNode chapterList = document.DocumentNode.SelectSingleNode("//div[contains(@class, 'chapters')]/table/tbody");
-
-		Regex volumeRex = new(@"[0-9a-z\-\.]+\/[0-9a-z\-]*v([0-9\.]+)");
-		Regex chapterNumRex = new(@"[0-9a-z\-\.]+\/[0-9a-z\-]*c([0-9\.]+)");
-		Regex chapterNameRex = new(@"Chapter [0-9\.]+:? (.*)");
-		
-		foreach (HtmlNode chapterInfo in chapterList.Descendants("tr"))
-		{
-			string fullString = chapterInfo.Descendants("a").First().InnerText;
-			string url = chapterInfo.Descendants("a").First()
-				.GetAttributeValue("href", "");
-
-			int? volumeNumber = volumeRex.IsMatch(url) ? int.Parse(volumeRex.Match(url).Groups[1].Value) : null;
-			
-			string chapterNumber = new(chapterNumRex.Match(url).Groups[1].Value);
-			string chapterName = chapterNameRex.Match(fullString).Groups[1].Value;
-			try
-			{
-				ret.Add(new Chapter(manga, url, chapterNumber, volumeNumber, chapterName));
-			}
-			catch (Exception e)
-			{
-			}
-		}
-		
-		return ret;
-	}
-
-	internal override string[] GetChapterImageUrls(Chapter chapter)
-	{
-		string requestUrl = chapter.Url;
-		// Leaving this in to check if the page exists
-		RequestResult requestResult =
-			downloadClient.MakeRequest(requestUrl, RequestType.Default);
-		if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300 || requestResult.htmlDocument is null)
-		{
-			return [];
-		}
-
-		string[] imageUrls = ParseImageUrlsFromHtml(requestResult.htmlDocument);
-		return imageUrls;
-	}
-
-	private string[] ParseImageUrlsFromHtml(HtmlDocument document)
-	{
-		// Images are loaded dynamically, but the urls are present in a piece of js code on the page
-		string js = document.DocumentNode.SelectSingleNode("//script[contains(., 'data-src')]").InnerText
-			.Replace("\r", "")
-			.Replace("\n", "")
-			.Replace("\t", "");
-		
-		// ReSharper disable once StringLiteralTypo
-		string regexPat = @"(var thzq=\[')(.*)(,];function)";
-		var group = Regex.Matches(js, regexPat).First().Groups[2].Value.Replace("'", "");
-		var urls = group.Split(',');
-		
-		return urls;
-	}
-}
\ No newline at end of file
diff --git a/API/Schema/MangaConnectors/Manganato.cs b/API/Schema/MangaConnectors/Manganato.cs
deleted file mode 100644
index de5bb0b..0000000
--- a/API/Schema/MangaConnectors/Manganato.cs
+++ /dev/null
@@ -1,219 +0,0 @@
-using System.Globalization;
-using System.Net;
-using System.Text.RegularExpressions;
-using API.MangaDownloadClients;
-using HtmlAgilityPack;
-
-namespace API.Schema.MangaConnectors;
-
-public class Manganato : MangaConnector
-{
-    public Manganato() : base("Manganato", ["en"],
-        ["natomanga.com", "manganato.gg", "mangakakalot.gg", "nelomanga.com"],
-        "https://www.manganato.gg/images/favicon-manganato.webp")
-    {
-        this.downloadClient = new HttpDownloadClient();
-    }
-
-    public override (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)[] GetManga(
-        string publicationTitle = "")
-    {
-        string sanitizedTitle = string.Join('_', Regex.Matches(publicationTitle, "[A-z]*").Where(str => str.Length > 0))
-            .ToLower();
-        string requestUrl = $"https://manganato.gg/search/story/{sanitizedTitle}";
-        RequestResult requestResult =
-            downloadClient.MakeRequest(requestUrl, RequestType.Default);
-        if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
-            return [];
-
-        if (requestResult.htmlDocument is null)
-            return [];
-        (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)[] publications =
-            ParsePublicationsFromHtml(requestResult.htmlDocument);
-        return publications;
-    }
-
-    private (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)[] ParsePublicationsFromHtml(
-        HtmlDocument document)
-    {
-        List<HtmlNode> searchResults =
-            document.DocumentNode.Descendants("div").Where(n => n.HasClass("story_item")).ToList();
-        List<string> urls = new();
-        foreach (HtmlNode mangaResult in searchResults)
-        {
-            try
-            {
-                urls.Add(mangaResult.Descendants("h3").First(n => n.HasClass("story_name"))
-                    .Descendants("a").First().GetAttributeValue("href", ""));
-            }
-            catch
-            {
-                //failed to get a url, send it to the void
-            }
-        }
-
-        List<(Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)> ret = new();
-        foreach (string url in urls)
-        {
-            (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)? manga = GetMangaFromUrl(url);
-            if (manga is { } m)
-                ret.Add(m);
-        }
-
-        return ret.ToArray();
-    }
-
-    public override (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)? GetMangaFromId(
-        string publicationId)
-    {
-        return GetMangaFromUrl($"https://chapmanganato.com/{publicationId}");
-    }
-
-    public override (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)?
-        GetMangaFromUrl(string url)
-    {
-        RequestResult requestResult =
-            downloadClient.MakeRequest(url, RequestType.MangaInfo);
-        if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
-            return null;
-
-        if (requestResult.htmlDocument is null)
-            return null;
-        return ParseSinglePublicationFromHtml(requestResult.htmlDocument, url.Split('/')[^1], url);
-    }
-
-    private (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?) ParseSinglePublicationFromHtml(
-        HtmlDocument document, string publicationId, string websiteUrl)
-    {
-        Dictionary<string, string> altTitles = new();
-        List<MangaTag> tags = new();
-        List<Author> authors = new();
-        MangaReleaseStatus releaseStatus = MangaReleaseStatus.Unreleased;
-
-        HtmlNode infoNode = document.DocumentNode.Descendants("ul").First(d => d.HasClass("manga-info-text"));
-
-        string sortName = infoNode.Descendants("h1").First().InnerText;
-
-        foreach (HtmlNode li in infoNode.Descendants("li"))
-        {
-            string text = li.InnerText.Trim().ToLower();
-
-            if (text.StartsWith("author(s) :"))
-            {
-                authors = li.Descendants("a").Select(a => a.InnerText.Trim()).Select(a => new Author(a)).ToList();
-            }
-            else if (text.StartsWith("status :"))
-            {
-                string status = text.Replace("status :", "").Trim().ToLower();
-                if (string.IsNullOrWhiteSpace(status))
-                    releaseStatus = MangaReleaseStatus.Continuing;
-                else if (status == "ongoing")
-                    releaseStatus = MangaReleaseStatus.Continuing;
-                else
-                    releaseStatus = Enum.Parse<MangaReleaseStatus>(status, true);
-            }
-            else if (li.HasClass("genres"))
-            {
-                tags = li.Descendants("a").Select(a => new MangaTag(a.InnerText.Trim())).ToList();
-            }
-        }
-
-        string posterUrl = document.DocumentNode.Descendants("div").First(s => s.HasClass("manga-info-pic"))
-            .Descendants("img").First()
-            .GetAttributes().First(a => a.Name == "src").Value;
-
-        string description = document.DocumentNode.SelectSingleNode("//div[@id='contentBox']")
-            .InnerText.Replace("Description :", "");
-        while (description.StartsWith('\n'))
-            description = description.Substring(1);
-
-        string pattern = "MMM-dd-yyyy HH:mm";
-
-        HtmlNode? oldestChapter = document.DocumentNode
-            .SelectNodes("//div[contains(concat(' ',normalize-space(@class),' '),' row ')]/span[@title]").MaxBy(
-                node => DateTime.ParseExact(node.GetAttributeValue("title", "Dec-31-2400 23:59"), pattern,
-                    CultureInfo.InvariantCulture).Millisecond);
-
-
-        uint year = Convert.ToUInt32(DateTime.ParseExact(
-            oldestChapter?.GetAttributeValue("title", "Dec 31 2400, 23:59") ?? "Dec 31 2400, 23:59", pattern,
-            CultureInfo.InvariantCulture).Year);
-
-        Manga manga = new(publicationId, sortName, description, websiteUrl, posterUrl, null, year, null, releaseStatus,
-            -1, this, authors, tags, [], []);
-        return (manga, authors, tags, [], []);
-    }
-
-    public override Chapter[] GetChapters(Manga manga, string language = "en")
-    {
-        string requestUrl = manga.WebsiteUrl;
-        RequestResult requestResult =
-            downloadClient.MakeRequest(requestUrl, RequestType.Default);
-        if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
-            return Array.Empty<Chapter>();
-
-        //Return Chapters ordered by Chapter-Number
-        if (requestResult.htmlDocument is null)
-            return Array.Empty<Chapter>();
-        List<Chapter> chapters = ParseChaptersFromHtml(manga, requestResult.htmlDocument);
-        return chapters.Order().ToArray();
-    }
-
-    internal override string[] GetChapterImageUrls(Chapter chapter)
-    {
-        string requestUrl = chapter.Url;
-        RequestResult requestResult =
-            downloadClient.MakeRequest(requestUrl, RequestType.Default);
-        if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300 ||
-            requestResult.htmlDocument is null)
-            return [];
-
-        string[] imageUrls = ParseImageUrlsFromHtml(requestResult.htmlDocument);
-
-        return imageUrls;
-    }
-
-    private List<Chapter> ParseChaptersFromHtml(Manga manga, HtmlDocument document)
-    {
-        List<Chapter> ret = new();
-
-        HtmlNode chapterList = document.DocumentNode.Descendants("div").First(l => l.HasClass("chapter-list"));
-
-        Regex volRex = new(@"Vol\.([0-9]+).*");
-        Regex chapterRex = new(@"https:\/\/chapmanganato.[A-z]+\/manga-[A-z0-9]+\/chapter-([0-9\.]+)");
-        Regex nameRex = new(@"Chapter ([0-9]+(\.[0-9]+)*){1}:? (.*)");
-
-        foreach (HtmlNode chapterInfo in chapterList.Descendants("div").Where(x => x.HasClass("row")))
-        {
-            string url = chapterInfo.Descendants("a").First().GetAttributeValue("href", "");
-            var name = chapterInfo.Descendants("a").First().InnerText.Trim();
-            string chapterName = nameRex.Match(name).Groups[3].Value;
-            string chapterNumber = Regex.Match(name, @"Chapter ([0-9]+(\.[0-9]+)*)").Groups[1].Value;
-            string? volumeNumber = Regex.Match(chapterName, @"Vol\.([0-9]+)").Groups[1].Value;
-            if (string.IsNullOrWhiteSpace(volumeNumber))
-                volumeNumber = "0";
-            try
-            {
-                ret.Add(new Chapter(manga, url, chapterNumber, int.Parse(volumeNumber), chapterName));
-            }
-            catch (Exception e)
-            {
-            }
-        }
-
-        ret.Reverse();
-        return ret;
-    }
-
-    private string[] ParseImageUrlsFromHtml(HtmlDocument document)
-    {
-        List<string> ret = new();
-
-        HtmlNode imageContainer =
-            document.DocumentNode.Descendants("div").First(i => i.HasClass("container-chapter-reader"));
-        foreach (HtmlNode imageNode in imageContainer.Descendants("img"))
-            ret.Add(imageNode.GetAttributeValue("src", ""));
-
-        return ret.ToArray();
-    }
-}
\ No newline at end of file
diff --git a/API/Schema/MangaConnectors/Mangaworld.cs b/API/Schema/MangaConnectors/Mangaworld.cs
deleted file mode 100644
index fd90e8c..0000000
--- a/API/Schema/MangaConnectors/Mangaworld.cs
+++ /dev/null
@@ -1,223 +0,0 @@
-using System.Text.RegularExpressions;
-using API.MangaDownloadClients;
-using HtmlAgilityPack;
-
-namespace API.Schema.MangaConnectors;
-
-public class Mangaworld : MangaConnector
-{
-    public Mangaworld() : base("Mangaworld", ["it"], ["www.mangaworld.ac", "www.mangaworld.nz"], "https://www.mangaworld.nz/public/assets/seo/android-icon-192x192.png")
-    {
-        this.downloadClient = new ChromiumDownloadClient();
-    }
-
-    public override (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)[] GetManga(string publicationTitle = "")
-    {
-        string sanitizedTitle = string.Join(' ', Regex.Matches(publicationTitle, "[A-z]*").Where(str => str.Length > 0)).ToLower();
-        string requestUrl = $"https://www.mangaworld.ac/archive?keyword={sanitizedTitle}";
-        RequestResult requestResult =
-            downloadClient.MakeRequest(requestUrl, RequestType.Default);
-        if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
-            return [];
-
-        if (requestResult.htmlDocument is null)
-            return [];
-        (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)[] publications = ParsePublicationsFromHtml(requestResult.htmlDocument);
-        return publications;
-    }
-
-    private (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)[] ParsePublicationsFromHtml(HtmlDocument document)
-    {
-        if (!document.DocumentNode.SelectSingleNode("//div[@class='comics-grid']").ChildNodes
-                .Any(node => node.HasClass("entry")))
-            return [];
-        
-        List<string> urls = document.DocumentNode
-            .SelectNodes(
-                "//div[@class='comics-grid']//div[@class='entry']//a[contains(concat(' ',normalize-space(@class),' '),'thumb')]")
-            .Select(thumb => thumb.GetAttributeValue("href", "")).ToList();
-
-        List<(Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)> ret = new();
-        foreach (string url in urls)
-        {
-            (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)? manga = GetMangaFromUrl(url);
-            if (manga is { } x)
-                ret.Add(x);
-        }
-
-        return ret.ToArray();
-    }
-
-    public override (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)? GetMangaFromId(string publicationId)
-    {
-        return GetMangaFromUrl($"https://www.mangaworld.ac/manga/{publicationId}");
-    }
-
-    public override (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)? GetMangaFromUrl(string url)
-    {
-        RequestResult requestResult =
-            downloadClient.MakeRequest(url, RequestType.MangaInfo);
-        if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
-            return null;
-        
-        if (requestResult.htmlDocument is null)
-            return null;
-        
-        Regex idRex = new (@"https:\/\/www\.mangaworld\.[a-z]{0,63}\/manga\/([0-9]+\/[0-9A-z\-]+).*");
-        string id = idRex.Match(url).Groups[1].Value;
-        return ParseSinglePublicationFromHtml(requestResult.htmlDocument, id, url);
-    }
-
-    private (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?) ParseSinglePublicationFromHtml(HtmlDocument document, string publicationId, string websiteUrl)
-    {
-        Dictionary<string, string> altTitlesDict = new();
-        string originalLanguage = "";
-        MangaReleaseStatus releaseStatus = MangaReleaseStatus.Unreleased;
-
-        HtmlNode infoNode = document.DocumentNode.Descendants("div").First(d => d.HasClass("info"));
-
-        string sortName = infoNode.Descendants("h1").First().InnerText;
-
-        HtmlNode metadata = infoNode.Descendants().First(d => d.HasClass("meta-data"));
-
-        HtmlNode altTitlesNode = metadata.SelectSingleNode("//span[text()='Titoli alternativi: ' or text()='Titolo alternativo: ']/..").ChildNodes[1];
-        string[] alts = altTitlesNode.InnerText.Split(", ");
-        for(int i = 0; i < alts.Length; i++)
-            altTitlesDict.Add(i.ToString(), alts[i]);
-        List<MangaAltTitle> altTitles = altTitlesDict.Select(a => new MangaAltTitle(a.Key, a.Value)).ToList();
-
-        HtmlNode genresNode =
-            metadata.SelectSingleNode("//span[text()='Generi: ' or text()='Genero: ']/..");
-        HashSet<string> tags = genresNode.SelectNodes("a").Select(node => node.InnerText).ToHashSet();
-        List<MangaTag> mangaTags = tags.Select(t => new MangaTag(t)).ToList();
-        
-        HtmlNode authorsNode =
-            metadata.SelectSingleNode("//span[text()='Autore: ' or text()='Autori: ']/..");
-        string[] authorNames = authorsNode.SelectNodes("a").Select(node => node.InnerText).ToArray();
-        List<Author> authors = authorNames.Select(n => new Author(n)).ToList();
-
-        string status = metadata.SelectSingleNode("//span[text()='Stato: ']/..").SelectNodes("a").First().InnerText;
-        // ReSharper disable 5 times StringLiteralTypo
-        switch (status.ToLower())
-        {
-            case "cancellato": releaseStatus = MangaReleaseStatus.Cancelled; break;
-            case "in pausa": releaseStatus = MangaReleaseStatus.OnHiatus; break;
-            case "droppato": releaseStatus = MangaReleaseStatus.Cancelled; break;
-            case "finito": releaseStatus = MangaReleaseStatus.Completed; break;
-            case "in corso": releaseStatus = MangaReleaseStatus.Continuing; break;
-        }
-
-        string coverUrl = document.DocumentNode.SelectSingleNode("//img[@class='rounded']").GetAttributeValue("src", "");
-
-        string description = document.DocumentNode.SelectSingleNode("//div[@id='noidungm']").InnerText;
-        
-        string yearString = metadata.SelectSingleNode("//span[text()='Anno di uscita: ']/..").SelectNodes("a").First().InnerText;
-        uint year = uint.Parse(yearString);
-        
-        Manga manga = new (publicationId, sortName, description, websiteUrl, coverUrl, null, year,
-            originalLanguage, releaseStatus, -1,
-            this, 
-            authors, 
-            mangaTags, 
-            [],
-            altTitles);
-		
-        return (manga, authors, mangaTags, [], altTitles);
-    }
-
-    public override Chapter[] GetChapters(Manga manga, string language="en")
-    {
-        string requestUrl = $"https://www.mangaworld.ac/manga/{manga.MangaId}";
-        RequestResult requestResult =
-            downloadClient.MakeRequest(requestUrl, RequestType.Default);
-        if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300 || requestResult.htmlDocument is null)
-            return [];
-        
-        List<Chapter> chapters = ParseChaptersFromHtml(manga, requestResult.htmlDocument);
-        return chapters.Order().ToArray();
-    }
-
-    private List<Chapter> ParseChaptersFromHtml(Manga manga, HtmlDocument document)
-    {
-        List<Chapter> ret = new();
-
-        HtmlNode chaptersWrapper =
-            document.DocumentNode.SelectSingleNode(
-                "//div[contains(concat(' ',normalize-space(@class),' '),'chapters-wrapper')]");
-
-        Regex volumeRex = new(@"[Vv]olume ([0-9]+).*");
-        Regex chapterRex = new(@"[Cc]apitolo ([0-9]+(?:\.[0-9]+)?).*");
-        Regex idRex = new(@".*\/read\/([a-z0-9]+)(?:[?\/].*)?");
-        if (chaptersWrapper.Descendants("div").Any(descendant => descendant.HasClass("volume-element")))
-        {
-            foreach (HtmlNode volNode in document.DocumentNode.SelectNodes("//div[contains(concat(' ',normalize-space(@class),' '),'volume-element')]"))
-            {
-                string volumeStr = volumeRex.Match(volNode.SelectNodes("div").First(node => node.HasClass("volume")).SelectSingleNode("p").InnerText).Groups[1].Value;
-                int volume = int.Parse(volumeStr);
-                foreach (HtmlNode chNode in volNode.SelectNodes("div").First(node => node.HasClass("volume-chapters")).SelectNodes("div"))
-                {
-
-                    string numberStr = chapterRex.Match(chNode.SelectSingleNode("a").SelectSingleNode("span").InnerText).Groups[1].Value;
-                    
-                    string chapterNumber = new(numberStr);
-                    string url = chNode.SelectSingleNode("a").GetAttributeValue("href", "");
-                    string id = idRex.Match(chNode.SelectSingleNode("a").GetAttributeValue("href", "")).Groups[1].Value;
-                    try
-                    {
-                        ret.Add(new Chapter(manga, url, chapterNumber, volume, null));
-                    }
-                    catch (Exception e)
-                    {
-                    }
-                }
-            }
-        }
-        else
-        {
-            foreach (HtmlNode chNode in chaptersWrapper.SelectNodes("div").Where(node => node.HasClass("chapter")))
-            {
-                string numberStr = chapterRex.Match(chNode.SelectSingleNode("a").SelectSingleNode("span").InnerText).Groups[1].Value;
-                
-                string chapterNumber = new(numberStr);
-                string url = chNode.SelectSingleNode("a").GetAttributeValue("href", "");
-                string id = idRex.Match(chNode.SelectSingleNode("a").GetAttributeValue("href", "")).Groups[1].Value;
-                try
-                {
-                    ret.Add(new Chapter(manga, url, chapterNumber, null, null));
-                }
-                catch (Exception e)
-                {
-                }
-            }
-        }
-
-        ret.Reverse();
-        return ret;
-    }
-
-    internal override string[] GetChapterImageUrls(Chapter chapter)
-    {
-        string requestUrl = $"{chapter.Url}?style=list";
-        RequestResult requestResult =
-            downloadClient.MakeRequest(requestUrl, RequestType.Default);
-        if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300 || requestResult.htmlDocument is null)
-        {
-            return [];
-        }
-
-        string[] imageUrls = ParseImageUrlsFromHtml(requestResult.htmlDocument);
-        return imageUrls;
-    }
-
-    private string[] ParseImageUrlsFromHtml(HtmlDocument document)
-    {
-        List<string> ret = new();
-
-        HtmlNode imageContainer =
-            document.DocumentNode.SelectSingleNode("//div[@id='page']");
-        foreach(HtmlNode imageNode in imageContainer.Descendants("img"))
-            ret.Add(imageNode.GetAttributeValue("src", ""));
-
-        return ret.ToArray();
-    }
-}
\ No newline at end of file
diff --git a/API/Schema/MangaConnectors/ManhuaPlus.cs b/API/Schema/MangaConnectors/ManhuaPlus.cs
deleted file mode 100644
index b0a27ff..0000000
--- a/API/Schema/MangaConnectors/ManhuaPlus.cs
+++ /dev/null
@@ -1,179 +0,0 @@
-using System.Text.RegularExpressions;
-using API.MangaDownloadClients;
-using HtmlAgilityPack;
-
-namespace API.Schema.MangaConnectors;
-
-public class ManhuaPlus : MangaConnector
-{
-    public ManhuaPlus() : base("ManhuaPlus", ["en"], ["manhuaplus.org"], "https://manhuaplus.org/uploads/images/favicon.png")
-    {
-        this.downloadClient = new ChromiumDownloadClient();
-    }
-
-    public override (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)[] GetManga(string publicationTitle = "")
-    {
-        string sanitizedTitle = string.Join(' ', Regex.Matches(publicationTitle, "[A-z]*").Where(str => str.Length > 0)).ToLower();
-        string requestUrl = $"https://manhuaplus.org/search?keyword={sanitizedTitle}";
-        RequestResult requestResult =
-            downloadClient.MakeRequest(requestUrl, RequestType.Default);
-        if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300 || requestResult.htmlDocument is null)
-            return [];
-        
-        (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)[] publications = ParsePublicationsFromHtml(requestResult.htmlDocument);
-        return publications;
-    }
-    
-    private (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)[] ParsePublicationsFromHtml(HtmlDocument document)
-    {
-        if (document.DocumentNode.SelectSingleNode("//h1/../..").ChildNodes//I already want to not.
-                .Any(node => node.InnerText.Contains("No manga found")))
-            return [];
-
-        List<string> urls = document.DocumentNode
-            .SelectNodes("//h1/../..//a[contains(@href, 'https://manhuaplus.org/manga/') and contains(concat(' ',normalize-space(@class),' '),' clamp ') and not(contains(@href, '/chapter'))]")
-                .Select(mangaNode => mangaNode.GetAttributeValue("href", "")).ToList();
-
-        List<(Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)> ret = new();
-        foreach (string url in urls)
-        {
-            (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)? manga = GetMangaFromUrl(url);
-            if (manga is { } x)
-                ret.Add(x);
-        }
-
-        return ret.ToArray();
-    }
-
-    public override (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)? GetMangaFromId(string publicationId)
-    {
-        return GetMangaFromUrl($"https://manhuaplus.org/manga/{publicationId}");
-    }
-
-    public override (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)? GetMangaFromUrl(string url)
-    {
-        Regex publicationIdRex = new(@"https:\/\/manhuaplus.org\/manga\/(.*)(\/.*)*");
-        string publicationId = publicationIdRex.Match(url).Groups[1].Value;
-
-        RequestResult requestResult = this.downloadClient.MakeRequest(url, RequestType.MangaInfo);
-        if((int)requestResult.statusCode < 300 && (int)requestResult.statusCode >= 200 && requestResult.htmlDocument is not null && requestResult.redirectedToUrl != "https://manhuaplus.org/home") //When manga doesnt exists it redirects to home
-            return ParseSinglePublicationFromHtml(requestResult.htmlDocument, publicationId, url);
-        return null;
-    }
-
-    private (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?) ParseSinglePublicationFromHtml(HtmlDocument document, string publicationId, string websiteUrl)
-    {
-        string originalLanguage = "", status = "";
-        Dictionary<string, string> altTitles = new(), links = new();
-        HashSet<string> tags = new();
-        MangaReleaseStatus releaseStatus = MangaReleaseStatus.Unreleased;
-
-        HtmlNode posterNode = document.DocumentNode.SelectSingleNode("/html/body/main/div/div/div[2]/div[1]/figure/a/img");//BRUH
-        Regex posterRex = new(@".*(\/uploads/covers/[a-zA-Z0-9\-\._\~\!\$\&\'\(\)\*\+\,\;\=\:\@]+).*");
-        string coverUrl = $"https://manhuaplus.org/{posterRex.Match(posterNode.GetAttributeValue("src", "")).Groups[1].Value}";
-
-        HtmlNode titleNode = document.DocumentNode.SelectSingleNode("//h1");
-        string sortName = titleNode.InnerText.Replace("\n", "");
-        
-        List<string> authorNames = new();
-        try
-        {
-            HtmlNode[] authorsNodes = document.DocumentNode
-                .SelectNodes("//a[contains(@href, 'https://manhuaplus.org/authors/')]")
-                .ToArray();
-            foreach (HtmlNode authorNode in authorsNodes)
-                authorNames.Add(authorNode.InnerText);
-        }
-        catch (ArgumentNullException e)
-        {
-        }
-        List<Author> authors = authorNames.Select(a => new Author(a)).ToList();
-
-        try
-        {
-            HtmlNode[] genreNodes = document.DocumentNode
-                .SelectNodes("//a[contains(@href, 'https://manhuaplus.org/genres/')]").ToArray();
-            foreach (HtmlNode genreNode in genreNodes)
-                tags.Add(genreNode.InnerText.Replace("\n", ""));
-        }
-        catch (ArgumentNullException e)
-        {
-        }
-        List<MangaTag> mangaTags = tags.Select(t => new MangaTag(t)).ToList();
-
-        Regex yearRex = new(@"(?:[0-9]{1,2}\/){2}([0-9]{2,4}) [0-9]{1,2}:[0-9]{1,2}");
-        HtmlNode yearNode = document.DocumentNode.SelectSingleNode("//aside//i[contains(concat(' ',normalize-space(@class),' '),' fa-clock ')]/../span");
-        Match match = yearRex.Match(yearNode.InnerText);
-        uint year = match.Success && match.Groups[1].Success ? uint.Parse(match.Groups[1].Value) : 0;
-
-        status = document.DocumentNode.SelectSingleNode("//aside//i[contains(concat(' ',normalize-space(@class),' '),' fa-rss ')]/../span").InnerText.Replace("\n", "");
-        switch (status.ToLower())
-        {
-            case "cancelled": releaseStatus = MangaReleaseStatus.Cancelled; break;
-            case "hiatus": releaseStatus = MangaReleaseStatus.OnHiatus; break;
-            case "discontinued": releaseStatus = MangaReleaseStatus.Cancelled; break;
-            case "complete": releaseStatus = MangaReleaseStatus.Completed; break;
-            case "ongoing": releaseStatus = MangaReleaseStatus.Continuing; break;
-        }
-
-        HtmlNode descriptionNode = document.DocumentNode
-            .SelectSingleNode("//div[@id='syn-target']");
-        string description = descriptionNode.InnerText;
-
-        Manga manga = new (publicationId, sortName, description, websiteUrl, coverUrl, null, year,
-            originalLanguage, releaseStatus, -1,
-            this, 
-            authors, 
-            mangaTags, 
-            [],
-            []);
-		
-        return (manga, authors, mangaTags, [], []);
-    }
-
-    public override Chapter[] GetChapters(Manga manga, string language="en")
-    {
-        RequestResult result = downloadClient.MakeRequest($"https://manhuaplus.org/manga/{manga.MangaId}", RequestType.Default);
-        if ((int)result.statusCode < 200 || (int)result.statusCode >= 300 || result.htmlDocument is null)
-        {
-            return Array.Empty<Chapter>();
-        }
-        
-        HtmlNodeCollection chapterNodes = result.htmlDocument.DocumentNode.SelectNodes("//li[contains(concat(' ',normalize-space(@class),' '),' chapter ')]//a");
-        string[] urls = chapterNodes.Select(node => node.GetAttributeValue("href", "")).ToArray();
-        Regex urlRex = new (@".*\/chapter-([0-9\-]+).*");
-        
-        List<Chapter> chapters = new();
-        foreach (string url in urls)
-        {
-            Match rexMatch = urlRex.Match(url);
-
-           string chapterNumber = new(rexMatch.Groups[1].Value);
-            string fullUrl = url;
-            try
-            {
-                chapters.Add(new Chapter(manga, fullUrl, chapterNumber, null, null));
-            }
-            catch (Exception e)
-            {
-            }
-        }
-        //Return Chapters ordered by Chapter-Number
-        return chapters.Order().ToArray();
-    }
-
-    internal override string[] GetChapterImageUrls(Chapter chapter)
-    {
-        RequestResult requestResult = this.downloadClient.MakeRequest(chapter.Url, RequestType.Default);
-        if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300 || requestResult.htmlDocument is null)
-        {
-            return [];
-        }
-
-        HtmlDocument document = requestResult.htmlDocument;
-        
-        HtmlNode[] images = document.DocumentNode.SelectNodes("//a[contains(concat(' ',normalize-space(@class),' '),' readImg ')]/img").ToArray();
-        List<string> urls = images.Select(node => node.GetAttributeValue("src", "")).ToList();
-        return urls.ToArray();
-    }
-}
\ No newline at end of file
diff --git a/API/Schema/MangaConnectors/Webtoons.cs b/API/Schema/MangaConnectors/Webtoons.cs
deleted file mode 100644
index add4239..0000000
--- a/API/Schema/MangaConnectors/Webtoons.cs
+++ /dev/null
@@ -1,259 +0,0 @@
-using System.Net;
-using System.Text.RegularExpressions;
-using API.MangaDownloadClients;
-using HtmlAgilityPack;
-
-namespace API.Schema.MangaConnectors;
-
-public class Webtoons : MangaConnector
-{
-
-    public Webtoons() : base("Webtoons", ["en"], ["www.webtoons.com"], "https://webtoons-static.pstatic.net/image/favicon/favicon.ico")
-    {
-        this.downloadClient = new HttpDownloadClient();
-    }
-
-    // Done
-    public override (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)[] GetManga(string publicationTitle = "")
-    {
-        string sanitizedTitle = string.Join(' ', Regex.Matches(publicationTitle, "[A-z]*").Where(m => m.Value.Length > 0)).ToLower();
-        string requestUrl = $"https://www.webtoons.com/en/search?keyword={sanitizedTitle}&searchType=WEBTOON";
-        RequestResult requestResult =
-            downloadClient.MakeRequest(requestUrl, RequestType.Default);
-        if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300) {
-            return [];
-        }
-
-        if (requestResult.htmlDocument is null)
-        {
-            return [];
-        }
-
-        (Manga, List<Author>, List<MangaTag>, List<Link>, List<MangaAltTitle>)[] publications =
-            ParsePublicationsFromHtml(requestResult.htmlDocument);
-        return publications;
-    }
-
-    // Done
-    public override (Manga, List<Author>, List<MangaTag>, List<Link>, List<MangaAltTitle>)? GetMangaFromId(string publicationId)
-    {
-        PublicationManager pb = new PublicationManager(publicationId);
-        return GetMangaFromUrl($"https://www.webtoons.com/en/{pb.Category}/{pb.Title}/list?title_no={pb.Id}");
-    }
-
-    // Done
-    public override (Manga, List<Author>, List<MangaTag>, List<Link>, List<MangaAltTitle>)? GetMangaFromUrl(string url)
-    {
-        RequestResult requestResult = downloadClient.MakeRequest(url, RequestType.MangaInfo);
-        if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300) {
-            return null;
-        }
-        if (requestResult.htmlDocument is null)
-        {
-            return null;
-        }
-        Regex regex = new Regex(@".*webtoons\.com/en/(?<category>[^/]+)/(?<title>[^/]+)/list\?title_no=(?<id>\d+).*");
-        Match match = regex.Match(url);
-
-        if(match.Success) {
-            PublicationManager pm = new PublicationManager(match.Groups["title"].Value, match.Groups["category"].Value, match.Groups["id"].Value);
-            return ParseSinglePublicationFromHtml(requestResult.htmlDocument, pm.getPublicationId(), url);
-        }
-        return null;
-    }
-
-    // Done
-    private (Manga, List<Author>, List<MangaTag>, List<Link>, List<MangaAltTitle>)[] ParsePublicationsFromHtml(HtmlDocument document)
-    {
-        HtmlNode mangaList = document.DocumentNode.SelectSingleNode("//ul[contains(@class, 'card_lst')]");
-        if (!mangaList.ChildNodes.Any(node => node.Name == "li")) {
-            return [];
-        }
-
-        List<string> urls = document.DocumentNode
-                            .SelectNodes("//ul[contains(@class, 'card_lst')]/li/a")
-                            .Select(node => node.GetAttributeValue("href", "https://www.webtoons.com"))
-                            .ToList();
-
-        List<(Manga, List<Author>, List<MangaTag>, List<Link>, List<MangaAltTitle>)> ret = new();
-        foreach (string url in urls)
-        {
-            (Manga, List<Author>, List<MangaTag>, List<Link>, List<MangaAltTitle>)? manga = GetMangaFromUrl(url);
-            if(manga is { } m)
-                ret.Add(m);
-        }
-
-        return ret.ToArray();
-    }
-
-    private string capitalizeString(string str = "") {
-        if(str.Length == 0) return "";
-        if(str.Length == 1) return str.ToUpper();
-        return char.ToUpper(str[0]) + str.Substring(1).ToLower();
-    }
-
-    // Done
-    private (Manga, List<Author>, List<MangaTag>, List<Link>, List<MangaAltTitle>) ParseSinglePublicationFromHtml(HtmlDocument document, string publicationId, string websiteUrl)
-    {
-        HtmlNode infoNode1 = document.DocumentNode.SelectSingleNode("//*[@id='content']/div[2]/div[1]/div[1]");
-        HtmlNode infoNode2 = document.DocumentNode.SelectSingleNode("//*[@id='content']/div[2]/div[2]/div[2]");
-
-        string sortName = infoNode1.SelectSingleNode(".//h1[contains(@class, 'subj')]").InnerText;
-        string description = infoNode2.SelectSingleNode(".//p[contains(@class, 'summary')]")
-                            .InnerText.Trim();
-
-        HtmlNode posterNode = document.DocumentNode.SelectSingleNode("//div[contains(@class, 'detail_body') and contains(@class, 'banner')]");
-
-        Regex regex = new Regex(@"url\((?<url>.*?)\)");
-        Match match = regex.Match(posterNode.GetAttributeValue("style", ""));
-
-        string coverUrl = match.Groups["url"].Value;
-
-        string genre = infoNode1.SelectSingleNode(".//h2[contains(@class, 'genre')]")
-                            .InnerText.Trim();
-        List<MangaTag> mangaTags = [new MangaTag(genre)];
-
-        List<HtmlNode> authorsNodes = infoNode1.SelectSingleNode(".//div[contains(@class, 'author_area')]").Descendants("a").ToList();
-        List<Author> authors = authorsNodes.Select(node => new Author(node.InnerText.Trim())).ToList();
-
-        string originalLanguage = "";
-
-        uint year = 0;
-
-        string status1 = infoNode2.SelectSingleNode(".//p").InnerText;
-        string status2 = infoNode2.SelectSingleNode(".//p/span").InnerText;
-        MangaReleaseStatus releaseStatus = MangaReleaseStatus.Unreleased;
-        if(status2.Length == 0 || status1.ToLower() == "completed") {
-            releaseStatus = MangaReleaseStatus.Completed;
-        } else if(status2.ToLower() == "up") {
-            releaseStatus = MangaReleaseStatus.Continuing;
-        }
-
-        Manga manga = new (publicationId, sortName, description, websiteUrl, coverUrl, null, year,
-            originalLanguage, releaseStatus, -1,
-            this, 
-            authors, 
-            mangaTags, 
-            [],
-            []);
-		
-        return (manga, authors, mangaTags, [], []);
-    }
-
-    // Done
-    public override Chapter[] GetChapters(Manga manga, string language = "en")
-    {
-        PublicationManager pm = new(manga.MangaId);
-        string requestUrl = $"https://www.webtoons.com/en/{pm.Category}/{pm.Title}/list?title_no={pm.Id}";
-        // Leaving this in for verification if the page exists
-        RequestResult requestResult =
-            downloadClient.MakeRequest(requestUrl, RequestType.Default);
-        if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
-            return Array.Empty<Chapter>();
-
-        // Get number of pages
-        int pages = requestResult.htmlDocument.DocumentNode
-                            .SelectNodes("//div[contains(@class, 'paginate')]/a")
-                            .ToList()
-                            .Count;
-        List<Chapter> chapters = new List<Chapter>();
-        
-        for(int page = 1; page <= pages; page++) {
-            string pageRequestUrl = $"{requestUrl}&page={page}";
-            chapters.AddRange(ParseChaptersFromHtml(manga, pageRequestUrl));
-        }
-
-        return chapters.Order().ToArray();
-    }
-
-    // Done
-    private List<Chapter> ParseChaptersFromHtml(Manga manga, string mangaUrl)
-    {
-        RequestResult result = downloadClient.MakeRequest(mangaUrl, RequestType.Default);
-        if ((int)result.statusCode < 200 || (int)result.statusCode >= 300 || result.htmlDocument is null)
-        {
-            return new List<Chapter>();
-        }
-
-        List<Chapter> ret = new();
-
-        foreach (HtmlNode chapterInfo in result.htmlDocument.DocumentNode.SelectNodes("//ul/li[contains(@class, '_episodeItem')]"))
-        {
-            HtmlNode infoNode = chapterInfo.SelectSingleNode(".//a");
-            string url = infoNode.GetAttributeValue("href", "");
-
-            string id = chapterInfo.GetAttributeValue("id", "");
-            if(id == "") continue;
-            string chapterNumber = chapterInfo.GetAttributeValue("data-episode-no", "");
-            if(chapterNumber == "") continue;
-            string chapterName = infoNode.SelectSingleNode(".//span[contains(@class, 'subj')]/span").InnerText.Trim();
-            ret.Add(new Chapter(manga, url, chapterNumber, null, chapterName));
-        }
-
-        return ret;
-    }
-
-    internal override string[] GetChapterImageUrls(Chapter chapter)
-    {
-        string requestUrl = chapter.Url;
-        // Leaving this in to check if the page exists
-        RequestResult requestResult =
-            downloadClient.MakeRequest(requestUrl, RequestType.Default);
-        if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
-        {
-            return [];
-        }
-
-        string[] imageUrls = ParseImageUrlsFromHtml(requestUrl);
-        return imageUrls;
-    }
-
-    private string[] ParseImageUrlsFromHtml(string mangaUrl)
-    {
-        RequestResult requestResult =
-            downloadClient.MakeRequest(mangaUrl, RequestType.Default);
-        if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
-        {
-            return [];
-        }
-        if (requestResult.htmlDocument is null)
-        {
-            return [];
-        }
-
-        return requestResult.htmlDocument.DocumentNode
-        .SelectNodes("//*[@id='_imageList']/img")
-        .Select(node =>
-            node.GetAttributeValue("data-url", ""))
-        .ToArray();
-    }
-}
-
-internal class PublicationManager {
-    public PublicationManager(string title = "", string category = "", string id = "") {
-        this.Title = title;
-        this.Category = category;
-        this.Id = id;
-    }
-
-    public PublicationManager(string publicationId) {
-        string[] parts = publicationId.Split("|");
-        if(parts.Length == 3) {
-            this.Title = parts[0];
-            this.Category = parts[1];
-            this.Id = parts[2];
-        } else {
-            this.Title = "";
-            this.Category = "";
-            this.Id = "";
-        }
-    }
-
-    public string getPublicationId() {
-        return $"{this.Title}|{this.Category}|{this.Id}";
-    }
-
-    public string Title { get; set; }
-    public string Category { get; set; }
-    public string Id { get; set; }
-}
\ No newline at end of file
diff --git a/API/Schema/MangaConnectors/WeebCentral.cs b/API/Schema/MangaConnectors/WeebCentral.cs
deleted file mode 100644
index fd4f20a..0000000
--- a/API/Schema/MangaConnectors/WeebCentral.cs
+++ /dev/null
@@ -1,175 +0,0 @@
-using System.Net;
-using System.Text.RegularExpressions;
-using API.MangaDownloadClients;
-using HtmlAgilityPack;
-
-namespace API.Schema.MangaConnectors;
-
-public class Weebcentral : MangaConnector
-{
-    private readonly string[] _filterWords =
-        { "a", "the", "of", "as", "to", "no", "for", "on", "with", "be", "and", "in", "wa", "at", "be", "ni" };
-
-    public Weebcentral() : base("Weebcentral", ["en"], ["weebcentral.com"], "https://weebcentral.com/favicon.ico")
-    {
-        downloadClient = new ChromiumDownloadClient();
-    }
-
-    public override (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)[] GetManga(string publicationTitle = "")
-    {
-        const int limit = 32; //How many values we want returned at once
-        var offset = 0; //"Page"
-        var requestUrl =
-            $"https://{BaseUris[0]}/search/data?limit={limit}&offset={offset}&text={publicationTitle}&sort=Best+Match&order=Ascending&official=Any&display_mode=Minimal%20Display";
-        var requestResult =
-            downloadClient.MakeRequest(requestUrl, RequestType.Default);
-        if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300 ||
-            requestResult.htmlDocument == null)
-        {
-            return [];
-        }
-
-        var publications = ParsePublicationsFromHtml(requestResult.htmlDocument);
-        
-        return publications;
-    }
-
-    private (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)[] ParsePublicationsFromHtml(HtmlDocument document)
-    {
-        if (document.DocumentNode.SelectNodes("//article").Count < 1)
-            return [];
-
-        var urls = document.DocumentNode.SelectNodes("/html/body/article/a[contains(concat(' ',normalize-space(@class),' '),' link ')]")
-            .Select(elem => elem.GetAttributeValue("href", "")).ToList();
-
-        List<(Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)> ret = new();
-        foreach (var url in urls)
-        {
-            (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)? manga = GetMangaFromUrl(url);
-            if (manga is { })
-                ret.Add(((Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?))manga);
-        }
-
-        return ret.ToArray();
-    }
-
-    public override (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)? GetMangaFromUrl(string url)
-    {
-        Regex publicationIdRex = new(@"https:\/\/weebcentral\.com\/series\/(\w*)\/(.*)");
-        var publicationId = publicationIdRex.Match(url).Groups[1].Value;
-
-        var requestResult = downloadClient.MakeRequest(url, RequestType.MangaInfo);
-        if ((int)requestResult.statusCode < 300 && (int)requestResult.statusCode >= 200 &&
-            requestResult.htmlDocument is not null)
-            return ParseSinglePublicationFromHtml(requestResult.htmlDocument, publicationId, url);
-        return null;
-    }
-
-    private (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?) ParseSinglePublicationFromHtml(HtmlDocument document, string publicationId, string websiteUrl)
-    {
-        HtmlNode posterNode =
-            document.DocumentNode.SelectSingleNode("//section[@class='flex items-center justify-center']/picture/img");
-        string posterUrl = posterNode?.GetAttributeValue("src", "") ?? "";
-
-        HtmlNode titleNode = document.DocumentNode.SelectSingleNode("//section/h1");
-        string sortName = titleNode?.InnerText ?? "Undefined";
-
-        HtmlNode[] authorsNodes =
-            document.DocumentNode.SelectNodes("//ul/li[strong/text() = 'Author(s): ']/span").ToArray();
-        List<Author> authors = authorsNodes.Select(n => new Author(n.InnerText)).ToList();
-
-        HtmlNode[] genreNodes =
-            document.DocumentNode.SelectNodes("//ul/li[strong/text() = 'Tags(s): ']/span").ToArray();
-        List<MangaTag> tags = genreNodes.Select(n => new MangaTag(n.InnerText.EndsWith(',') ? n.InnerText.Substring(0,n.InnerText.Length-1) : n.InnerText)).ToList();
-
-        HtmlNode statusNode = document.DocumentNode.SelectSingleNode("//ul/li[strong/text() = 'Status: ']/a");
-        string statusText = statusNode?.InnerText ?? "";
-        MangaReleaseStatus releaseStatus = statusText.ToLower() switch
-        {
-            "cancelled" => MangaReleaseStatus.Cancelled,
-            "hiatus" => MangaReleaseStatus.OnHiatus,
-            "complete" => MangaReleaseStatus.Completed,
-            "ongoing" => MangaReleaseStatus.Continuing,
-            _ => MangaReleaseStatus.Unreleased
-        };
-
-        HtmlNode yearNode = document.DocumentNode.SelectSingleNode("//ul/li[strong/text() = 'Released: ']/span");
-        uint year = Convert.ToUInt32(yearNode?.InnerText ?? "0");
-
-        HtmlNode descriptionNode = document.DocumentNode.SelectSingleNode("//ul/li[strong/text() = 'Description']/p");
-        string description = descriptionNode?.InnerText ?? "Undefined";
-
-        HtmlNode[] altTitleNodes = document.DocumentNode
-            .SelectNodes("//ul/li[strong/text() = 'Associated Name(s)']/ul/li")?.ToArray() ?? [];
-        List<MangaAltTitle> altTitles = altTitleNodes.Select(n => new MangaAltTitle("", n.InnerText)).ToList();
-
-        Manga m = new(publicationId, sortName, description, websiteUrl, posterUrl, null, year, null, releaseStatus, -1,
-            this, authors, tags, [], altTitles);
-        return (m, authors, tags, [], altTitles);
-    }
-
-    public override (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)? GetMangaFromId(string publicationId)
-    {
-        return GetMangaFromUrl($"https://{BaseUris[0]}/series/{publicationId}");
-    }
-
-    public override Chapter[] GetChapters(Manga manga, string language = "en")
-    {
-                var requestUrl = $"https://{BaseUris[0]}/series/{manga.MangaConnectorId}/full-chapter-list";
-        var requestResult =
-            downloadClient.MakeRequest(requestUrl, RequestType.Default);
-        if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
-            return [];
-
-        //Return Chapters ordered by Chapter-Number
-        if (requestResult.htmlDocument is null)
-            return [];
-        var chapters = ParseChaptersFromHtml(manga, requestResult.htmlDocument);
-                return chapters.Order().ToArray();
-    }
-
-    internal override string[] GetChapterImageUrls(Chapter chapter)
-    {
-        var requestResult = downloadClient.MakeRequest(chapter.Url, RequestType.Default);
-        if (requestResult.htmlDocument is null)
-            return [];
-
-        var document = requestResult.htmlDocument;
-
-        var imageNodes =
-            document.DocumentNode.SelectNodes($"//section[@hx-get='{chapter.Url}/images']/img")?.ToArray() ?? [];
-        var urls = imageNodes.Select(imgNode => imgNode.GetAttributeValue("src", "")).ToArray();
-        
-        return urls;
-    }
-
-    private List<Chapter> ParseChaptersFromHtml(Manga manga, HtmlDocument document)
-    {
-        var chaptersWrapper = document.DocumentNode.SelectSingleNode("/html/body");
-
-        Regex chapterRex = new(@"(\d+(?:\.\d+)*)");
-        Regex idRex = new(@"https:\/\/weebcentral\.com\/chapters\/(\w*)");
-
-        var ret = chaptersWrapper.Descendants("a").Select(elem =>
-        {
-            var url = elem.GetAttributeValue("href", "") ?? "Undefined";
-
-            if (!url.StartsWith("https://") && !url.StartsWith("http://"))
-                return new Chapter(manga, "", "");
-
-            var idMatch = idRex.Match(url);
-            var id = idMatch.Success ? idMatch.Groups[1].Value : null;
-
-            var chapterNode = elem.SelectSingleNode("span[@class='grow flex items-center gap-2']/span")?.InnerText ??
-                              "Undefined";
-
-            var chapterNumberMatch = chapterRex.Match(chapterNode);
-            var chapterNumber = chapterNumberMatch.Success ? chapterNumberMatch.Groups[1].Value : "-1";
-
-            return new Chapter(manga, url, chapterNumber);
-        }).Where(elem => elem.ChapterNumber != String.Empty && elem.Url != string.Empty).ToList();
-
-        ret.Reverse();
-        return ret;
-    }
-}
\ No newline at end of file
diff --git a/API/Schema/PgsqlContext.cs b/API/Schema/PgsqlContext.cs
index 3d6aaf0..b3995bb 100644
--- a/API/Schema/PgsqlContext.cs
+++ b/API/Schema/PgsqlContext.cs
@@ -14,100 +14,159 @@ public class PgsqlContext(DbContextOptions<PgsqlContext> options) : DbContext(op
     public DbSet<LocalLibrary> LocalLibraries { get; set; }
     public DbSet<Chapter> Chapters { get; set; }
     public DbSet<Author> Authors { get; set; }
-    public DbSet<Link> Links { get; set; }
     public DbSet<MangaTag> Tags { get; set; }
-    public DbSet<MangaAltTitle> AltTitles { get; set; }
     public DbSet<LibraryConnector> LibraryConnectors { get; set; }
     public DbSet<NotificationConnector> NotificationConnectors { get; set; }
     public DbSet<Notification> Notifications { get; set; }
     
     protected override void OnModelCreating(ModelBuilder modelBuilder)
     {
-        modelBuilder.Entity<MangaConnector>()
-            .HasDiscriminator(c => c.Name)
-            .HasValue<Global>("Global")
-            .HasValue<AsuraToon>("AsuraToon")
-            .HasValue<Bato>("Bato")
-            .HasValue<MangaHere>("MangaHere")
-            .HasValue<MangaKatana>("MangaKatana")
-            .HasValue<Mangaworld>("Mangaworld")
-            .HasValue<ManhuaPlus>("ManhuaPlus")
-            .HasValue<Weebcentral>("Weebcentral")
-            .HasValue<Manganato>("Manganato")
-            .HasValue<MangaDex>("MangaDex");
-        modelBuilder.Entity<LibraryConnector>()
-            .HasDiscriminator<LibraryType>(l => l.LibraryType)
-            .HasValue<Komga>(LibraryType.Komga)
-            .HasValue<Kavita>(LibraryType.Kavita);
-
+        //Job Types
         modelBuilder.Entity<Job>()
-            .HasDiscriminator<JobType>(j => j.JobType)
+            .HasDiscriminator(j => j.JobType)
             .HasValue<MoveFileOrFolderJob>(JobType.MoveFileOrFolderJob)
+            .HasValue<MoveMangaLibraryJob>(JobType.MoveMangaLibraryJob)
             .HasValue<DownloadAvailableChaptersJob>(JobType.DownloadAvailableChaptersJob)
             .HasValue<DownloadSingleChapterJob>(JobType.DownloadSingleChapterJob)
             .HasValue<DownloadMangaCoverJob>(JobType.DownloadMangaCoverJob)
-            .HasValue<UpdateMetadataJob>(JobType.UpdateMetaDataJob)
             .HasValue<RetrieveChaptersJob>(JobType.RetrieveChaptersJob)
             .HasValue<UpdateFilesDownloadedJob>(JobType.UpdateFilesDownloadedJob);
+        
+        //Job specification
+        modelBuilder.Entity<DownloadAvailableChaptersJob>()
+            .HasOne<Manga>(j => j.Manga)
+            .WithMany()
+            .HasForeignKey(j => j.MangaId)
+            .IsRequired()
+            .OnDelete(DeleteBehavior.Cascade);
+        modelBuilder.Entity<DownloadAvailableChaptersJob>()
+            .Navigation(j => j.Manga)
+            .AutoInclude();
+        modelBuilder.Entity<DownloadMangaCoverJob>()
+            .HasOne<Manga>(j => j.Manga)
+            .WithMany()
+            .HasForeignKey(j => j.MangaId)
+            .IsRequired()
+            .OnDelete(DeleteBehavior.Cascade);
+        modelBuilder.Entity<DownloadMangaCoverJob>()
+            .Navigation(j => j.Manga)
+            .AutoInclude();
+        modelBuilder.Entity<DownloadSingleChapterJob>()
+            .HasOne<Chapter>(j => j.Chapter)
+            .WithMany()
+            .HasForeignKey(j => j.ChapterId)
+            .IsRequired()
+            .OnDelete(DeleteBehavior.Cascade);
+        modelBuilder.Entity<DownloadSingleChapterJob>()
+            .Navigation(j => j.Chapter)
+            .AutoInclude();
+        modelBuilder.Entity<MoveMangaLibraryJob>()
+            .HasOne<Manga>(j => j.Manga)
+            .WithMany()
+            .HasForeignKey(j => j.MangaId)
+            .IsRequired()
+            .OnDelete(DeleteBehavior.Cascade);
+        modelBuilder.Entity<MoveMangaLibraryJob>()
+            .Navigation(j => j.Manga)
+            .AutoInclude();
+        modelBuilder.Entity<MoveMangaLibraryJob>()
+            .HasOne<LocalLibrary>(j => j.ToLibrary)
+            .WithMany()
+            .HasForeignKey(j => j.ToLibraryId)
+            .IsRequired()
+            .OnDelete(DeleteBehavior.Cascade);
+        modelBuilder.Entity<MoveMangaLibraryJob>()
+            .Navigation(j => j.ToLibrary)
+            .AutoInclude();
+        modelBuilder.Entity<RetrieveChaptersJob>()
+            .HasOne<Manga>(j => j.Manga)
+            .WithMany()
+            .HasForeignKey(j => j.MangaId)
+            .IsRequired()
+            .OnDelete(DeleteBehavior.Cascade);
+        modelBuilder.Entity<RetrieveChaptersJob>()
+            .Navigation(j => j.Manga)
+            .AutoInclude();
+        
+        //Job has possible ParentJob
         modelBuilder.Entity<Job>()
             .HasMany<Job>()
-            .WithOne(j => j.ParentJob)
+            .WithOne(childJob => childJob.ParentJob)
+            .HasForeignKey(childjob => childjob.ParentJobId)
+            .IsRequired(false)
             .OnDelete(DeleteBehavior.Cascade);
+        //Job might be dependent on other Jobs
         modelBuilder.Entity<Job>()
-            .HasMany<Job>(j => j.DependsOnJobs)
+            .HasMany<Job>(root => root.DependsOnJobs)
             .WithMany();
-        modelBuilder.Entity<UpdateMetadataJob>()
-            .Navigation(umj => umj.Manga)
-            .AutoInclude();
-
-        modelBuilder.Entity<Manga>()
-            .HasOne<MangaConnector>(m => m.MangaConnector)
-            .WithMany()
-            .HasForeignKey(m => m.MangaConnectorId)
+        modelBuilder.Entity<Job>()
+            .Navigation(root => root.DependsOnJobs)
+            .AutoInclude(false);
+        
+        //MangaConnector Types
+        modelBuilder.Entity<MangaConnector>()
+            .HasDiscriminator(c => c.Name)
+            .HasValue<Global>("Global")
+            .HasValue<MangaDex>("MangaDex");
+        //MangaConnector is responsible for many Manga
+        modelBuilder.Entity<MangaConnector>()
+            .HasMany<Manga>()
+            .WithOne(m => m.MangaConnector)
+            .HasForeignKey(m => m.MangaConnectorName)
+            .IsRequired()
             .OnDelete(DeleteBehavior.Cascade);
         modelBuilder.Entity<Manga>()
             .Navigation(m => m.MangaConnector)
             .AutoInclude();
+
+        //Manga has many Chapters
         modelBuilder.Entity<Manga>()
-            .HasOne<LocalLibrary>(m => m.Library)
-            .WithMany()
-            .OnDelete(DeleteBehavior.Restrict);
-        modelBuilder.Entity<Manga>()
-            .Navigation(m => m.Library)
-            .AutoInclude();
-        modelBuilder.Entity<Manga>()
-            .HasMany<Author>(m => m.Authors)
-            .WithMany();
-        modelBuilder.Entity<Manga>()
-            .Navigation(m => m.Authors)
-            .AutoInclude();
-        modelBuilder.Entity<Manga>()
-            .HasMany<MangaTag>(m => m.MangaTags)
-            .WithMany();
-        modelBuilder.Entity<Manga>()
-            .Navigation(m => m.MangaTags)
-            .AutoInclude();
-        modelBuilder.Entity<Manga>()
-            .HasMany<Link>(m => m.Links)
-            .WithOne()
-            .OnDelete(DeleteBehavior.Cascade);
-        modelBuilder.Entity<Manga>()
-            .Navigation(m => m.Links)
-            .AutoInclude();
-        modelBuilder.Entity<Manga>()
-            .HasMany<MangaAltTitle>(m => m.AltTitles)
-            .WithOne()
-            .OnDelete(DeleteBehavior.Cascade);
-        modelBuilder.Entity<Manga>()
-            .Navigation(m => m.AltTitles)
-            .AutoInclude();
-        modelBuilder.Entity<Chapter>()
-            .HasOne<Manga>(c => c.ParentManga)
-            .WithMany()
+            .HasMany<Chapter>(m => m.Chapters)
+            .WithOne(c => c.ParentManga)
             .HasForeignKey(c => c.ParentMangaId)
+            .IsRequired()
             .OnDelete(DeleteBehavior.Cascade);
         modelBuilder.Entity<Chapter>()
             .Navigation(c => c.ParentManga)
             .AutoInclude();
+        //Manga owns MangaAltTitles
+        modelBuilder.Entity<Manga>()
+            .OwnsMany<MangaAltTitle>(m => m.AltTitles)
+            .WithOwner();
+        //Manga owns Links
+        modelBuilder.Entity<Manga>()
+            .OwnsMany<Link>(m => m.Links)
+            .WithOwner();
+        //Manga has many Tags associated with many Manga
+        modelBuilder.Entity<Manga>()
+            .HasMany<MangaTag>(m => m.MangaTags)
+            .WithMany()
+            .UsingEntity("MangaTagToManga",
+                l=> l.HasOne(typeof(MangaTag)).WithMany().HasForeignKey("MangaTagIds").HasPrincipalKey(nameof(MangaTag.Tag)),
+                r => r.HasOne(typeof(Manga)).WithMany().HasForeignKey("MangaIds").HasPrincipalKey(nameof(Manga.MangaId)),
+                j => j.HasKey("MangaTagIds", "MangaIds")
+            );
+        //Manga has many Authors associated with many Manga
+        modelBuilder.Entity<Manga>()
+            .HasMany<Author>(m => m.Authors)
+            .WithMany()
+            .UsingEntity("AuthorToManga",
+                l=> l.HasOne(typeof(Author)).WithMany().HasForeignKey("AuthorIds").HasPrincipalKey(nameof(Author.AuthorId)),
+                r => r.HasOne(typeof(Manga)).WithMany().HasForeignKey("MangaIds").HasPrincipalKey(nameof(Manga.MangaId)),
+                j => j.HasKey("AuthorIds", "MangaIds")
+            );
+        
+        //LocalLibrary has many Mangas
+        modelBuilder.Entity<LocalLibrary>()
+            .HasMany<Manga>()
+            .WithOne(m => m.Library)
+            .HasForeignKey(m => m.LibraryId)
+            .OnDelete(DeleteBehavior.SetNull);
+        
+        //LibraryConnector Types
+        modelBuilder.Entity<LibraryConnector>()
+            .HasDiscriminator(l => l.LibraryType)
+            .HasValue<Komga>(LibraryType.Komga)
+            .HasValue<Kavita>(LibraryType.Kavita);
     }
 }
\ No newline at end of file
diff --git a/API/Tranga.cs b/API/Tranga.cs
index f1b7e56..976ed9e 100644
--- a/API/Tranga.cs
+++ b/API/Tranga.cs
@@ -119,50 +119,35 @@ public static class Tranga
         Log.Info("JobStarter Thread running.");
         while (true)
         {
-            List<Job> completedJobs = context.Jobs.Where(j => j.state >= JobState.Completed).ToList();
-            Log.Debug($"Completed jobs: {completedJobs.Count}");
-            foreach (Job job in completedJobs)
-                if (job.RecurrenceMs <= 0)
-                    context.Jobs.Remove(job);
+            //Update finished Jobs to new states
+            List<Job> completedJobs = context.Jobs.Where(j => j.state == JobState.Completed).ToList();
+            foreach (Job completedJob in completedJobs)
+                if (completedJob.RecurrenceMs <= 0)
+                    context.Jobs.Remove(completedJob);
                 else
                 {
-                    if (job.state >= JobState.Failed)
-                        job.Enabled = false;
-                    else
-                        job.state = JobState.Waiting;
-                    job.LastExecution = DateTime.UtcNow;
+                    completedJob.state = JobState.CompletedWaiting;
+                    completedJob.LastExecution = DateTime.UtcNow;
                 }
-
-            List<Job> runJobs = context.Jobs.Where(j => j.state <= JobState.Running && j.Enabled == true).ToList()
-                .Where(j => j.NextExecution < DateTime.UtcNow).ToList();
-            IEnumerable<Job> orderedJobs = OrderJobs(runJobs, context).ToList();
-            Log.Debug($"Jobs Due: {runJobs.Count} Running: {RunningJobs.Count} Ordered: {orderedJobs.Count()}");
-            foreach (Job job in orderedJobs)
+            List<Job> failedJobs = context.Jobs.Where(j => j.state == JobState.Failed).ToList();
+            foreach (Job failedJob in failedJobs)
             {
-                // If the job is already running, skip it
-                if (RunningJobs.Values.Any(j => j.JobId == job.JobId)) continue;
+                failedJob.Enabled = false;
+                failedJob.LastExecution = DateTime.UtcNow;
+            }
 
-                //If a Job for that connector is already running, skip it
-                if (job is DownloadAvailableChaptersJob dncj)
-                {
-                    if (RunningJobs.Values.Any(j =>
-                            j is DownloadAvailableChaptersJob rdncj && 
-                            context.Mangas.Find(rdncj.MangaId)?.MangaConnector == context.Mangas.Find(dncj.MangaId)?.MangaConnector))
-                    {
-                        continue;
-                    }
-                }
-                else if (job is DownloadSingleChapterJob dscj)
-                {
-                    if (RunningJobs.Values.Any(j =>
-                            j is DownloadSingleChapterJob rdscj &&
-                            context.Chapters.Find(rdscj.ChapterId)?.ParentManga?.MangaConnector ==
-                            context.Chapters.Find(dscj.ChapterId)?.ParentManga?.MangaConnector))
-                    {
-                        continue;
-                    }
-                }
+            //Retrieve waiting and due Jobs
+            List<Job> waitingJobs = context.Jobs.Where(j =>
+                j.Enabled && (j.state == JobState.FirstExecution || j.state == JobState.CompletedWaiting)).ToList();
+            List<Job> runningJobs = context.Jobs.Where(j => j.state == JobState.Running).ToList();
+            List<Job> dueJobs = waitingJobs.Where(j => j.NextExecution < DateTime.UtcNow).ToList();
 
+            List<MangaConnector> busyConnectors = GetBusyConnectors(runningJobs);
+            List<Job> startJobs = FilterJobPreconditions(dueJobs, busyConnectors);
+            
+            //Start Jobs that are allowed to run (preconditions match)
+            foreach (Job job in startJobs)
+            {
                 Thread t = new(() =>
                 {
                     job.Run(serviceProvider);
@@ -170,6 +155,10 @@ public static class Tranga
                 RunningJobs.Add(t, job);
                 t.Start();
             }
+            Log.Debug($"Jobs Completed: {completedJobs.Count} Failed: {failedJobs.Count} Running: {runningJobs.Count}\n" +
+                      $"Waiting: {waitingJobs.Count}\n" +
+                      $"\tof which Due: {dueJobs.Count}\n" +
+                      $"\t\tof which Started: {startJobs.Count}");
 
             (Thread, Job)[] removeFromThreadsList = RunningJobs.Where(t => !t.Key.IsAlive)
                 .Select(t => (t.Key, t.Value)).ToArray();
@@ -191,84 +180,39 @@ public static class Tranga
         }
     }
 
-    private static IEnumerable<Job> OrderJobs(List<Job> jobs, PgsqlContext context)
+    private static List<MangaConnector> GetBusyConnectors(List<Job> runningJobs)
     {
-        Dictionary<JobType, List<Job>> jobsByType = new();
-        foreach (Job job in jobs)
-            if(!jobsByType.TryAdd(job.JobType, [job]))
-                jobsByType[job.JobType].Add(job);
+        HashSet<MangaConnector> busyConnectors = new();
+        foreach (Job runningJob in runningJobs)
+        {
+            if(GetJobConnector(runningJob) is { } mangaConnector)
+                busyConnectors.Add(mangaConnector);
+        }
+        return busyConnectors.ToList();
+    }
 
-        IEnumerable<Job> ret = new List<Job>();
-        if(jobsByType.ContainsKey(JobType.MoveMangaLibraryJob))
-            ret = ret.Concat(jobsByType[JobType.MoveMangaLibraryJob]);
-        if(jobsByType.ContainsKey(JobType.MoveFileOrFolderJob))
-            ret = ret.Concat(jobsByType[JobType.MoveFileOrFolderJob]);
-        if(jobsByType.ContainsKey(JobType.DownloadMangaCoverJob)) 
-            ret = ret.Concat(jobsByType[JobType.DownloadMangaCoverJob]);
-        if(jobsByType.ContainsKey(JobType.UpdateFilesDownloadedJob))
-            ret = ret.Concat(jobsByType[JobType.UpdateFilesDownloadedJob]);
+    private static List<Job> FilterJobPreconditions(List<Job> dueJobs, List<MangaConnector> busyConnectors) =>
+        dueJobs
+            .Where(j => j.DependenciesFulfilled)
+            .Where(j =>
+            {
+                //Filter jobs with busy connectors
+                if (GetJobConnector(j) is { } mangaConnector)
+                    return busyConnectors.Contains(mangaConnector) == false;
+                return true;
+            })
+            .ToList();
 
-        Dictionary<MangaConnector, List<Job>> metadataJobsByConnector = new();
-        if (jobsByType.ContainsKey(JobType.DownloadAvailableChaptersJob))
-        {
-            foreach (DownloadAvailableChaptersJob job in jobsByType[JobType.DownloadAvailableChaptersJob])
-            {
-                Manga? manga = context.Mangas.Find(job.MangaId);
-                if(manga is null)
-                    continue;
-                MangaConnector connector = manga.MangaConnector ?? context.MangaConnectors.Find(manga.MangaConnectorId)!;
-                if(!metadataJobsByConnector.TryAdd(connector, [job]))
-                    metadataJobsByConnector[connector].Add(job);
-            }
-        }
-        if (jobsByType.ContainsKey(JobType.UpdateMetaDataJob))
-        {
-            foreach (UpdateMetadataJob job in jobsByType[JobType.UpdateMetaDataJob])
-            {
-                Manga manga = job.Manga ?? context.Mangas.Find(job.MangaId)!;
-                MangaConnector connector = manga.MangaConnector ?? context.MangaConnectors.Find(manga.MangaConnectorId)!;
-                if(!metadataJobsByConnector.TryAdd(connector, [job]))
-                    metadataJobsByConnector[connector].Add(job);
-            }
-        }
-        if (jobsByType.ContainsKey(JobType.RetrieveChaptersJob))
-        {
-            foreach (RetrieveChaptersJob job in jobsByType[JobType.RetrieveChaptersJob])
-            {
-                Manga? manga = context.Mangas.Find(job.MangaId);
-                if(manga is null)
-                    continue;
-                MangaConnector connector = manga.MangaConnector ?? context.MangaConnectors.Find(manga.MangaConnectorId)!;
-                if(!metadataJobsByConnector.TryAdd(connector, [job]))
-                    metadataJobsByConnector[connector].Add(job);
-            }
-        }
-        foreach (List<Job> metadataJobs in metadataJobsByConnector.Values)
-            ret = ret.Append(metadataJobs.MinBy(j => j.NextExecution))!;
-
-        if (jobsByType.ContainsKey(JobType.DownloadSingleChapterJob))
-        {
-            
-            Dictionary<MangaConnector, List<DownloadSingleChapterJob>> downloadJobsByConnector = new();
-            foreach (DownloadSingleChapterJob job in jobsByType[JobType.DownloadSingleChapterJob])
-            {
-                Chapter? chapter = context.Chapters.Find(job.ChapterId);
-                if(chapter is null)
-                    continue;
-                Manga manga = chapter.ParentManga ?? context.Mangas.Find(chapter.ParentMangaId)!;
-                MangaConnector connector = manga.MangaConnector ?? context.MangaConnectors.Find(manga.MangaConnectorId)!;
-            
-                if(!downloadJobsByConnector.TryAdd(connector, [job]))
-                    downloadJobsByConnector[connector].Add(job);
-            }
-            //From all jobs select those that are supposed to be executed soonest, then select the minimum chapternumber
-            foreach (List<DownloadSingleChapterJob> downloadJobs in downloadJobsByConnector.Values)
-                ret = ret.Append(
-                    downloadJobs.Where(j => j.NextExecution == downloadJobs
-                            .MinBy(mj => mj.NextExecution)!.NextExecution)
-                        .MinBy(j => context.Chapters.Find(j.ChapterId)!))!;
-        }
-        
-        return ret;
+    private static MangaConnector? GetJobConnector(Job job)
+    {
+        if (job is DownloadAvailableChaptersJob dacj)
+            return dacj.Manga.MangaConnector;
+        if (job is DownloadMangaCoverJob dmcj)
+            return  dmcj.Manga.MangaConnector;
+        if (job is DownloadSingleChapterJob dscj)
+            return  dscj.Chapter.ParentManga.MangaConnector;
+        if (job is RetrieveChaptersJob rcj)
+            return rcj.Manga.MangaConnector;
+        return null;
     }
 }
\ No newline at end of file

From 53d9be5656a3d16d13a4614d84dd95713c85e3cd Mon Sep 17 00:00:00 2001
From: Glax <johanna@bernloehr.eu>
Date: Fri, 9 May 2025 12:03:01 +0200
Subject: [PATCH 02/50] Add ToString Overrides for Chapter and Manga

---
 API/Schema/Chapter.cs | 5 +++++
 API/Schema/Manga.cs   | 5 +++++
 2 files changed, 10 insertions(+)

diff --git a/API/Schema/Chapter.cs b/API/Schema/Chapter.cs
index b56990a..054dd7a 100644
--- a/API/Schema/Chapter.cs
+++ b/API/Schema/Chapter.cs
@@ -181,4 +181,9 @@ public class Chapter : IComparable<Chapter>
         );
         return comicInfo.ToString();
     }
+
+    public override string ToString()
+    {
+        return $"{ChapterId} Vol.{VolumeNumber} Ch.{ChapterNumber} - {Title}";
+    }
 }
\ No newline at end of file
diff --git a/API/Schema/Manga.cs b/API/Schema/Manga.cs
index f3951cb..867a82f 100644
--- a/API/Schema/Manga.cs
+++ b/API/Schema/Manga.cs
@@ -140,4 +140,9 @@ public class Manga
         }
         return sb.ToString();
     }
+
+    public override string ToString()
+    {
+        return $"{MangaId} {Name}";
+    }
 }
\ No newline at end of file

From 2d69b30e83206484f1e23eab8e532ab5f12cbc49 Mon Sep 17 00:00:00 2001
From: Glax <johanna@bernloehr.eu>
Date: Fri, 9 May 2025 12:03:18 +0200
Subject: [PATCH 03/50] Fix missing Entity-Relation for
 UpdateFilesDownloadedJob

---
 API/Schema/PgsqlContext.cs | 9 +++++++++
 1 file changed, 9 insertions(+)

diff --git a/API/Schema/PgsqlContext.cs b/API/Schema/PgsqlContext.cs
index b3995bb..c7cbbb9 100644
--- a/API/Schema/PgsqlContext.cs
+++ b/API/Schema/PgsqlContext.cs
@@ -87,6 +87,15 @@ public class PgsqlContext(DbContextOptions<PgsqlContext> options) : DbContext(op
         modelBuilder.Entity<RetrieveChaptersJob>()
             .Navigation(j => j.Manga)
             .AutoInclude();
+        modelBuilder.Entity<UpdateFilesDownloadedJob>()
+            .HasOne<Manga>(j => j.Manga)
+            .WithMany()
+            .HasForeignKey(j => j.MangaId)
+            .IsRequired()
+            .OnDelete(DeleteBehavior.Cascade);
+        modelBuilder.Entity<UpdateFilesDownloadedJob>()
+            .Navigation(j => j.Manga)
+            .AutoInclude();
         
         //Job has possible ParentJob
         modelBuilder.Entity<Job>()

From b49b11828c1370efd5c59f70460c2f7509d39a69 Mon Sep 17 00:00:00 2001
From: Glax <johanna@bernloehr.eu>
Date: Fri, 9 May 2025 12:22:32 +0200
Subject: [PATCH 04/50] Add ToString Overriddes

---
 API/Schema/Author.cs        |  5 +++++
 API/Schema/Jobs/Job.cs      |  5 +++++
 API/Schema/Link.cs          |  5 +++++
 API/Schema/LocalLibrary.cs  |  5 +++++
 API/Schema/MangaAltTitle.cs |  5 +++++
 API/Schema/MangaTag.cs      |  5 +++++
 API/Schema/Notification.cs  | 40 +++++++++++++++++++++++++++++--------
 7 files changed, 62 insertions(+), 8 deletions(-)

diff --git a/API/Schema/Author.cs b/API/Schema/Author.cs
index 37685c3..b3149af 100644
--- a/API/Schema/Author.cs
+++ b/API/Schema/Author.cs
@@ -12,4 +12,9 @@ public class Author(string authorName)
     [StringLength(128)]
     [Required]
     public string AuthorName { get; init; } = authorName;
+
+    public override string ToString()
+    {
+        return $"{AuthorId} {AuthorName}";
+    }
 }
\ No newline at end of file
diff --git a/API/Schema/Jobs/Job.cs b/API/Schema/Jobs/Job.cs
index 07fb42a..c722b27 100644
--- a/API/Schema/Jobs/Job.cs
+++ b/API/Schema/Jobs/Job.cs
@@ -84,4 +84,9 @@ public abstract class Job
     }
     
     protected abstract IEnumerable<Job> RunInternal(PgsqlContext context);
+
+    public override string ToString()
+    {
+        return $"{JobId}";
+    }
 }
\ No newline at end of file
diff --git a/API/Schema/Link.cs b/API/Schema/Link.cs
index e20ac8b..e797a1e 100644
--- a/API/Schema/Link.cs
+++ b/API/Schema/Link.cs
@@ -16,4 +16,9 @@ public class Link(string linkProvider, string linkUrl)
     [Required]
     [Url]
     public string LinkUrl { get; init; } = linkUrl;
+
+    public override string ToString()
+    {
+        return $"{LinkId} {LinkProvider} {LinkUrl}";
+    }
 }
\ No newline at end of file
diff --git a/API/Schema/LocalLibrary.cs b/API/Schema/LocalLibrary.cs
index 5ad7597..274b763 100644
--- a/API/Schema/LocalLibrary.cs
+++ b/API/Schema/LocalLibrary.cs
@@ -14,4 +14,9 @@ public class LocalLibrary(string basePath, string libraryName)
     [StringLength(512)]
     [Required]
     public string LibraryName { get; internal set; } = libraryName;
+
+    public override string ToString()
+    {
+        return $"{LocalLibraryId} {LibraryName} - {BasePath}";
+    }
 }
\ No newline at end of file
diff --git a/API/Schema/MangaAltTitle.cs b/API/Schema/MangaAltTitle.cs
index 86248cf..23e5854 100644
--- a/API/Schema/MangaAltTitle.cs
+++ b/API/Schema/MangaAltTitle.cs
@@ -15,4 +15,9 @@ public class MangaAltTitle(string language, string title)
     [StringLength(256)]
     [Required]
     public string Title { get; set; } = title;
+
+    public override string ToString()
+    {
+        return $"{AltTitleId} {Language} {Title}";
+    }
 }
\ No newline at end of file
diff --git a/API/Schema/MangaTag.cs b/API/Schema/MangaTag.cs
index 2967d25..aa2e94b 100644
--- a/API/Schema/MangaTag.cs
+++ b/API/Schema/MangaTag.cs
@@ -9,4 +9,9 @@ public class MangaTag(string tag)
     [StringLength(64)]
     [Required]
     public string Tag { get; init; } = tag;
+
+    public override string ToString()
+    {
+        return $"{Tag}";
+    }
 }
\ No newline at end of file
diff --git a/API/Schema/Notification.cs b/API/Schema/Notification.cs
index 46dfb67..e58fe16 100644
--- a/API/Schema/Notification.cs
+++ b/API/Schema/Notification.cs
@@ -4,25 +4,49 @@ using Microsoft.EntityFrameworkCore;
 namespace API.Schema;
 
 [PrimaryKey("NotificationId")]
-public class Notification(string title, string message = "", NotificationUrgency urgency = NotificationUrgency.Normal, DateTime? date = null)
+public class Notification
 {
     [StringLength(64)]
     [Required]
-    public string NotificationId { get; init; } = TokenGen.CreateToken("Notification");
+    public string NotificationId { get; init; }
 
     [Required]
-    public NotificationUrgency Urgency { get; init; } = urgency;
+    public NotificationUrgency Urgency { get; init; }
 
     [StringLength(128)]
     [Required]
-    public string Title { get; init; } = title;
+    public string Title { get; init; }
 
     [StringLength(512)]
     [Required]
-    public string Message { get; init; } = message;
+    public string Message { get; init; }
     
     [Required]
-    public DateTime Date { get; init; } = date ?? DateTime.UtcNow;
-    
-    public Notification() : this("") { }
+    public DateTime Date { get; init; }
+
+    public Notification(string title, string message = "", NotificationUrgency urgency = NotificationUrgency.Normal, DateTime? date = null)
+    {
+        this.NotificationId = TokenGen.CreateToken("Notification");
+        this.Title = title;
+        this.Message = message;
+        this.Urgency = urgency;
+        this.Date = date ?? DateTime.UtcNow;
+    }
+
+    /// <summary>
+    /// EF ONLY!!!
+    /// </summary>
+    public Notification(string notificationId, string title, string message, NotificationUrgency urgency, DateTime date)
+    {
+        this.NotificationId = notificationId;
+        this.Title = title;
+        this.Message = message;
+        this.Urgency = urgency;
+        this.Date = date;
+    }
+
+    public override string ToString()
+    {
+        return $"{NotificationId} {Urgency} {Title}";
+    }
 }
\ No newline at end of file

From 0f6c06002659f374a534c47855f40a2d25505ce7 Mon Sep 17 00:00:00 2001
From: Glax <johanna@bernloehr.eu>
Date: Fri, 9 May 2025 12:30:30 +0200
Subject: [PATCH 05/50] Remove unnecessary default value for EF Constructors

---
 API/Schema/Jobs/DownloadAvailableChaptersJob.cs | 2 +-
 API/Schema/Jobs/DownloadMangaCoverJob.cs        | 2 +-
 API/Schema/Jobs/DownloadSingleChapterJob.cs     | 4 ++--
 API/Schema/Jobs/Job.cs                          | 2 +-
 API/Schema/Jobs/MoveFileOrFolderJob.cs          | 2 +-
 API/Schema/Jobs/MoveMangaLibraryJob.cs          | 2 +-
 API/Schema/Jobs/RetrieveChaptersJob.cs          | 2 +-
 API/Schema/Jobs/UpdateFilesDownloadedJob.cs     | 2 +-
 8 files changed, 9 insertions(+), 9 deletions(-)

diff --git a/API/Schema/Jobs/DownloadAvailableChaptersJob.cs b/API/Schema/Jobs/DownloadAvailableChaptersJob.cs
index 60358ba..164ad9b 100644
--- a/API/Schema/Jobs/DownloadAvailableChaptersJob.cs
+++ b/API/Schema/Jobs/DownloadAvailableChaptersJob.cs
@@ -18,7 +18,7 @@ public class DownloadAvailableChaptersJob : Job
     /// <summary>
     /// EF ONLY!!!
     /// </summary>
-    public DownloadAvailableChaptersJob(string mangaId, ulong recurrenceMs, string? parentJobId = null)
+    internal DownloadAvailableChaptersJob(string mangaId, ulong recurrenceMs, string? parentJobId)
         : base(TokenGen.CreateToken(typeof(DownloadAvailableChaptersJob)), JobType.DownloadAvailableChaptersJob, recurrenceMs, parentJobId)
     {
         this.MangaId = mangaId;
diff --git a/API/Schema/Jobs/DownloadMangaCoverJob.cs b/API/Schema/Jobs/DownloadMangaCoverJob.cs
index 533fe70..4eccd68 100644
--- a/API/Schema/Jobs/DownloadMangaCoverJob.cs
+++ b/API/Schema/Jobs/DownloadMangaCoverJob.cs
@@ -19,7 +19,7 @@ public class DownloadMangaCoverJob : Job
     /// <summary>
     /// EF ONLY!!!
     /// </summary>
-    public DownloadMangaCoverJob(string mangaId, string? parentJobId = null)
+    internal DownloadMangaCoverJob(string mangaId, string? parentJobId)
         : base(TokenGen.CreateToken(typeof(DownloadMangaCoverJob)), JobType.DownloadMangaCoverJob, 0, parentJobId)
     {
         this.MangaId = mangaId;
diff --git a/API/Schema/Jobs/DownloadSingleChapterJob.cs b/API/Schema/Jobs/DownloadSingleChapterJob.cs
index 94fabaa..b26a01c 100644
--- a/API/Schema/Jobs/DownloadSingleChapterJob.cs
+++ b/API/Schema/Jobs/DownloadSingleChapterJob.cs
@@ -27,8 +27,8 @@ public class DownloadSingleChapterJob : Job
     /// <summary>
     /// EF ONLY!!!
     /// </summary>
-    public DownloadSingleChapterJob(string chapterId, string? parentJobId = null)
-        : base(TokenGen.CreateToken(typeof(DownloadSingleChapterJob)), JobType.DownloadSingleChapterJob, 0)
+    internal DownloadSingleChapterJob(string chapterId, string? parentJobId)
+        : base(TokenGen.CreateToken(typeof(DownloadSingleChapterJob)), JobType.DownloadSingleChapterJob, 0, parentJobId)
     {
         this.ChapterId = chapterId;
     }
diff --git a/API/Schema/Jobs/Job.cs b/API/Schema/Jobs/Job.cs
index c722b27..7639a16 100644
--- a/API/Schema/Jobs/Job.cs
+++ b/API/Schema/Jobs/Job.cs
@@ -47,7 +47,7 @@ public abstract class Job
     /// <summary>
     /// EF ONLY!!!
     /// </summary>
-    protected Job(string jobId, JobType jobType, ulong recurrenceMs, string? parentJobId)
+    protected internal Job(string jobId, JobType jobType, ulong recurrenceMs, string? parentJobId)
     {
         this.JobId = jobId;
         this.JobType = jobType;
diff --git a/API/Schema/Jobs/MoveFileOrFolderJob.cs b/API/Schema/Jobs/MoveFileOrFolderJob.cs
index 9b62db2..b837239 100644
--- a/API/Schema/Jobs/MoveFileOrFolderJob.cs
+++ b/API/Schema/Jobs/MoveFileOrFolderJob.cs
@@ -21,7 +21,7 @@ public class MoveFileOrFolderJob : Job
     /// <summary>
     /// EF ONLY!!!
     /// </summary>
-    public MoveFileOrFolderJob(string jobId, string fromLocation, string toLocation, string? parentJobId = null)
+    internal MoveFileOrFolderJob(string jobId, string fromLocation, string toLocation, string? parentJobId)
         : base(jobId, JobType.MoveFileOrFolderJob, 0, parentJobId)
     {
         this.FromLocation = fromLocation;
diff --git a/API/Schema/Jobs/MoveMangaLibraryJob.cs b/API/Schema/Jobs/MoveMangaLibraryJob.cs
index 13cd201..853af00 100644
--- a/API/Schema/Jobs/MoveMangaLibraryJob.cs
+++ b/API/Schema/Jobs/MoveMangaLibraryJob.cs
@@ -23,7 +23,7 @@ public class MoveMangaLibraryJob : Job
     /// <summary>
     /// EF ONLY!!!
     /// </summary>
-    public MoveMangaLibraryJob(string mangaId, string toLibraryId, string? parentJobId = null)
+    internal MoveMangaLibraryJob(string mangaId, string toLibraryId, string? parentJobId)
         : base(TokenGen.CreateToken(typeof(MoveMangaLibraryJob)), JobType.MoveMangaLibraryJob, 0, parentJobId)
     {
         this.MangaId = mangaId;
diff --git a/API/Schema/Jobs/RetrieveChaptersJob.cs b/API/Schema/Jobs/RetrieveChaptersJob.cs
index e4f7257..d9e7b7e 100644
--- a/API/Schema/Jobs/RetrieveChaptersJob.cs
+++ b/API/Schema/Jobs/RetrieveChaptersJob.cs
@@ -22,7 +22,7 @@ public class RetrieveChaptersJob : Job
     /// <summary>
     /// EF ONLY!!!
     /// </summary>
-    public RetrieveChaptersJob(string mangaId, string language, ulong recurrenceMs, string? parentJobId = null)
+    internal RetrieveChaptersJob(string mangaId, string language, ulong recurrenceMs, string? parentJobId)
         : base(TokenGen.CreateToken(typeof(RetrieveChaptersJob)), JobType.RetrieveChaptersJob, recurrenceMs, parentJobId)
     {
         this.MangaId = mangaId;
diff --git a/API/Schema/Jobs/UpdateFilesDownloadedJob.cs b/API/Schema/Jobs/UpdateFilesDownloadedJob.cs
index 3e151bf..a1e0c96 100644
--- a/API/Schema/Jobs/UpdateFilesDownloadedJob.cs
+++ b/API/Schema/Jobs/UpdateFilesDownloadedJob.cs
@@ -19,7 +19,7 @@ public class UpdateFilesDownloadedJob : Job
     /// <summary>
     /// EF ONLY!!!
     /// </summary>
-    public UpdateFilesDownloadedJob(string mangaId, ulong recurrenceMs, string? parentJobId = null)
+    internal UpdateFilesDownloadedJob(string mangaId, ulong recurrenceMs, string? parentJobId)
         : base(TokenGen.CreateToken(typeof(UpdateFilesDownloadedJob)), JobType.UpdateFilesDownloadedJob, recurrenceMs, parentJobId)
     {
         this.MangaId = mangaId;

From 694b88d20011e72ba3054be40adf4d3d9ce9987a Mon Sep 17 00:00:00 2001
From: Glax <johanna@bernloehr.eu>
Date: Fri, 9 May 2025 12:31:40 +0200
Subject: [PATCH 06/50] Reload Jobs loaded in context

---
 API/Tranga.cs | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/API/Tranga.cs b/API/Tranga.cs
index 976ed9e..753f26d 100644
--- a/API/Tranga.cs
+++ b/API/Tranga.cs
@@ -5,6 +5,7 @@ using API.Schema.NotificationConnectors;
 using log4net;
 using log4net.Config;
 using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.ChangeTracking;
 
 namespace API;
 
@@ -119,6 +120,8 @@ public static class Tranga
         Log.Info("JobStarter Thread running.");
         while (true)
         {
+            foreach (EntityEntry entityEntry in context.ChangeTracker.Entries().ToArray())
+                entityEntry.Reload();
             //Update finished Jobs to new states
             List<Job> completedJobs = context.Jobs.Where(j => j.state == JobState.Completed).ToList();
             foreach (Job completedJob in completedJobs)

From 475a29b10dc4290cc5374e0da08224e1a0fe5513 Mon Sep 17 00:00:00 2001
From: Glax <johanna@bernloehr.eu>
Date: Thu, 15 May 2025 15:13:53 +0200
Subject: [PATCH 07/50] Attach Entities to Jobs

---
 API/Controllers/JobController.cs              |  1 +
 API/Controllers/LibraryConnectorController.cs |  3 +-
 API/Controllers/LocalLibrariesController.cs   |  1 +
 API/Controllers/MangaConnectorController.cs   |  2 +-
 API/Controllers/MangaController.cs            |  1 +
 .../NotificationConnectorController.cs        |  4 +-
 API/Controllers/QueryController.cs            |  1 +
 API/Controllers/SearchController.cs           |  1 +
 API/Controllers/SettingsController.cs         |  1 +
 API/Program.cs                                | 64 ++++++++++++-------
 API/Schema/Contexts/LibraryContext.cs         | 18 ++++++
 API/Schema/Contexts/NotificationsContext.cs   | 10 +++
 API/Schema/{ => Contexts}/PgsqlContext.cs     | 32 ++++++----
 .../Jobs/DownloadAvailableChaptersJob.cs      |  2 +
 API/Schema/Jobs/DownloadMangaCoverJob.cs      |  2 +
 API/Schema/Jobs/DownloadSingleChapterJob.cs   |  2 +
 API/Schema/Jobs/Job.cs                        |  5 +-
 API/Schema/Jobs/MoveFileOrFolderJob.cs        |  1 +
 API/Schema/Jobs/MoveMangaLibraryJob.cs        |  2 +
 API/Schema/Jobs/RetrieveChaptersJob.cs        |  3 +-
 API/Schema/Jobs/UpdateFilesDownloadedJob.cs   |  2 +
 API/Schema/MangaConnectors/Global.cs          |  4 +-
 API/Tranga.cs                                 | 27 ++++----
 23 files changed, 132 insertions(+), 57 deletions(-)
 create mode 100644 API/Schema/Contexts/LibraryContext.cs
 create mode 100644 API/Schema/Contexts/NotificationsContext.cs
 rename API/Schema/{ => Contexts}/PgsqlContext.cs (90%)

diff --git a/API/Controllers/JobController.cs b/API/Controllers/JobController.cs
index 97714b0..a4011ea 100644
--- a/API/Controllers/JobController.cs
+++ b/API/Controllers/JobController.cs
@@ -1,5 +1,6 @@
 using API.APIEndpointRecords;
 using API.Schema;
+using API.Schema.Contexts;
 using API.Schema.Jobs;
 using Asp.Versioning;
 using log4net;
diff --git a/API/Controllers/LibraryConnectorController.cs b/API/Controllers/LibraryConnectorController.cs
index 343462c..0db5748 100644
--- a/API/Controllers/LibraryConnectorController.cs
+++ b/API/Controllers/LibraryConnectorController.cs
@@ -1,4 +1,5 @@
 using API.Schema;
+using API.Schema.Contexts;
 using API.Schema.LibraryConnectors;
 using Asp.Versioning;
 using log4net;
@@ -10,7 +11,7 @@ namespace API.Controllers;
 [ApiVersion(2)]
 [ApiController]
 [Route("v{v:apiVersion}/[controller]")]
-public class LibraryConnectorController(PgsqlContext context, ILog Log) : Controller
+public class LibraryConnectorController(LibraryContext context, ILog Log) : Controller
 {
     /// <summary>
     /// Gets all configured ToLibrary-Connectors
diff --git a/API/Controllers/LocalLibrariesController.cs b/API/Controllers/LocalLibrariesController.cs
index 004fa6d..9f09118 100644
--- a/API/Controllers/LocalLibrariesController.cs
+++ b/API/Controllers/LocalLibrariesController.cs
@@ -1,5 +1,6 @@
 using API.APIEndpointRecords;
 using API.Schema;
+using API.Schema.Contexts;
 using Asp.Versioning;
 using log4net;
 using Microsoft.AspNetCore.Mvc;
diff --git a/API/Controllers/MangaConnectorController.cs b/API/Controllers/MangaConnectorController.cs
index 91c7f14..095ee59 100644
--- a/API/Controllers/MangaConnectorController.cs
+++ b/API/Controllers/MangaConnectorController.cs
@@ -1,4 +1,4 @@
-using API.Schema;
+using API.Schema.Contexts;
 using API.Schema.MangaConnectors;
 using Asp.Versioning;
 using log4net;
diff --git a/API/Controllers/MangaController.cs b/API/Controllers/MangaController.cs
index 07a4065..6098d3a 100644
--- a/API/Controllers/MangaController.cs
+++ b/API/Controllers/MangaController.cs
@@ -1,4 +1,5 @@
 using API.Schema;
+using API.Schema.Contexts;
 using API.Schema.Jobs;
 using Asp.Versioning;
 using log4net;
diff --git a/API/Controllers/NotificationConnectorController.cs b/API/Controllers/NotificationConnectorController.cs
index 66ef992..a242e68 100644
--- a/API/Controllers/NotificationConnectorController.cs
+++ b/API/Controllers/NotificationConnectorController.cs
@@ -1,6 +1,6 @@
 using System.Text;
 using API.APIEndpointRecords;
-using API.Schema;
+using API.Schema.Contexts;
 using API.Schema.NotificationConnectors;
 using Asp.Versioning;
 using log4net;
@@ -13,7 +13,7 @@ namespace API.Controllers;
 [ApiController]
 [Produces("application/json")]
 [Route("v{v:apiVersion}/[controller]")]
-public class NotificationConnectorController(PgsqlContext context, ILog Log) : Controller
+public class NotificationConnectorController(NotificationsContext context, ILog Log) : Controller
 {
     /// <summary>
     /// Gets all configured Notification-Connectors
diff --git a/API/Controllers/QueryController.cs b/API/Controllers/QueryController.cs
index 8877a8d..f4d603f 100644
--- a/API/Controllers/QueryController.cs
+++ b/API/Controllers/QueryController.cs
@@ -1,4 +1,5 @@
 using API.Schema;
+using API.Schema.Contexts;
 using Asp.Versioning;
 using log4net;
 using Microsoft.AspNetCore.Mvc;
diff --git a/API/Controllers/SearchController.cs b/API/Controllers/SearchController.cs
index 8ba0e45..86daa71 100644
--- a/API/Controllers/SearchController.cs
+++ b/API/Controllers/SearchController.cs
@@ -1,4 +1,5 @@
 using API.Schema;
+using API.Schema.Contexts;
 using API.Schema.Jobs;
 using API.Schema.MangaConnectors;
 using Asp.Versioning;
diff --git a/API/Controllers/SettingsController.cs b/API/Controllers/SettingsController.cs
index 04da6a5..f6cbd05 100644
--- a/API/Controllers/SettingsController.cs
+++ b/API/Controllers/SettingsController.cs
@@ -1,5 +1,6 @@
 using API.MangaDownloadClients;
 using API.Schema;
+using API.Schema.Contexts;
 using API.Schema.Jobs;
 using Asp.Versioning;
 using log4net;
diff --git a/API/Program.cs b/API/Program.cs
index 0670a65..e9f59a5 100644
--- a/API/Program.cs
+++ b/API/Program.cs
@@ -1,7 +1,7 @@
 using System.Reflection;
-using System.Text.Json.Serialization;
 using API;
 using API.Schema;
+using API.Schema.Contexts;
 using API.Schema.Jobs;
 using API.Schema.MangaConnectors;
 using Asp.Versioning;
@@ -55,11 +55,17 @@ builder.Services.AddSwaggerGen(opt =>
 });
 builder.Services.ConfigureOptions<NamedSwaggerGenOptions>();
 
+string ConnectionString = $"Host={Environment.GetEnvironmentVariable("POSTGRES_HOST") ?? "localhost:5432"}; " +
+                          $"Database={Environment.GetEnvironmentVariable("POSTGRES_DB") ?? "postgres"}; " +
+                          $"Username={Environment.GetEnvironmentVariable("POSTGRES_USER") ?? "postgres"}; " +
+                          $"Password={Environment.GetEnvironmentVariable("POSTGRES_PASSWORD") ?? "postgres"}";
+
 builder.Services.AddDbContext<PgsqlContext>(options =>
-    options.UseNpgsql($"Host={Environment.GetEnvironmentVariable("POSTGRES_HOST")??"localhost:5432"}; " +
-                      $"Database={Environment.GetEnvironmentVariable("POSTGRES_DB")??"postgres"}; " +
-                      $"Username={Environment.GetEnvironmentVariable("POSTGRES_USER")??"postgres"}; " +
-                      $"Password={Environment.GetEnvironmentVariable("POSTGRES_PASSWORD")??"postgres"}"));
+    options.UseNpgsql(ConnectionString));
+builder.Services.AddDbContext<NotificationsContext>(options =>
+    options.UseNpgsql(ConnectionString));
+builder.Services.AddDbContext<LibraryContext>(options =>
+    options.UseNpgsql(ConnectionString));
 
 builder.Services.AddControllers(options =>
 {
@@ -99,30 +105,42 @@ app.UseHttpsRedirection();
 
 app.UseMiddleware<RequestTimeMiddleware>();
 
-using (var scope = app.Services.CreateScope())
+using (IServiceScope scope = app.Services.CreateScope())
 {
-    var db = scope.ServiceProvider.GetRequiredService<PgsqlContext>();
-    db.Database.Migrate();
-}
-
-using (var scope = app.Services.CreateScope())
-{
-    PgsqlContext context = scope.ServiceProvider.GetService<PgsqlContext>()!;
+    PgsqlContext context = scope.ServiceProvider.GetRequiredService<PgsqlContext>();
+    context.Database.Migrate();
     
     MangaConnector[] connectors =
-        [
-            new MangaDex(),
-            new Global(scope.ServiceProvider.GetService<PgsqlContext>()!)
-        ];
+    [
+        new MangaDex(),
+        new Global(scope.ServiceProvider.GetService<PgsqlContext>()!)
+    ];
     MangaConnector[] newConnectors = connectors.Where(c => !context.MangaConnectors.Contains(c)).ToArray();
     context.MangaConnectors.AddRange(newConnectors);
-
-    context.Jobs.AddRange(context.Mangas.AsEnumerable().Select(m => new UpdateFilesDownloadedJob(m, 0)));
-    
-    context.Jobs.RemoveRange(context.Jobs.Where(j => j.state == JobState.Completed && j.RecurrenceMs < 1));
-
     if (!context.LocalLibraries.Any())
-        context.LocalLibraries.Add(new LocalLibrary(TrangaSettings.downloadLocation, "Default ToLibrary"));
+        context.LocalLibraries.Add(new LocalLibrary(TrangaSettings.downloadLocation, "Default Library"));
+
+    context.Jobs.AddRange(context.Jobs.Where(j => j.JobType == JobType.DownloadAvailableChaptersJob)
+        .AsEnumerable()
+        .Select(dacj =>
+        {
+            DownloadAvailableChaptersJob? j = dacj as DownloadAvailableChaptersJob;
+            return new UpdateFilesDownloadedJob(j!.Manga, 0);
+        }));
+    context.Jobs.RemoveRange(context.Jobs.Where(j => j.state == JobState.Completed && j.RecurrenceMs < 1));
+    foreach (Job job in context.Jobs.Where(j => j.state == JobState.Running))
+    {
+        job.state = JobState.FirstExecution;
+        job.LastExecution = DateTime.UnixEpoch;
+    }
+    
+    context.SaveChanges();
+}
+
+using (IServiceScope scope = app.Services.CreateScope())
+{
+    NotificationsContext context = scope.ServiceProvider.GetRequiredService<NotificationsContext>();
+    context.Database.Migrate();
     
     string[] emojis = { "(•‿•)", "(づ \u25d5‿\u25d5 )づ", "( \u02d8\u25bd\u02d8)っ\u2668", "=\uff3e\u25cf \u22cf \u25cf\uff3e=", "(ΦωΦ)", "(\u272a\u3268\u272a)", "( ノ・o・ )ノ", "(〜^\u2207^ )〜", "~(\u2267ω\u2266)~","૮ \u00b4• ﻌ \u00b4• ა", "(\u02c3ᆺ\u02c2)", "(=\ud83d\udf66 \u0f1d \ud83d\udf66=)"};
     context.Notifications.Add(new Notification("Tranga Started", emojis[Random.Shared.Next(0, emojis.Length - 1)], NotificationUrgency.High));
diff --git a/API/Schema/Contexts/LibraryContext.cs b/API/Schema/Contexts/LibraryContext.cs
new file mode 100644
index 0000000..8d13ef6
--- /dev/null
+++ b/API/Schema/Contexts/LibraryContext.cs
@@ -0,0 +1,18 @@
+using API.Schema.LibraryConnectors;
+using Microsoft.EntityFrameworkCore;
+
+namespace API.Schema.Contexts;
+
+public class LibraryContext(DbContextOptions<LibraryContext> options) : DbContext(options)
+{
+    public DbSet<LibraryConnector> LibraryConnectors { get; set; }
+
+    protected override void OnModelCreating(ModelBuilder modelBuilder)
+    {
+        //LibraryConnector Types
+        modelBuilder.Entity<LibraryConnector>()
+            .HasDiscriminator(l => l.LibraryType)
+            .HasValue<Komga>(LibraryType.Komga)
+            .HasValue<Kavita>(LibraryType.Kavita);
+    }
+}
\ No newline at end of file
diff --git a/API/Schema/Contexts/NotificationsContext.cs b/API/Schema/Contexts/NotificationsContext.cs
new file mode 100644
index 0000000..26f5699
--- /dev/null
+++ b/API/Schema/Contexts/NotificationsContext.cs
@@ -0,0 +1,10 @@
+using API.Schema.NotificationConnectors;
+using Microsoft.EntityFrameworkCore;
+
+namespace API.Schema.Contexts;
+
+public class NotificationsContext(DbContextOptions<NotificationsContext> options) : DbContext(options)
+{
+    public DbSet<NotificationConnector> NotificationConnectors { get; set; }
+    public DbSet<Notification> Notifications { get; set; }
+}
\ No newline at end of file
diff --git a/API/Schema/PgsqlContext.cs b/API/Schema/Contexts/PgsqlContext.cs
similarity index 90%
rename from API/Schema/PgsqlContext.cs
rename to API/Schema/Contexts/PgsqlContext.cs
index c7cbbb9..fc1f1f9 100644
--- a/API/Schema/PgsqlContext.cs
+++ b/API/Schema/Contexts/PgsqlContext.cs
@@ -1,10 +1,9 @@
 using API.Schema.Jobs;
 using API.Schema.LibraryConnectors;
 using API.Schema.MangaConnectors;
-using API.Schema.NotificationConnectors;
 using Microsoft.EntityFrameworkCore;
 
-namespace API.Schema;
+namespace API.Schema.Contexts;
 
 public class PgsqlContext(DbContextOptions<PgsqlContext> options) : DbContext(options)
 {
@@ -15,9 +14,6 @@ public class PgsqlContext(DbContextOptions<PgsqlContext> options) : DbContext(op
     public DbSet<Chapter> Chapters { get; set; }
     public DbSet<Author> Authors { get; set; }
     public DbSet<MangaTag> Tags { get; set; }
-    public DbSet<LibraryConnector> LibraryConnectors { get; set; }
-    public DbSet<NotificationConnector> NotificationConnectors { get; set; }
-    public DbSet<Notification> Notifications { get; set; }
     
     protected override void OnModelCreating(ModelBuilder modelBuilder)
     {
@@ -109,7 +105,7 @@ public class PgsqlContext(DbContextOptions<PgsqlContext> options) : DbContext(op
             .HasMany<Job>(root => root.DependsOnJobs)
             .WithMany();
         modelBuilder.Entity<Job>()
-            .Navigation(root => root.DependsOnJobs)
+            .Navigation(j => j.DependsOnJobs)
             .AutoInclude(false);
         
         //MangaConnector Types
@@ -138,14 +134,23 @@ public class PgsqlContext(DbContextOptions<PgsqlContext> options) : DbContext(op
         modelBuilder.Entity<Chapter>()
             .Navigation(c => c.ParentManga)
             .AutoInclude();
+        modelBuilder.Entity<Manga>()
+            .Navigation(m => m.Chapters)
+            .AutoInclude();
         //Manga owns MangaAltTitles
         modelBuilder.Entity<Manga>()
             .OwnsMany<MangaAltTitle>(m => m.AltTitles)
             .WithOwner();
+        modelBuilder.Entity<Manga>()
+            .Navigation(m => m.AltTitles)
+            .AutoInclude();
         //Manga owns Links
         modelBuilder.Entity<Manga>()
             .OwnsMany<Link>(m => m.Links)
             .WithOwner();
+        modelBuilder.Entity<Manga>()
+            .Navigation(m => m.Links)
+            .AutoInclude();
         //Manga has many Tags associated with many Manga
         modelBuilder.Entity<Manga>()
             .HasMany<MangaTag>(m => m.MangaTags)
@@ -155,6 +160,9 @@ public class PgsqlContext(DbContextOptions<PgsqlContext> options) : DbContext(op
                 r => r.HasOne(typeof(Manga)).WithMany().HasForeignKey("MangaIds").HasPrincipalKey(nameof(Manga.MangaId)),
                 j => j.HasKey("MangaTagIds", "MangaIds")
             );
+        modelBuilder.Entity<Manga>()
+            .Navigation(m => m.MangaTags)
+            .AutoInclude();
         //Manga has many Authors associated with many Manga
         modelBuilder.Entity<Manga>()
             .HasMany<Author>(m => m.Authors)
@@ -164,6 +172,9 @@ public class PgsqlContext(DbContextOptions<PgsqlContext> options) : DbContext(op
                 r => r.HasOne(typeof(Manga)).WithMany().HasForeignKey("MangaIds").HasPrincipalKey(nameof(Manga.MangaId)),
                 j => j.HasKey("AuthorIds", "MangaIds")
             );
+        modelBuilder.Entity<Manga>()
+            .Navigation(m => m.Authors)
+            .AutoInclude();
         
         //LocalLibrary has many Mangas
         modelBuilder.Entity<LocalLibrary>()
@@ -171,11 +182,8 @@ public class PgsqlContext(DbContextOptions<PgsqlContext> options) : DbContext(op
             .WithOne(m => m.Library)
             .HasForeignKey(m => m.LibraryId)
             .OnDelete(DeleteBehavior.SetNull);
-        
-        //LibraryConnector Types
-        modelBuilder.Entity<LibraryConnector>()
-            .HasDiscriminator(l => l.LibraryType)
-            .HasValue<Komga>(LibraryType.Komga)
-            .HasValue<Kavita>(LibraryType.Kavita);
+        modelBuilder.Entity<Manga>()
+            .Navigation(m => m.Library)
+            .AutoInclude();
     }
 }
\ No newline at end of file
diff --git a/API/Schema/Jobs/DownloadAvailableChaptersJob.cs b/API/Schema/Jobs/DownloadAvailableChaptersJob.cs
index 164ad9b..1e45cfa 100644
--- a/API/Schema/Jobs/DownloadAvailableChaptersJob.cs
+++ b/API/Schema/Jobs/DownloadAvailableChaptersJob.cs
@@ -1,4 +1,5 @@
 using System.ComponentModel.DataAnnotations;
+using API.Schema.Contexts;
 using Newtonsoft.Json;
 
 namespace API.Schema.Jobs;
@@ -26,6 +27,7 @@ public class DownloadAvailableChaptersJob : Job
     
     protected override IEnumerable<Job> RunInternal(PgsqlContext context)
     {
+        context.Attach(Manga);
         return Manga.Chapters.Select(chapter => new DownloadSingleChapterJob(chapter, this));
     }
 }
\ No newline at end of file
diff --git a/API/Schema/Jobs/DownloadMangaCoverJob.cs b/API/Schema/Jobs/DownloadMangaCoverJob.cs
index 4eccd68..d748fa3 100644
--- a/API/Schema/Jobs/DownloadMangaCoverJob.cs
+++ b/API/Schema/Jobs/DownloadMangaCoverJob.cs
@@ -1,4 +1,5 @@
 using System.ComponentModel.DataAnnotations;
+using API.Schema.Contexts;
 using Microsoft.EntityFrameworkCore;
 using Newtonsoft.Json;
 
@@ -27,6 +28,7 @@ public class DownloadMangaCoverJob : Job
     
     protected override IEnumerable<Job> RunInternal(PgsqlContext context)
     {
+        context.Attach(Manga);
         try
         {
             Manga.CoverFileNameInCache = Manga.MangaConnector.SaveCoverImageToCache(Manga);
diff --git a/API/Schema/Jobs/DownloadSingleChapterJob.cs b/API/Schema/Jobs/DownloadSingleChapterJob.cs
index b26a01c..9e4f94f 100644
--- a/API/Schema/Jobs/DownloadSingleChapterJob.cs
+++ b/API/Schema/Jobs/DownloadSingleChapterJob.cs
@@ -2,6 +2,7 @@
 using System.IO.Compression;
 using System.Runtime.InteropServices;
 using API.MangaDownloadClients;
+using API.Schema.Contexts;
 using Newtonsoft.Json;
 using SixLabors.ImageSharp;
 using SixLabors.ImageSharp.Formats.Jpeg;
@@ -35,6 +36,7 @@ public class DownloadSingleChapterJob : Job
     
     protected override IEnumerable<Job> RunInternal(PgsqlContext context)
     {
+        context.Attach(Chapter);
         string[] imageUrls = Chapter.ParentManga.MangaConnector.GetChapterImageUrls(Chapter);
         if (imageUrls.Length < 1)
         {
diff --git a/API/Schema/Jobs/Job.cs b/API/Schema/Jobs/Job.cs
index 7639a16..fbb36c3 100644
--- a/API/Schema/Jobs/Job.cs
+++ b/API/Schema/Jobs/Job.cs
@@ -1,5 +1,6 @@
 using System.ComponentModel.DataAnnotations;
 using System.ComponentModel.DataAnnotations.Schema;
+using API.Schema.Contexts;
 using log4net;
 using Microsoft.EntityFrameworkCore;
 using Newtonsoft.Json;
@@ -62,14 +63,16 @@ public abstract class Job
     {
         Log.Debug($"Running job {JobId}");
         using IServiceScope scope = serviceProvider.CreateScope();
-        PgsqlContext context = scope.ServiceProvider.GetRequiredService<PgsqlContext>();
 
         try
         {
+            PgsqlContext context = scope.ServiceProvider.GetRequiredService<PgsqlContext>();
+            context.Attach(this);
             this.state = JobState.Running;
             context.SaveChanges();
             Job[] newJobs = RunInternal(context).ToArray();
             this.state = JobState.Completed;
+            context.SaveChanges();
             context.Jobs.AddRange(newJobs);
             context.SaveChanges();
             Log.Info($"Job {JobId} completed. Generated {newJobs.Length} new jobs.");
diff --git a/API/Schema/Jobs/MoveFileOrFolderJob.cs b/API/Schema/Jobs/MoveFileOrFolderJob.cs
index b837239..ef71104 100644
--- a/API/Schema/Jobs/MoveFileOrFolderJob.cs
+++ b/API/Schema/Jobs/MoveFileOrFolderJob.cs
@@ -1,4 +1,5 @@
 using System.ComponentModel.DataAnnotations;
+using API.Schema.Contexts;
 
 namespace API.Schema.Jobs;
 
diff --git a/API/Schema/Jobs/MoveMangaLibraryJob.cs b/API/Schema/Jobs/MoveMangaLibraryJob.cs
index 853af00..a533e9c 100644
--- a/API/Schema/Jobs/MoveMangaLibraryJob.cs
+++ b/API/Schema/Jobs/MoveMangaLibraryJob.cs
@@ -1,4 +1,5 @@
 using System.ComponentModel.DataAnnotations;
+using API.Schema.Contexts;
 using Microsoft.EntityFrameworkCore;
 using Newtonsoft.Json;
 
@@ -32,6 +33,7 @@ public class MoveMangaLibraryJob : Job
     
     protected override IEnumerable<Job> RunInternal(PgsqlContext context)
     {
+        context.Attach(Manga);
         Dictionary<Chapter, string> oldPath = Manga.Chapters.ToDictionary(c => c, c => c.FullArchiveFilePath);
         Manga.Library = ToLibrary;
         try
diff --git a/API/Schema/Jobs/RetrieveChaptersJob.cs b/API/Schema/Jobs/RetrieveChaptersJob.cs
index d9e7b7e..a89f7c6 100644
--- a/API/Schema/Jobs/RetrieveChaptersJob.cs
+++ b/API/Schema/Jobs/RetrieveChaptersJob.cs
@@ -1,5 +1,5 @@
 using System.ComponentModel.DataAnnotations;
-using API.Schema.MangaConnectors;
+using API.Schema.Contexts;
 using Microsoft.EntityFrameworkCore;
 using Newtonsoft.Json;
 
@@ -31,6 +31,7 @@ public class RetrieveChaptersJob : Job
     
     protected override IEnumerable<Job> RunInternal(PgsqlContext context)
     {
+        context.Attach(Manga);
         // This gets all chapters that are not downloaded
         Chapter[] allChapters = Manga.MangaConnector.GetChapters(Manga, Language);
         Chapter[] newChapters = allChapters.Where(chapter => context.Chapters.Contains(chapter) == false).ToArray();
diff --git a/API/Schema/Jobs/UpdateFilesDownloadedJob.cs b/API/Schema/Jobs/UpdateFilesDownloadedJob.cs
index a1e0c96..b0497ee 100644
--- a/API/Schema/Jobs/UpdateFilesDownloadedJob.cs
+++ b/API/Schema/Jobs/UpdateFilesDownloadedJob.cs
@@ -1,4 +1,5 @@
 using System.ComponentModel.DataAnnotations;
+using API.Schema.Contexts;
 using Microsoft.EntityFrameworkCore;
 using Newtonsoft.Json;
 
@@ -27,6 +28,7 @@ public class UpdateFilesDownloadedJob : Job
     
     protected override IEnumerable<Job> RunInternal(PgsqlContext context)
     {
+        context.Attach(Manga);
         foreach (Chapter chapter in Manga.Chapters)
             chapter.Downloaded = chapter.CheckDownloaded();
 
diff --git a/API/Schema/MangaConnectors/Global.cs b/API/Schema/MangaConnectors/Global.cs
index c4f0b73..0e04501 100644
--- a/API/Schema/MangaConnectors/Global.cs
+++ b/API/Schema/MangaConnectors/Global.cs
@@ -1,4 +1,6 @@
-namespace API.Schema.MangaConnectors;
+using API.Schema.Contexts;
+
+namespace API.Schema.MangaConnectors;
 
 public class Global : MangaConnector
 {
diff --git a/API/Tranga.cs b/API/Tranga.cs
index 753f26d..1024568 100644
--- a/API/Tranga.cs
+++ b/API/Tranga.cs
@@ -1,4 +1,5 @@
 using API.Schema;
+using API.Schema.Contexts;
 using API.Schema.Jobs;
 using API.Schema.MangaConnectors;
 using API.Schema.NotificationConnectors;
@@ -30,12 +31,7 @@ public static class Tranga
         }
         IServiceProvider serviceProvider = (IServiceProvider)serviceProviderObj!;
         using IServiceScope scope = serviceProvider.CreateScope();
-        PgsqlContext? context = scope.ServiceProvider.GetService<PgsqlContext>();
-        if (context is null)
-        {
-            Log.Error("PgsqlContext is null");
-            return;
-        }
+        NotificationsContext context = scope.ServiceProvider.GetRequiredService<NotificationsContext>();
 
         try
         {
@@ -64,12 +60,7 @@ public static class Tranga
     {
         Log.Info($"Sending notifications for {urgency}");
         using IServiceScope scope = serviceProvider.CreateScope();
-        PgsqlContext? context = scope.ServiceProvider.GetService<PgsqlContext>();
-        if (context is null)
-        {
-            Log.Error("PgsqlContext is null");
-            return;
-        }
+        NotificationsContext context = scope.ServiceProvider.GetRequiredService<NotificationsContext>();
         
         List<Notification> notifications = context.Notifications.Where(n => n.Urgency == urgency).ToList();
         if (!notifications.Any())
@@ -117,6 +108,8 @@ public static class Tranga
         }
 
         Log.Info(TRANGA);
+        Log.Info("Loading Jobs");
+        context.Jobs.Load();
         Log.Info("JobStarter Thread running.");
         while (true)
         {
@@ -146,7 +139,7 @@ public static class Tranga
             List<Job> dueJobs = waitingJobs.Where(j => j.NextExecution < DateTime.UtcNow).ToList();
 
             List<MangaConnector> busyConnectors = GetBusyConnectors(runningJobs);
-            List<Job> startJobs = FilterJobPreconditions(dueJobs, busyConnectors);
+            List<Job> startJobs = FilterJobPreconditions(context, dueJobs, busyConnectors);
             
             //Start Jobs that are allowed to run (preconditions match)
             foreach (Job job in startJobs)
@@ -194,9 +187,13 @@ public static class Tranga
         return busyConnectors.ToList();
     }
 
-    private static List<Job> FilterJobPreconditions(List<Job> dueJobs, List<MangaConnector> busyConnectors) =>
+    private static List<Job> FilterJobPreconditions(PgsqlContext context, List<Job> dueJobs, List<MangaConnector> busyConnectors) =>
         dueJobs
-            .Where(j => j.DependenciesFulfilled)
+            .Where(j =>
+            {
+                context.Entry(j).Collection(j => j.DependsOnJobs).Load(LoadOptions.ForceIdentityResolution);
+                return j.DependenciesFulfilled;
+            })
             .Where(j =>
             {
                 //Filter jobs with busy connectors

From 4b4e24c6a0f4e375fa8444dc3aaa0b151591e8aa Mon Sep 17 00:00:00 2001
From: Glax <johanna@bernloehr.eu>
Date: Thu, 15 May 2025 15:14:09 +0200
Subject: [PATCH 08/50] Migrations update

---
 .../20250509033915_Initial.Designer.cs        | 786 ------------------
 .../20250509034207_Initial-2.Designer.cs      | 786 ------------------
 API/Migrations/20250509034207_Initial-2.cs    |  22 -
 .../20250509035413_Initial-3.Designer.cs      | 786 ------------------
 API/Migrations/20250509035413_Initial-3.cs    | 130 ---
 .../20250509035606_Initial-4.Designer.cs      | 784 -----------------
 API/Migrations/20250509035606_Initial-4.cs    |  40 -
 API/Migrations/20250509035754_Initial-5.cs    |  40 -
 .../20250515120732_Initial.Designer.cs        |  71 ++
 .../library/20250515120732_Initial.cs         |  35 +
 .../library/LibraryContextModelSnapshot.cs    |  68 ++
 .../20250515120746_Initial.Designer.cs        |  89 ++
 .../notifications/20250515120746_Initial.cs   |  59 ++
 .../NotificationsContextModelSnapshot.cs      |  86 ++
 .../20250515120724_Initial-1.Designer.cs}     | 109 +--
 .../20250515120724_Initial-1.cs}              | 129 +--
 .../{ => pgsql}/PgsqlContextModelSnapshot.cs  | 105 +--
 17 files changed, 450 insertions(+), 3675 deletions(-)
 delete mode 100644 API/Migrations/20250509033915_Initial.Designer.cs
 delete mode 100644 API/Migrations/20250509034207_Initial-2.Designer.cs
 delete mode 100644 API/Migrations/20250509034207_Initial-2.cs
 delete mode 100644 API/Migrations/20250509035413_Initial-3.Designer.cs
 delete mode 100644 API/Migrations/20250509035413_Initial-3.cs
 delete mode 100644 API/Migrations/20250509035606_Initial-4.Designer.cs
 delete mode 100644 API/Migrations/20250509035606_Initial-4.cs
 delete mode 100644 API/Migrations/20250509035754_Initial-5.cs
 create mode 100644 API/Migrations/library/20250515120732_Initial.Designer.cs
 create mode 100644 API/Migrations/library/20250515120732_Initial.cs
 create mode 100644 API/Migrations/library/LibraryContextModelSnapshot.cs
 create mode 100644 API/Migrations/notifications/20250515120746_Initial.Designer.cs
 create mode 100644 API/Migrations/notifications/20250515120746_Initial.cs
 create mode 100644 API/Migrations/notifications/NotificationsContextModelSnapshot.cs
 rename API/Migrations/{20250509035754_Initial-5.Designer.cs => pgsql/20250515120724_Initial-1.Designer.cs} (86%)
 rename API/Migrations/{20250509033915_Initial.cs => pgsql/20250515120724_Initial-1.cs} (84%)
 rename API/Migrations/{ => pgsql}/PgsqlContextModelSnapshot.cs (86%)

diff --git a/API/Migrations/20250509033915_Initial.Designer.cs b/API/Migrations/20250509033915_Initial.Designer.cs
deleted file mode 100644
index 0979c82..0000000
--- a/API/Migrations/20250509033915_Initial.Designer.cs
+++ /dev/null
@@ -1,786 +0,0 @@
-// <auto-generated />
-using System;
-using System.Collections.Generic;
-using API.Schema;
-using Microsoft.EntityFrameworkCore;
-using Microsoft.EntityFrameworkCore.Infrastructure;
-using Microsoft.EntityFrameworkCore.Migrations;
-using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
-using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
-
-#nullable disable
-
-namespace API.Migrations
-{
-    [DbContext(typeof(PgsqlContext))]
-    [Migration("20250509033915_Initial")]
-    partial class Initial
-    {
-        /// <inheritdoc />
-        protected override void BuildTargetModel(ModelBuilder modelBuilder)
-        {
-#pragma warning disable 612, 618
-            modelBuilder
-                .HasAnnotation("ProductVersion", "9.0.3")
-                .HasAnnotation("Relational:MaxIdentifierLength", 63);
-
-            NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "hstore");
-            NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
-
-            modelBuilder.Entity("API.Schema.Author", b =>
-                {
-                    b.Property<string>("AuthorId")
-                        .HasMaxLength(64)
-                        .HasColumnType("character varying(64)");
-
-                    b.Property<string>("AuthorName")
-                        .IsRequired()
-                        .HasMaxLength(128)
-                        .HasColumnType("character varying(128)");
-
-                    b.HasKey("AuthorId");
-
-                    b.ToTable("Authors");
-                });
-
-            modelBuilder.Entity("API.Schema.Chapter", b =>
-                {
-                    b.Property<string>("ChapterId")
-                        .HasMaxLength(64)
-                        .HasColumnType("character varying(64)");
-
-                    b.Property<string>("ChapterNumber")
-                        .IsRequired()
-                        .HasMaxLength(10)
-                        .HasColumnType("character varying(10)");
-
-                    b.Property<bool>("Downloaded")
-                        .HasColumnType("boolean");
-
-                    b.Property<string>("FileName")
-                        .IsRequired()
-                        .HasMaxLength(256)
-                        .HasColumnType("character varying(256)");
-
-                    b.Property<string>("ParentMangaId")
-                        .IsRequired()
-                        .HasColumnType("character varying(64)");
-
-                    b.Property<string>("Title")
-                        .HasMaxLength(256)
-                        .HasColumnType("character varying(256)");
-
-                    b.Property<string>("Url")
-                        .IsRequired()
-                        .HasMaxLength(2048)
-                        .HasColumnType("character varying(2048)");
-
-                    b.Property<int?>("VolumeNumber")
-                        .HasColumnType("integer");
-
-                    b.HasKey("ChapterId");
-
-                    b.HasIndex("ParentMangaId");
-
-                    b.ToTable("Chapters");
-                });
-
-            modelBuilder.Entity("API.Schema.Jobs.Job", b =>
-                {
-                    b.Property<string>("JobId")
-                        .HasMaxLength(64)
-                        .HasColumnType("character varying(64)");
-
-                    b.Property<bool>("Enabled")
-                        .HasColumnType("boolean");
-
-                    b.Property<byte>("JobType")
-                        .HasColumnType("smallint");
-
-                    b.Property<DateTime>("LastExecution")
-                        .HasColumnType("timestamp with time zone");
-
-                    b.Property<string>("ParentJobId")
-                        .IsRequired()
-                        .HasMaxLength(64)
-                        .HasColumnType("character varying(64)");
-
-                    b.Property<decimal>("RecurrenceMs")
-                        .HasColumnType("numeric(20,0)");
-
-                    b.Property<byte>("state")
-                        .HasColumnType("smallint");
-
-                    b.HasKey("JobId");
-
-                    b.HasIndex("ParentJobId");
-
-                    b.ToTable("Jobs");
-
-                    b.HasDiscriminator<byte>("JobType");
-
-                    b.UseTphMappingStrategy();
-                });
-
-            modelBuilder.Entity("API.Schema.LibraryConnectors.LibraryConnector", b =>
-                {
-                    b.Property<string>("LibraryConnectorId")
-                        .HasMaxLength(64)
-                        .HasColumnType("character varying(64)");
-
-                    b.Property<string>("Auth")
-                        .IsRequired()
-                        .HasMaxLength(256)
-                        .HasColumnType("character varying(256)");
-
-                    b.Property<string>("BaseUrl")
-                        .IsRequired()
-                        .HasMaxLength(256)
-                        .HasColumnType("character varying(256)");
-
-                    b.Property<byte>("LibraryType")
-                        .HasColumnType("smallint");
-
-                    b.HasKey("LibraryConnectorId");
-
-                    b.ToTable("LibraryConnectors");
-
-                    b.HasDiscriminator<byte>("LibraryType");
-
-                    b.UseTphMappingStrategy();
-                });
-
-            modelBuilder.Entity("API.Schema.LocalLibrary", b =>
-                {
-                    b.Property<string>("LocalLibraryId")
-                        .HasMaxLength(64)
-                        .HasColumnType("character varying(64)");
-
-                    b.Property<string>("BasePath")
-                        .IsRequired()
-                        .HasMaxLength(256)
-                        .HasColumnType("character varying(256)");
-
-                    b.Property<string>("LibraryName")
-                        .IsRequired()
-                        .HasMaxLength(512)
-                        .HasColumnType("character varying(512)");
-
-                    b.HasKey("LocalLibraryId");
-
-                    b.ToTable("LocalLibraries");
-                });
-
-            modelBuilder.Entity("API.Schema.Manga", b =>
-                {
-                    b.Property<string>("MangaId")
-                        .HasMaxLength(64)
-                        .HasColumnType("character varying(64)");
-
-                    b.Property<string>("CoverFileNameInCache")
-                        .HasMaxLength(512)
-                        .HasColumnType("character varying(512)");
-
-                    b.Property<string>("CoverUrl")
-                        .IsRequired()
-                        .HasMaxLength(512)
-                        .HasColumnType("character varying(512)");
-
-                    b.Property<string>("Description")
-                        .IsRequired()
-                        .HasColumnType("text");
-
-                    b.Property<string>("DirectoryName")
-                        .IsRequired()
-                        .HasMaxLength(1024)
-                        .HasColumnType("character varying(1024)");
-
-                    b.Property<string>("IdOnConnectorSite")
-                        .IsRequired()
-                        .HasMaxLength(256)
-                        .HasColumnType("character varying(256)");
-
-                    b.Property<float>("IgnoreChaptersBefore")
-                        .HasColumnType("real");
-
-                    b.Property<string>("LibraryId")
-                        .IsRequired()
-                        .HasMaxLength(64)
-                        .HasColumnType("character varying(64)");
-
-                    b.Property<string>("MangaConnectorName")
-                        .IsRequired()
-                        .HasMaxLength(32)
-                        .HasColumnType("character varying(32)");
-
-                    b.Property<string>("Name")
-                        .IsRequired()
-                        .HasMaxLength(512)
-                        .HasColumnType("character varying(512)");
-
-                    b.Property<string>("OriginalLanguage")
-                        .HasMaxLength(8)
-                        .HasColumnType("character varying(8)");
-
-                    b.Property<byte>("ReleaseStatus")
-                        .HasColumnType("smallint");
-
-                    b.Property<string>("WebsiteUrl")
-                        .IsRequired()
-                        .HasMaxLength(512)
-                        .HasColumnType("character varying(512)");
-
-                    b.Property<long>("Year")
-                        .HasColumnType("bigint");
-
-                    b.HasKey("MangaId");
-
-                    b.HasIndex("LibraryId");
-
-                    b.HasIndex("MangaConnectorName");
-
-                    b.ToTable("Mangas");
-                });
-
-            modelBuilder.Entity("API.Schema.MangaConnectors.MangaConnector", b =>
-                {
-                    b.Property<string>("Name")
-                        .HasMaxLength(32)
-                        .HasColumnType("character varying(32)");
-
-                    b.PrimitiveCollection<string[]>("BaseUris")
-                        .IsRequired()
-                        .HasMaxLength(256)
-                        .HasColumnType("text[]");
-
-                    b.Property<bool>("Enabled")
-                        .HasColumnType("boolean");
-
-                    b.Property<string>("IconUrl")
-                        .IsRequired()
-                        .HasMaxLength(2048)
-                        .HasColumnType("character varying(2048)");
-
-                    b.PrimitiveCollection<string[]>("SupportedLanguages")
-                        .IsRequired()
-                        .HasMaxLength(8)
-                        .HasColumnType("text[]");
-
-                    b.HasKey("Name");
-
-                    b.ToTable("MangaConnectors");
-
-                    b.HasDiscriminator<string>("Name").HasValue("MangaConnector");
-
-                    b.UseTphMappingStrategy();
-                });
-
-            modelBuilder.Entity("API.Schema.MangaTag", b =>
-                {
-                    b.Property<string>("Tag")
-                        .HasMaxLength(64)
-                        .HasColumnType("character varying(64)");
-
-                    b.HasKey("Tag");
-
-                    b.ToTable("Tags");
-                });
-
-            modelBuilder.Entity("API.Schema.Notification", b =>
-                {
-                    b.Property<string>("NotificationId")
-                        .HasMaxLength(64)
-                        .HasColumnType("character varying(64)");
-
-                    b.Property<DateTime>("Date")
-                        .HasColumnType("timestamp with time zone");
-
-                    b.Property<string>("Message")
-                        .IsRequired()
-                        .HasMaxLength(512)
-                        .HasColumnType("character varying(512)");
-
-                    b.Property<string>("Title")
-                        .IsRequired()
-                        .HasMaxLength(128)
-                        .HasColumnType("character varying(128)");
-
-                    b.Property<byte>("Urgency")
-                        .HasColumnType("smallint");
-
-                    b.HasKey("NotificationId");
-
-                    b.ToTable("Notifications");
-                });
-
-            modelBuilder.Entity("API.Schema.NotificationConnectors.NotificationConnector", b =>
-                {
-                    b.Property<string>("Name")
-                        .HasMaxLength(64)
-                        .HasColumnType("character varying(64)");
-
-                    b.Property<string>("Body")
-                        .IsRequired()
-                        .HasMaxLength(4096)
-                        .HasColumnType("character varying(4096)");
-
-                    b.Property<Dictionary<string, string>>("Headers")
-                        .IsRequired()
-                        .HasColumnType("hstore");
-
-                    b.Property<string>("HttpMethod")
-                        .IsRequired()
-                        .HasMaxLength(8)
-                        .HasColumnType("character varying(8)");
-
-                    b.Property<string>("Url")
-                        .IsRequired()
-                        .HasMaxLength(2048)
-                        .HasColumnType("character varying(2048)");
-
-                    b.HasKey("Name");
-
-                    b.ToTable("NotificationConnectors");
-                });
-
-            modelBuilder.Entity("AuthorToManga", b =>
-                {
-                    b.Property<string>("AuthorIds")
-                        .HasColumnType("character varying(64)");
-
-                    b.Property<string>("MangaIds")
-                        .HasColumnType("character varying(64)");
-
-                    b.HasKey("AuthorIds", "MangaIds");
-
-                    b.HasIndex("MangaIds");
-
-                    b.ToTable("AuthorToManga");
-                });
-
-            modelBuilder.Entity("JobJob", b =>
-                {
-                    b.Property<string>("DependsOnJobsJobId")
-                        .HasColumnType("character varying(64)");
-
-                    b.Property<string>("JobId")
-                        .HasColumnType("character varying(64)");
-
-                    b.HasKey("DependsOnJobsJobId", "JobId");
-
-                    b.HasIndex("JobId");
-
-                    b.ToTable("JobJob");
-                });
-
-            modelBuilder.Entity("MangaTagToManga", b =>
-                {
-                    b.Property<string>("MangaTagIds")
-                        .HasColumnType("character varying(64)");
-
-                    b.Property<string>("MangaIds")
-                        .HasColumnType("character varying(64)");
-
-                    b.HasKey("MangaTagIds", "MangaIds");
-
-                    b.HasIndex("MangaIds");
-
-                    b.ToTable("MangaTagToManga");
-                });
-
-            modelBuilder.Entity("API.Schema.Jobs.DownloadAvailableChaptersJob", b =>
-                {
-                    b.HasBaseType("API.Schema.Jobs.Job");
-
-                    b.Property<string>("MangaId")
-                        .IsRequired()
-                        .HasMaxLength(64)
-                        .HasColumnType("character varying(64)");
-
-                    b.HasIndex("MangaId");
-
-                    b.ToTable("Jobs", t =>
-                        {
-                            t.Property("MangaId")
-                                .HasColumnName("DownloadAvailableChaptersJob_MangaId");
-                        });
-
-                    b.HasDiscriminator().HasValue((byte)1);
-                });
-
-            modelBuilder.Entity("API.Schema.Jobs.DownloadMangaCoverJob", b =>
-                {
-                    b.HasBaseType("API.Schema.Jobs.Job");
-
-                    b.Property<string>("MangaId")
-                        .IsRequired()
-                        .HasMaxLength(64)
-                        .HasColumnType("character varying(64)");
-
-                    b.HasIndex("MangaId");
-
-                    b.HasDiscriminator().HasValue((byte)4);
-                });
-
-            modelBuilder.Entity("API.Schema.Jobs.DownloadSingleChapterJob", b =>
-                {
-                    b.HasBaseType("API.Schema.Jobs.Job");
-
-                    b.Property<string>("ChapterId")
-                        .IsRequired()
-                        .HasMaxLength(64)
-                        .HasColumnType("character varying(64)");
-
-                    b.HasIndex("ChapterId");
-
-                    b.HasDiscriminator().HasValue((byte)0);
-                });
-
-            modelBuilder.Entity("API.Schema.Jobs.MoveFileOrFolderJob", b =>
-                {
-                    b.HasBaseType("API.Schema.Jobs.Job");
-
-                    b.Property<string>("FromLocation")
-                        .IsRequired()
-                        .HasMaxLength(256)
-                        .HasColumnType("character varying(256)");
-
-                    b.Property<string>("ToLocation")
-                        .IsRequired()
-                        .HasMaxLength(256)
-                        .HasColumnType("character varying(256)");
-
-                    b.HasDiscriminator().HasValue((byte)3);
-                });
-
-            modelBuilder.Entity("API.Schema.Jobs.MoveMangaLibraryJob", b =>
-                {
-                    b.HasBaseType("API.Schema.Jobs.Job");
-
-                    b.Property<string>("MangaId")
-                        .IsRequired()
-                        .HasMaxLength(64)
-                        .HasColumnType("character varying(64)");
-
-                    b.Property<string>("ToLibraryId")
-                        .IsRequired()
-                        .HasMaxLength(64)
-                        .HasColumnType("character varying(64)");
-
-                    b.HasIndex("MangaId");
-
-                    b.HasIndex("ToLibraryId");
-
-                    b.ToTable("Jobs", t =>
-                        {
-                            t.Property("MangaId")
-                                .HasColumnName("MoveMangaLibraryJob_MangaId");
-                        });
-
-                    b.HasDiscriminator().HasValue((byte)7);
-                });
-
-            modelBuilder.Entity("API.Schema.Jobs.RetrieveChaptersJob", b =>
-                {
-                    b.HasBaseType("API.Schema.Jobs.Job");
-
-                    b.Property<string>("Language")
-                        .IsRequired()
-                        .HasMaxLength(8)
-                        .HasColumnType("character varying(8)");
-
-                    b.Property<string>("MangaId")
-                        .IsRequired()
-                        .HasMaxLength(64)
-                        .HasColumnType("character varying(64)");
-
-                    b.HasIndex("MangaId");
-
-                    b.ToTable("Jobs", t =>
-                        {
-                            t.Property("MangaId")
-                                .HasColumnName("RetrieveChaptersJob_MangaId");
-                        });
-
-                    b.HasDiscriminator().HasValue((byte)5);
-                });
-
-            modelBuilder.Entity("API.Schema.Jobs.UpdateFilesDownloadedJob", b =>
-                {
-                    b.HasBaseType("API.Schema.Jobs.Job");
-
-                    b.Property<string>("MangaId")
-                        .IsRequired()
-                        .HasMaxLength(64)
-                        .HasColumnType("character varying(64)");
-
-                    b.HasIndex("MangaId");
-
-                    b.ToTable("Jobs", t =>
-                        {
-                            t.Property("MangaId")
-                                .HasColumnName("UpdateFilesDownloadedJob_MangaId");
-                        });
-
-                    b.HasDiscriminator().HasValue((byte)6);
-                });
-
-            modelBuilder.Entity("API.Schema.LibraryConnectors.Kavita", b =>
-                {
-                    b.HasBaseType("API.Schema.LibraryConnectors.LibraryConnector");
-
-                    b.HasDiscriminator().HasValue((byte)1);
-                });
-
-            modelBuilder.Entity("API.Schema.LibraryConnectors.Komga", b =>
-                {
-                    b.HasBaseType("API.Schema.LibraryConnectors.LibraryConnector");
-
-                    b.HasDiscriminator().HasValue((byte)0);
-                });
-
-            modelBuilder.Entity("API.Schema.MangaConnectors.Global", b =>
-                {
-                    b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
-
-                    b.HasDiscriminator().HasValue("Global");
-                });
-
-            modelBuilder.Entity("API.Schema.MangaConnectors.MangaDex", b =>
-                {
-                    b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
-
-                    b.HasDiscriminator().HasValue("MangaDex");
-                });
-
-            modelBuilder.Entity("API.Schema.Chapter", b =>
-                {
-                    b.HasOne("API.Schema.Manga", "ParentManga")
-                        .WithMany("Chapters")
-                        .HasForeignKey("ParentMangaId")
-                        .OnDelete(DeleteBehavior.Cascade)
-                        .IsRequired();
-
-                    b.Navigation("ParentManga");
-                });
-
-            modelBuilder.Entity("API.Schema.Jobs.Job", b =>
-                {
-                    b.HasOne("API.Schema.Jobs.Job", "ParentJob")
-                        .WithMany()
-                        .HasForeignKey("ParentJobId")
-                        .OnDelete(DeleteBehavior.Cascade);
-
-                    b.Navigation("ParentJob");
-                });
-
-            modelBuilder.Entity("API.Schema.Manga", b =>
-                {
-                    b.HasOne("API.Schema.LocalLibrary", "Library")
-                        .WithMany()
-                        .HasForeignKey("LibraryId")
-                        .OnDelete(DeleteBehavior.SetNull)
-                        .IsRequired();
-
-                    b.HasOne("API.Schema.MangaConnectors.MangaConnector", "MangaConnector")
-                        .WithMany()
-                        .HasForeignKey("MangaConnectorName")
-                        .OnDelete(DeleteBehavior.Cascade)
-                        .IsRequired();
-
-                    b.OwnsMany("API.Schema.Link", "Links", b1 =>
-                        {
-                            b1.Property<string>("LinkId")
-                                .HasMaxLength(64)
-                                .HasColumnType("character varying(64)");
-
-                            b1.Property<string>("LinkProvider")
-                                .IsRequired()
-                                .HasMaxLength(64)
-                                .HasColumnType("character varying(64)");
-
-                            b1.Property<string>("LinkUrl")
-                                .IsRequired()
-                                .HasMaxLength(2048)
-                                .HasColumnType("character varying(2048)");
-
-                            b1.Property<string>("MangaId")
-                                .IsRequired()
-                                .HasColumnType("character varying(64)");
-
-                            b1.HasKey("LinkId");
-
-                            b1.HasIndex("MangaId");
-
-                            b1.ToTable("Links");
-
-                            b1.WithOwner()
-                                .HasForeignKey("MangaId");
-                        });
-
-                    b.OwnsMany("API.Schema.MangaAltTitle", "AltTitles", b1 =>
-                        {
-                            b1.Property<string>("AltTitleId")
-                                .HasMaxLength(64)
-                                .HasColumnType("character varying(64)");
-
-                            b1.Property<string>("Language")
-                                .IsRequired()
-                                .HasMaxLength(8)
-                                .HasColumnType("character varying(8)");
-
-                            b1.Property<string>("MangaId")
-                                .IsRequired()
-                                .HasColumnType("character varying(64)");
-
-                            b1.Property<string>("Title")
-                                .IsRequired()
-                                .HasMaxLength(256)
-                                .HasColumnType("character varying(256)");
-
-                            b1.HasKey("AltTitleId");
-
-                            b1.HasIndex("MangaId");
-
-                            b1.ToTable("AltTitles");
-
-                            b1.WithOwner()
-                                .HasForeignKey("MangaId");
-                        });
-
-                    b.Navigation("AltTitles");
-
-                    b.Navigation("Library");
-
-                    b.Navigation("Links");
-
-                    b.Navigation("MangaConnector");
-                });
-
-            modelBuilder.Entity("AuthorToManga", b =>
-                {
-                    b.HasOne("API.Schema.Author", null)
-                        .WithMany()
-                        .HasForeignKey("AuthorIds")
-                        .OnDelete(DeleteBehavior.Cascade)
-                        .IsRequired();
-
-                    b.HasOne("API.Schema.Manga", null)
-                        .WithMany()
-                        .HasForeignKey("MangaIds")
-                        .OnDelete(DeleteBehavior.Cascade)
-                        .IsRequired();
-                });
-
-            modelBuilder.Entity("JobJob", b =>
-                {
-                    b.HasOne("API.Schema.Jobs.Job", null)
-                        .WithMany()
-                        .HasForeignKey("DependsOnJobsJobId")
-                        .OnDelete(DeleteBehavior.Cascade)
-                        .IsRequired();
-
-                    b.HasOne("API.Schema.Jobs.Job", null)
-                        .WithMany()
-                        .HasForeignKey("JobId")
-                        .OnDelete(DeleteBehavior.Cascade)
-                        .IsRequired();
-                });
-
-            modelBuilder.Entity("MangaTagToManga", b =>
-                {
-                    b.HasOne("API.Schema.Manga", null)
-                        .WithMany()
-                        .HasForeignKey("MangaIds")
-                        .OnDelete(DeleteBehavior.Cascade)
-                        .IsRequired();
-
-                    b.HasOne("API.Schema.MangaTag", null)
-                        .WithMany()
-                        .HasForeignKey("MangaTagIds")
-                        .OnDelete(DeleteBehavior.Cascade)
-                        .IsRequired();
-                });
-
-            modelBuilder.Entity("API.Schema.Jobs.DownloadAvailableChaptersJob", b =>
-                {
-                    b.HasOne("API.Schema.Manga", "Manga")
-                        .WithMany()
-                        .HasForeignKey("MangaId")
-                        .OnDelete(DeleteBehavior.Cascade)
-                        .IsRequired();
-
-                    b.Navigation("Manga");
-                });
-
-            modelBuilder.Entity("API.Schema.Jobs.DownloadMangaCoverJob", b =>
-                {
-                    b.HasOne("API.Schema.Manga", "Manga")
-                        .WithMany()
-                        .HasForeignKey("MangaId")
-                        .OnDelete(DeleteBehavior.Cascade)
-                        .IsRequired();
-
-                    b.Navigation("Manga");
-                });
-
-            modelBuilder.Entity("API.Schema.Jobs.DownloadSingleChapterJob", b =>
-                {
-                    b.HasOne("API.Schema.Chapter", "Chapter")
-                        .WithMany()
-                        .HasForeignKey("ChapterId")
-                        .OnDelete(DeleteBehavior.Cascade)
-                        .IsRequired();
-
-                    b.Navigation("Chapter");
-                });
-
-            modelBuilder.Entity("API.Schema.Jobs.MoveMangaLibraryJob", b =>
-                {
-                    b.HasOne("API.Schema.Manga", "Manga")
-                        .WithMany()
-                        .HasForeignKey("MangaId")
-                        .OnDelete(DeleteBehavior.Cascade)
-                        .IsRequired();
-
-                    b.HasOne("API.Schema.LocalLibrary", "ToLibrary")
-                        .WithMany()
-                        .HasForeignKey("ToLibraryId")
-                        .OnDelete(DeleteBehavior.Cascade)
-                        .IsRequired();
-
-                    b.Navigation("Manga");
-
-                    b.Navigation("ToLibrary");
-                });
-
-            modelBuilder.Entity("API.Schema.Jobs.RetrieveChaptersJob", b =>
-                {
-                    b.HasOne("API.Schema.Manga", "Manga")
-                        .WithMany()
-                        .HasForeignKey("MangaId")
-                        .OnDelete(DeleteBehavior.Cascade)
-                        .IsRequired();
-
-                    b.Navigation("Manga");
-                });
-
-            modelBuilder.Entity("API.Schema.Jobs.UpdateFilesDownloadedJob", b =>
-                {
-                    b.HasOne("API.Schema.Manga", "Manga")
-                        .WithMany()
-                        .HasForeignKey("MangaId")
-                        .OnDelete(DeleteBehavior.Cascade)
-                        .IsRequired();
-
-                    b.Navigation("Manga");
-                });
-
-            modelBuilder.Entity("API.Schema.Manga", b =>
-                {
-                    b.Navigation("Chapters");
-                });
-#pragma warning restore 612, 618
-        }
-    }
-}
diff --git a/API/Migrations/20250509034207_Initial-2.Designer.cs b/API/Migrations/20250509034207_Initial-2.Designer.cs
deleted file mode 100644
index e73b2c8..0000000
--- a/API/Migrations/20250509034207_Initial-2.Designer.cs
+++ /dev/null
@@ -1,786 +0,0 @@
-// <auto-generated />
-using System;
-using System.Collections.Generic;
-using API.Schema;
-using Microsoft.EntityFrameworkCore;
-using Microsoft.EntityFrameworkCore.Infrastructure;
-using Microsoft.EntityFrameworkCore.Migrations;
-using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
-using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
-
-#nullable disable
-
-namespace API.Migrations
-{
-    [DbContext(typeof(PgsqlContext))]
-    [Migration("20250509034207_Initial-2")]
-    partial class Initial2
-    {
-        /// <inheritdoc />
-        protected override void BuildTargetModel(ModelBuilder modelBuilder)
-        {
-#pragma warning disable 612, 618
-            modelBuilder
-                .HasAnnotation("ProductVersion", "9.0.3")
-                .HasAnnotation("Relational:MaxIdentifierLength", 63);
-
-            NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "hstore");
-            NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
-
-            modelBuilder.Entity("API.Schema.Author", b =>
-                {
-                    b.Property<string>("AuthorId")
-                        .HasMaxLength(64)
-                        .HasColumnType("character varying(64)");
-
-                    b.Property<string>("AuthorName")
-                        .IsRequired()
-                        .HasMaxLength(128)
-                        .HasColumnType("character varying(128)");
-
-                    b.HasKey("AuthorId");
-
-                    b.ToTable("Authors");
-                });
-
-            modelBuilder.Entity("API.Schema.Chapter", b =>
-                {
-                    b.Property<string>("ChapterId")
-                        .HasMaxLength(64)
-                        .HasColumnType("character varying(64)");
-
-                    b.Property<string>("ChapterNumber")
-                        .IsRequired()
-                        .HasMaxLength(10)
-                        .HasColumnType("character varying(10)");
-
-                    b.Property<bool>("Downloaded")
-                        .HasColumnType("boolean");
-
-                    b.Property<string>("FileName")
-                        .IsRequired()
-                        .HasMaxLength(256)
-                        .HasColumnType("character varying(256)");
-
-                    b.Property<string>("ParentMangaId")
-                        .IsRequired()
-                        .HasColumnType("character varying(64)");
-
-                    b.Property<string>("Title")
-                        .HasMaxLength(256)
-                        .HasColumnType("character varying(256)");
-
-                    b.Property<string>("Url")
-                        .IsRequired()
-                        .HasMaxLength(2048)
-                        .HasColumnType("character varying(2048)");
-
-                    b.Property<int?>("VolumeNumber")
-                        .HasColumnType("integer");
-
-                    b.HasKey("ChapterId");
-
-                    b.HasIndex("ParentMangaId");
-
-                    b.ToTable("Chapters");
-                });
-
-            modelBuilder.Entity("API.Schema.Jobs.Job", b =>
-                {
-                    b.Property<string>("JobId")
-                        .HasMaxLength(64)
-                        .HasColumnType("character varying(64)");
-
-                    b.Property<bool>("Enabled")
-                        .HasColumnType("boolean");
-
-                    b.Property<byte>("JobType")
-                        .HasColumnType("smallint");
-
-                    b.Property<DateTime>("LastExecution")
-                        .HasColumnType("timestamp with time zone");
-
-                    b.Property<string>("ParentJobId")
-                        .IsRequired()
-                        .HasMaxLength(64)
-                        .HasColumnType("character varying(64)");
-
-                    b.Property<decimal>("RecurrenceMs")
-                        .HasColumnType("numeric(20,0)");
-
-                    b.Property<byte>("state")
-                        .HasColumnType("smallint");
-
-                    b.HasKey("JobId");
-
-                    b.HasIndex("ParentJobId");
-
-                    b.ToTable("Jobs");
-
-                    b.HasDiscriminator<byte>("JobType");
-
-                    b.UseTphMappingStrategy();
-                });
-
-            modelBuilder.Entity("API.Schema.LibraryConnectors.LibraryConnector", b =>
-                {
-                    b.Property<string>("LibraryConnectorId")
-                        .HasMaxLength(64)
-                        .HasColumnType("character varying(64)");
-
-                    b.Property<string>("Auth")
-                        .IsRequired()
-                        .HasMaxLength(256)
-                        .HasColumnType("character varying(256)");
-
-                    b.Property<string>("BaseUrl")
-                        .IsRequired()
-                        .HasMaxLength(256)
-                        .HasColumnType("character varying(256)");
-
-                    b.Property<byte>("LibraryType")
-                        .HasColumnType("smallint");
-
-                    b.HasKey("LibraryConnectorId");
-
-                    b.ToTable("LibraryConnectors");
-
-                    b.HasDiscriminator<byte>("LibraryType");
-
-                    b.UseTphMappingStrategy();
-                });
-
-            modelBuilder.Entity("API.Schema.LocalLibrary", b =>
-                {
-                    b.Property<string>("LocalLibraryId")
-                        .HasMaxLength(64)
-                        .HasColumnType("character varying(64)");
-
-                    b.Property<string>("BasePath")
-                        .IsRequired()
-                        .HasMaxLength(256)
-                        .HasColumnType("character varying(256)");
-
-                    b.Property<string>("LibraryName")
-                        .IsRequired()
-                        .HasMaxLength(512)
-                        .HasColumnType("character varying(512)");
-
-                    b.HasKey("LocalLibraryId");
-
-                    b.ToTable("LocalLibraries");
-                });
-
-            modelBuilder.Entity("API.Schema.Manga", b =>
-                {
-                    b.Property<string>("MangaId")
-                        .HasMaxLength(64)
-                        .HasColumnType("character varying(64)");
-
-                    b.Property<string>("CoverFileNameInCache")
-                        .HasMaxLength(512)
-                        .HasColumnType("character varying(512)");
-
-                    b.Property<string>("CoverUrl")
-                        .IsRequired()
-                        .HasMaxLength(512)
-                        .HasColumnType("character varying(512)");
-
-                    b.Property<string>("Description")
-                        .IsRequired()
-                        .HasColumnType("text");
-
-                    b.Property<string>("DirectoryName")
-                        .IsRequired()
-                        .HasMaxLength(1024)
-                        .HasColumnType("character varying(1024)");
-
-                    b.Property<string>("IdOnConnectorSite")
-                        .IsRequired()
-                        .HasMaxLength(256)
-                        .HasColumnType("character varying(256)");
-
-                    b.Property<float>("IgnoreChaptersBefore")
-                        .HasColumnType("real");
-
-                    b.Property<string>("LibraryId")
-                        .IsRequired()
-                        .HasMaxLength(64)
-                        .HasColumnType("character varying(64)");
-
-                    b.Property<string>("MangaConnectorName")
-                        .IsRequired()
-                        .HasMaxLength(32)
-                        .HasColumnType("character varying(32)");
-
-                    b.Property<string>("Name")
-                        .IsRequired()
-                        .HasMaxLength(512)
-                        .HasColumnType("character varying(512)");
-
-                    b.Property<string>("OriginalLanguage")
-                        .HasMaxLength(8)
-                        .HasColumnType("character varying(8)");
-
-                    b.Property<byte>("ReleaseStatus")
-                        .HasColumnType("smallint");
-
-                    b.Property<string>("WebsiteUrl")
-                        .IsRequired()
-                        .HasMaxLength(512)
-                        .HasColumnType("character varying(512)");
-
-                    b.Property<long>("Year")
-                        .HasColumnType("bigint");
-
-                    b.HasKey("MangaId");
-
-                    b.HasIndex("LibraryId");
-
-                    b.HasIndex("MangaConnectorName");
-
-                    b.ToTable("Mangas");
-                });
-
-            modelBuilder.Entity("API.Schema.MangaConnectors.MangaConnector", b =>
-                {
-                    b.Property<string>("Name")
-                        .HasMaxLength(32)
-                        .HasColumnType("character varying(32)");
-
-                    b.PrimitiveCollection<string[]>("BaseUris")
-                        .IsRequired()
-                        .HasMaxLength(256)
-                        .HasColumnType("text[]");
-
-                    b.Property<bool>("Enabled")
-                        .HasColumnType("boolean");
-
-                    b.Property<string>("IconUrl")
-                        .IsRequired()
-                        .HasMaxLength(2048)
-                        .HasColumnType("character varying(2048)");
-
-                    b.PrimitiveCollection<string[]>("SupportedLanguages")
-                        .IsRequired()
-                        .HasMaxLength(8)
-                        .HasColumnType("text[]");
-
-                    b.HasKey("Name");
-
-                    b.ToTable("MangaConnectors");
-
-                    b.HasDiscriminator<string>("Name").HasValue("MangaConnector");
-
-                    b.UseTphMappingStrategy();
-                });
-
-            modelBuilder.Entity("API.Schema.MangaTag", b =>
-                {
-                    b.Property<string>("Tag")
-                        .HasMaxLength(64)
-                        .HasColumnType("character varying(64)");
-
-                    b.HasKey("Tag");
-
-                    b.ToTable("Tags");
-                });
-
-            modelBuilder.Entity("API.Schema.Notification", b =>
-                {
-                    b.Property<string>("NotificationId")
-                        .HasMaxLength(64)
-                        .HasColumnType("character varying(64)");
-
-                    b.Property<DateTime>("Date")
-                        .HasColumnType("timestamp with time zone");
-
-                    b.Property<string>("Message")
-                        .IsRequired()
-                        .HasMaxLength(512)
-                        .HasColumnType("character varying(512)");
-
-                    b.Property<string>("Title")
-                        .IsRequired()
-                        .HasMaxLength(128)
-                        .HasColumnType("character varying(128)");
-
-                    b.Property<byte>("Urgency")
-                        .HasColumnType("smallint");
-
-                    b.HasKey("NotificationId");
-
-                    b.ToTable("Notifications");
-                });
-
-            modelBuilder.Entity("API.Schema.NotificationConnectors.NotificationConnector", b =>
-                {
-                    b.Property<string>("Name")
-                        .HasMaxLength(64)
-                        .HasColumnType("character varying(64)");
-
-                    b.Property<string>("Body")
-                        .IsRequired()
-                        .HasMaxLength(4096)
-                        .HasColumnType("character varying(4096)");
-
-                    b.Property<Dictionary<string, string>>("Headers")
-                        .IsRequired()
-                        .HasColumnType("hstore");
-
-                    b.Property<string>("HttpMethod")
-                        .IsRequired()
-                        .HasMaxLength(8)
-                        .HasColumnType("character varying(8)");
-
-                    b.Property<string>("Url")
-                        .IsRequired()
-                        .HasMaxLength(2048)
-                        .HasColumnType("character varying(2048)");
-
-                    b.HasKey("Name");
-
-                    b.ToTable("NotificationConnectors");
-                });
-
-            modelBuilder.Entity("AuthorToManga", b =>
-                {
-                    b.Property<string>("AuthorIds")
-                        .HasColumnType("character varying(64)");
-
-                    b.Property<string>("MangaIds")
-                        .HasColumnType("character varying(64)");
-
-                    b.HasKey("AuthorIds", "MangaIds");
-
-                    b.HasIndex("MangaIds");
-
-                    b.ToTable("AuthorToManga");
-                });
-
-            modelBuilder.Entity("JobJob", b =>
-                {
-                    b.Property<string>("DependsOnJobsJobId")
-                        .HasColumnType("character varying(64)");
-
-                    b.Property<string>("JobId")
-                        .HasColumnType("character varying(64)");
-
-                    b.HasKey("DependsOnJobsJobId", "JobId");
-
-                    b.HasIndex("JobId");
-
-                    b.ToTable("JobJob");
-                });
-
-            modelBuilder.Entity("MangaTagToManga", b =>
-                {
-                    b.Property<string>("MangaTagIds")
-                        .HasColumnType("character varying(64)");
-
-                    b.Property<string>("MangaIds")
-                        .HasColumnType("character varying(64)");
-
-                    b.HasKey("MangaTagIds", "MangaIds");
-
-                    b.HasIndex("MangaIds");
-
-                    b.ToTable("MangaTagToManga");
-                });
-
-            modelBuilder.Entity("API.Schema.Jobs.DownloadAvailableChaptersJob", b =>
-                {
-                    b.HasBaseType("API.Schema.Jobs.Job");
-
-                    b.Property<string>("MangaId")
-                        .IsRequired()
-                        .HasMaxLength(64)
-                        .HasColumnType("character varying(64)");
-
-                    b.HasIndex("MangaId");
-
-                    b.ToTable("Jobs", t =>
-                        {
-                            t.Property("MangaId")
-                                .HasColumnName("DownloadAvailableChaptersJob_MangaId");
-                        });
-
-                    b.HasDiscriminator().HasValue((byte)1);
-                });
-
-            modelBuilder.Entity("API.Schema.Jobs.DownloadMangaCoverJob", b =>
-                {
-                    b.HasBaseType("API.Schema.Jobs.Job");
-
-                    b.Property<string>("MangaId")
-                        .IsRequired()
-                        .HasMaxLength(64)
-                        .HasColumnType("character varying(64)");
-
-                    b.HasIndex("MangaId");
-
-                    b.HasDiscriminator().HasValue((byte)4);
-                });
-
-            modelBuilder.Entity("API.Schema.Jobs.DownloadSingleChapterJob", b =>
-                {
-                    b.HasBaseType("API.Schema.Jobs.Job");
-
-                    b.Property<string>("ChapterId")
-                        .IsRequired()
-                        .HasMaxLength(64)
-                        .HasColumnType("character varying(64)");
-
-                    b.HasIndex("ChapterId");
-
-                    b.HasDiscriminator().HasValue((byte)0);
-                });
-
-            modelBuilder.Entity("API.Schema.Jobs.MoveFileOrFolderJob", b =>
-                {
-                    b.HasBaseType("API.Schema.Jobs.Job");
-
-                    b.Property<string>("FromLocation")
-                        .IsRequired()
-                        .HasMaxLength(256)
-                        .HasColumnType("character varying(256)");
-
-                    b.Property<string>("ToLocation")
-                        .IsRequired()
-                        .HasMaxLength(256)
-                        .HasColumnType("character varying(256)");
-
-                    b.HasDiscriminator().HasValue((byte)3);
-                });
-
-            modelBuilder.Entity("API.Schema.Jobs.MoveMangaLibraryJob", b =>
-                {
-                    b.HasBaseType("API.Schema.Jobs.Job");
-
-                    b.Property<string>("MangaId")
-                        .IsRequired()
-                        .HasMaxLength(64)
-                        .HasColumnType("character varying(64)");
-
-                    b.Property<string>("ToLibraryId")
-                        .IsRequired()
-                        .HasMaxLength(64)
-                        .HasColumnType("character varying(64)");
-
-                    b.HasIndex("MangaId");
-
-                    b.HasIndex("ToLibraryId");
-
-                    b.ToTable("Jobs", t =>
-                        {
-                            t.Property("MangaId")
-                                .HasColumnName("MoveMangaLibraryJob_MangaId");
-                        });
-
-                    b.HasDiscriminator().HasValue((byte)7);
-                });
-
-            modelBuilder.Entity("API.Schema.Jobs.RetrieveChaptersJob", b =>
-                {
-                    b.HasBaseType("API.Schema.Jobs.Job");
-
-                    b.Property<string>("Language")
-                        .IsRequired()
-                        .HasMaxLength(8)
-                        .HasColumnType("character varying(8)");
-
-                    b.Property<string>("MangaId")
-                        .IsRequired()
-                        .HasMaxLength(64)
-                        .HasColumnType("character varying(64)");
-
-                    b.HasIndex("MangaId");
-
-                    b.ToTable("Jobs", t =>
-                        {
-                            t.Property("MangaId")
-                                .HasColumnName("RetrieveChaptersJob_MangaId");
-                        });
-
-                    b.HasDiscriminator().HasValue((byte)5);
-                });
-
-            modelBuilder.Entity("API.Schema.Jobs.UpdateFilesDownloadedJob", b =>
-                {
-                    b.HasBaseType("API.Schema.Jobs.Job");
-
-                    b.Property<string>("MangaId")
-                        .IsRequired()
-                        .HasMaxLength(64)
-                        .HasColumnType("character varying(64)");
-
-                    b.HasIndex("MangaId");
-
-                    b.ToTable("Jobs", t =>
-                        {
-                            t.Property("MangaId")
-                                .HasColumnName("UpdateFilesDownloadedJob_MangaId");
-                        });
-
-                    b.HasDiscriminator().HasValue((byte)6);
-                });
-
-            modelBuilder.Entity("API.Schema.LibraryConnectors.Kavita", b =>
-                {
-                    b.HasBaseType("API.Schema.LibraryConnectors.LibraryConnector");
-
-                    b.HasDiscriminator().HasValue((byte)1);
-                });
-
-            modelBuilder.Entity("API.Schema.LibraryConnectors.Komga", b =>
-                {
-                    b.HasBaseType("API.Schema.LibraryConnectors.LibraryConnector");
-
-                    b.HasDiscriminator().HasValue((byte)0);
-                });
-
-            modelBuilder.Entity("API.Schema.MangaConnectors.Global", b =>
-                {
-                    b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
-
-                    b.HasDiscriminator().HasValue("Global");
-                });
-
-            modelBuilder.Entity("API.Schema.MangaConnectors.MangaDex", b =>
-                {
-                    b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
-
-                    b.HasDiscriminator().HasValue("MangaDex");
-                });
-
-            modelBuilder.Entity("API.Schema.Chapter", b =>
-                {
-                    b.HasOne("API.Schema.Manga", "ParentManga")
-                        .WithMany("Chapters")
-                        .HasForeignKey("ParentMangaId")
-                        .OnDelete(DeleteBehavior.Cascade)
-                        .IsRequired();
-
-                    b.Navigation("ParentManga");
-                });
-
-            modelBuilder.Entity("API.Schema.Jobs.Job", b =>
-                {
-                    b.HasOne("API.Schema.Jobs.Job", "ParentJob")
-                        .WithMany()
-                        .HasForeignKey("ParentJobId")
-                        .OnDelete(DeleteBehavior.Cascade);
-
-                    b.Navigation("ParentJob");
-                });
-
-            modelBuilder.Entity("API.Schema.Manga", b =>
-                {
-                    b.HasOne("API.Schema.LocalLibrary", "Library")
-                        .WithMany()
-                        .HasForeignKey("LibraryId")
-                        .OnDelete(DeleteBehavior.SetNull)
-                        .IsRequired();
-
-                    b.HasOne("API.Schema.MangaConnectors.MangaConnector", "MangaConnector")
-                        .WithMany()
-                        .HasForeignKey("MangaConnectorName")
-                        .OnDelete(DeleteBehavior.Cascade)
-                        .IsRequired();
-
-                    b.OwnsMany("API.Schema.Link", "Links", b1 =>
-                        {
-                            b1.Property<string>("LinkId")
-                                .HasMaxLength(64)
-                                .HasColumnType("character varying(64)");
-
-                            b1.Property<string>("LinkProvider")
-                                .IsRequired()
-                                .HasMaxLength(64)
-                                .HasColumnType("character varying(64)");
-
-                            b1.Property<string>("LinkUrl")
-                                .IsRequired()
-                                .HasMaxLength(2048)
-                                .HasColumnType("character varying(2048)");
-
-                            b1.Property<string>("MangaId")
-                                .IsRequired()
-                                .HasColumnType("character varying(64)");
-
-                            b1.HasKey("LinkId");
-
-                            b1.HasIndex("MangaId");
-
-                            b1.ToTable("Links");
-
-                            b1.WithOwner()
-                                .HasForeignKey("MangaId");
-                        });
-
-                    b.OwnsMany("API.Schema.MangaAltTitle", "AltTitles", b1 =>
-                        {
-                            b1.Property<string>("AltTitleId")
-                                .HasMaxLength(64)
-                                .HasColumnType("character varying(64)");
-
-                            b1.Property<string>("Language")
-                                .IsRequired()
-                                .HasMaxLength(8)
-                                .HasColumnType("character varying(8)");
-
-                            b1.Property<string>("MangaId")
-                                .IsRequired()
-                                .HasColumnType("character varying(64)");
-
-                            b1.Property<string>("Title")
-                                .IsRequired()
-                                .HasMaxLength(256)
-                                .HasColumnType("character varying(256)");
-
-                            b1.HasKey("AltTitleId");
-
-                            b1.HasIndex("MangaId");
-
-                            b1.ToTable("AltTitles");
-
-                            b1.WithOwner()
-                                .HasForeignKey("MangaId");
-                        });
-
-                    b.Navigation("AltTitles");
-
-                    b.Navigation("Library");
-
-                    b.Navigation("Links");
-
-                    b.Navigation("MangaConnector");
-                });
-
-            modelBuilder.Entity("AuthorToManga", b =>
-                {
-                    b.HasOne("API.Schema.Author", null)
-                        .WithMany()
-                        .HasForeignKey("AuthorIds")
-                        .OnDelete(DeleteBehavior.Cascade)
-                        .IsRequired();
-
-                    b.HasOne("API.Schema.Manga", null)
-                        .WithMany()
-                        .HasForeignKey("MangaIds")
-                        .OnDelete(DeleteBehavior.Cascade)
-                        .IsRequired();
-                });
-
-            modelBuilder.Entity("JobJob", b =>
-                {
-                    b.HasOne("API.Schema.Jobs.Job", null)
-                        .WithMany()
-                        .HasForeignKey("DependsOnJobsJobId")
-                        .OnDelete(DeleteBehavior.Cascade)
-                        .IsRequired();
-
-                    b.HasOne("API.Schema.Jobs.Job", null)
-                        .WithMany()
-                        .HasForeignKey("JobId")
-                        .OnDelete(DeleteBehavior.Cascade)
-                        .IsRequired();
-                });
-
-            modelBuilder.Entity("MangaTagToManga", b =>
-                {
-                    b.HasOne("API.Schema.Manga", null)
-                        .WithMany()
-                        .HasForeignKey("MangaIds")
-                        .OnDelete(DeleteBehavior.Cascade)
-                        .IsRequired();
-
-                    b.HasOne("API.Schema.MangaTag", null)
-                        .WithMany()
-                        .HasForeignKey("MangaTagIds")
-                        .OnDelete(DeleteBehavior.Cascade)
-                        .IsRequired();
-                });
-
-            modelBuilder.Entity("API.Schema.Jobs.DownloadAvailableChaptersJob", b =>
-                {
-                    b.HasOne("API.Schema.Manga", "Manga")
-                        .WithMany()
-                        .HasForeignKey("MangaId")
-                        .OnDelete(DeleteBehavior.Cascade)
-                        .IsRequired();
-
-                    b.Navigation("Manga");
-                });
-
-            modelBuilder.Entity("API.Schema.Jobs.DownloadMangaCoverJob", b =>
-                {
-                    b.HasOne("API.Schema.Manga", "Manga")
-                        .WithMany()
-                        .HasForeignKey("MangaId")
-                        .OnDelete(DeleteBehavior.Cascade)
-                        .IsRequired();
-
-                    b.Navigation("Manga");
-                });
-
-            modelBuilder.Entity("API.Schema.Jobs.DownloadSingleChapterJob", b =>
-                {
-                    b.HasOne("API.Schema.Chapter", "Chapter")
-                        .WithMany()
-                        .HasForeignKey("ChapterId")
-                        .OnDelete(DeleteBehavior.Cascade)
-                        .IsRequired();
-
-                    b.Navigation("Chapter");
-                });
-
-            modelBuilder.Entity("API.Schema.Jobs.MoveMangaLibraryJob", b =>
-                {
-                    b.HasOne("API.Schema.Manga", "Manga")
-                        .WithMany()
-                        .HasForeignKey("MangaId")
-                        .OnDelete(DeleteBehavior.Cascade)
-                        .IsRequired();
-
-                    b.HasOne("API.Schema.LocalLibrary", "ToLibrary")
-                        .WithMany()
-                        .HasForeignKey("ToLibraryId")
-                        .OnDelete(DeleteBehavior.Cascade)
-                        .IsRequired();
-
-                    b.Navigation("Manga");
-
-                    b.Navigation("ToLibrary");
-                });
-
-            modelBuilder.Entity("API.Schema.Jobs.RetrieveChaptersJob", b =>
-                {
-                    b.HasOne("API.Schema.Manga", "Manga")
-                        .WithMany()
-                        .HasForeignKey("MangaId")
-                        .OnDelete(DeleteBehavior.Cascade)
-                        .IsRequired();
-
-                    b.Navigation("Manga");
-                });
-
-            modelBuilder.Entity("API.Schema.Jobs.UpdateFilesDownloadedJob", b =>
-                {
-                    b.HasOne("API.Schema.Manga", "Manga")
-                        .WithMany()
-                        .HasForeignKey("MangaId")
-                        .OnDelete(DeleteBehavior.Cascade)
-                        .IsRequired();
-
-                    b.Navigation("Manga");
-                });
-
-            modelBuilder.Entity("API.Schema.Manga", b =>
-                {
-                    b.Navigation("Chapters");
-                });
-#pragma warning restore 612, 618
-        }
-    }
-}
diff --git a/API/Migrations/20250509034207_Initial-2.cs b/API/Migrations/20250509034207_Initial-2.cs
deleted file mode 100644
index b14848f..0000000
--- a/API/Migrations/20250509034207_Initial-2.cs
+++ /dev/null
@@ -1,22 +0,0 @@
-using Microsoft.EntityFrameworkCore.Migrations;
-
-#nullable disable
-
-namespace API.Migrations
-{
-    /// <inheritdoc />
-    public partial class Initial2 : Migration
-    {
-        /// <inheritdoc />
-        protected override void Up(MigrationBuilder migrationBuilder)
-        {
-
-        }
-
-        /// <inheritdoc />
-        protected override void Down(MigrationBuilder migrationBuilder)
-        {
-
-        }
-    }
-}
diff --git a/API/Migrations/20250509035413_Initial-3.Designer.cs b/API/Migrations/20250509035413_Initial-3.Designer.cs
deleted file mode 100644
index 453ec1b..0000000
--- a/API/Migrations/20250509035413_Initial-3.Designer.cs
+++ /dev/null
@@ -1,786 +0,0 @@
-// <auto-generated />
-using System;
-using System.Collections.Generic;
-using API.Schema;
-using Microsoft.EntityFrameworkCore;
-using Microsoft.EntityFrameworkCore.Infrastructure;
-using Microsoft.EntityFrameworkCore.Migrations;
-using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
-using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
-
-#nullable disable
-
-namespace API.Migrations
-{
-    [DbContext(typeof(PgsqlContext))]
-    [Migration("20250509035413_Initial-3")]
-    partial class Initial3
-    {
-        /// <inheritdoc />
-        protected override void BuildTargetModel(ModelBuilder modelBuilder)
-        {
-#pragma warning disable 612, 618
-            modelBuilder
-                .HasAnnotation("ProductVersion", "9.0.3")
-                .HasAnnotation("Relational:MaxIdentifierLength", 63);
-
-            NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "hstore");
-            NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
-
-            modelBuilder.Entity("API.Schema.Author", b =>
-                {
-                    b.Property<string>("AuthorId")
-                        .HasMaxLength(64)
-                        .HasColumnType("character varying(64)");
-
-                    b.Property<string>("AuthorName")
-                        .IsRequired()
-                        .HasMaxLength(128)
-                        .HasColumnType("character varying(128)");
-
-                    b.HasKey("AuthorId");
-
-                    b.ToTable("Authors");
-                });
-
-            modelBuilder.Entity("API.Schema.Chapter", b =>
-                {
-                    b.Property<string>("ChapterId")
-                        .HasMaxLength(64)
-                        .HasColumnType("character varying(64)");
-
-                    b.Property<string>("ChapterNumber")
-                        .IsRequired()
-                        .HasMaxLength(10)
-                        .HasColumnType("character varying(10)");
-
-                    b.Property<bool>("Downloaded")
-                        .HasColumnType("boolean");
-
-                    b.Property<string>("FileName")
-                        .IsRequired()
-                        .HasMaxLength(256)
-                        .HasColumnType("character varying(256)");
-
-                    b.Property<string>("ParentMangaId")
-                        .IsRequired()
-                        .HasColumnType("character varying(64)");
-
-                    b.Property<string>("Title")
-                        .HasMaxLength(256)
-                        .HasColumnType("character varying(256)");
-
-                    b.Property<string>("Url")
-                        .IsRequired()
-                        .HasMaxLength(2048)
-                        .HasColumnType("character varying(2048)");
-
-                    b.Property<int?>("VolumeNumber")
-                        .HasColumnType("integer");
-
-                    b.HasKey("ChapterId");
-
-                    b.HasIndex("ParentMangaId");
-
-                    b.ToTable("Chapters");
-                });
-
-            modelBuilder.Entity("API.Schema.Jobs.Job", b =>
-                {
-                    b.Property<string>("JobId")
-                        .HasMaxLength(64)
-                        .HasColumnType("character varying(64)");
-
-                    b.Property<bool>("Enabled")
-                        .HasColumnType("boolean");
-
-                    b.Property<byte>("JobType")
-                        .HasColumnType("smallint");
-
-                    b.Property<DateTime>("LastExecution")
-                        .HasColumnType("timestamp with time zone");
-
-                    b.Property<string>("ParentJobId")
-                        .IsRequired()
-                        .HasMaxLength(64)
-                        .HasColumnType("character varying(64)");
-
-                    b.Property<decimal>("RecurrenceMs")
-                        .HasColumnType("numeric(20,0)");
-
-                    b.Property<byte>("state")
-                        .HasColumnType("smallint");
-
-                    b.HasKey("JobId");
-
-                    b.HasIndex("ParentJobId");
-
-                    b.ToTable("Jobs");
-
-                    b.HasDiscriminator<byte>("JobType");
-
-                    b.UseTphMappingStrategy();
-                });
-
-            modelBuilder.Entity("API.Schema.LibraryConnectors.LibraryConnector", b =>
-                {
-                    b.Property<string>("LibraryConnectorId")
-                        .HasMaxLength(64)
-                        .HasColumnType("character varying(64)");
-
-                    b.Property<string>("Auth")
-                        .IsRequired()
-                        .HasMaxLength(256)
-                        .HasColumnType("character varying(256)");
-
-                    b.Property<string>("BaseUrl")
-                        .IsRequired()
-                        .HasMaxLength(256)
-                        .HasColumnType("character varying(256)");
-
-                    b.Property<byte>("LibraryType")
-                        .HasColumnType("smallint");
-
-                    b.HasKey("LibraryConnectorId");
-
-                    b.ToTable("LibraryConnectors");
-
-                    b.HasDiscriminator<byte>("LibraryType");
-
-                    b.UseTphMappingStrategy();
-                });
-
-            modelBuilder.Entity("API.Schema.LocalLibrary", b =>
-                {
-                    b.Property<string>("LocalLibraryId")
-                        .HasMaxLength(64)
-                        .HasColumnType("character varying(64)");
-
-                    b.Property<string>("BasePath")
-                        .IsRequired()
-                        .HasMaxLength(256)
-                        .HasColumnType("character varying(256)");
-
-                    b.Property<string>("LibraryName")
-                        .IsRequired()
-                        .HasMaxLength(512)
-                        .HasColumnType("character varying(512)");
-
-                    b.HasKey("LocalLibraryId");
-
-                    b.ToTable("LocalLibraries");
-                });
-
-            modelBuilder.Entity("API.Schema.Manga", b =>
-                {
-                    b.Property<string>("MangaId")
-                        .HasMaxLength(64)
-                        .HasColumnType("character varying(64)");
-
-                    b.Property<string>("CoverFileNameInCache")
-                        .HasMaxLength(512)
-                        .HasColumnType("character varying(512)");
-
-                    b.Property<string>("CoverUrl")
-                        .IsRequired()
-                        .HasMaxLength(512)
-                        .HasColumnType("character varying(512)");
-
-                    b.Property<string>("Description")
-                        .IsRequired()
-                        .HasColumnType("text");
-
-                    b.Property<string>("DirectoryName")
-                        .IsRequired()
-                        .HasMaxLength(1024)
-                        .HasColumnType("character varying(1024)");
-
-                    b.Property<string>("IdOnConnectorSite")
-                        .IsRequired()
-                        .HasMaxLength(256)
-                        .HasColumnType("character varying(256)");
-
-                    b.Property<float>("IgnoreChaptersBefore")
-                        .HasColumnType("real");
-
-                    b.Property<string>("LibraryId")
-                        .IsRequired()
-                        .HasMaxLength(64)
-                        .HasColumnType("character varying(64)");
-
-                    b.Property<string>("MangaConnectorName")
-                        .IsRequired()
-                        .HasMaxLength(32)
-                        .HasColumnType("character varying(32)");
-
-                    b.Property<string>("Name")
-                        .IsRequired()
-                        .HasMaxLength(512)
-                        .HasColumnType("character varying(512)");
-
-                    b.Property<string>("OriginalLanguage")
-                        .HasMaxLength(8)
-                        .HasColumnType("character varying(8)");
-
-                    b.Property<byte>("ReleaseStatus")
-                        .HasColumnType("smallint");
-
-                    b.Property<string>("WebsiteUrl")
-                        .IsRequired()
-                        .HasMaxLength(512)
-                        .HasColumnType("character varying(512)");
-
-                    b.Property<long>("Year")
-                        .HasColumnType("bigint");
-
-                    b.HasKey("MangaId");
-
-                    b.HasIndex("LibraryId");
-
-                    b.HasIndex("MangaConnectorName");
-
-                    b.ToTable("Mangas");
-                });
-
-            modelBuilder.Entity("API.Schema.MangaConnectors.MangaConnector", b =>
-                {
-                    b.Property<string>("Name")
-                        .HasMaxLength(32)
-                        .HasColumnType("character varying(32)");
-
-                    b.PrimitiveCollection<string[]>("BaseUris")
-                        .IsRequired()
-                        .HasMaxLength(256)
-                        .HasColumnType("text[]");
-
-                    b.Property<bool>("Enabled")
-                        .HasColumnType("boolean");
-
-                    b.Property<string>("IconUrl")
-                        .IsRequired()
-                        .HasMaxLength(2048)
-                        .HasColumnType("character varying(2048)");
-
-                    b.PrimitiveCollection<string[]>("SupportedLanguages")
-                        .IsRequired()
-                        .HasMaxLength(8)
-                        .HasColumnType("text[]");
-
-                    b.HasKey("Name");
-
-                    b.ToTable("MangaConnectors");
-
-                    b.HasDiscriminator<string>("Name").HasValue("MangaConnector");
-
-                    b.UseTphMappingStrategy();
-                });
-
-            modelBuilder.Entity("API.Schema.MangaTag", b =>
-                {
-                    b.Property<string>("Tag")
-                        .HasMaxLength(64)
-                        .HasColumnType("character varying(64)");
-
-                    b.HasKey("Tag");
-
-                    b.ToTable("Tags");
-                });
-
-            modelBuilder.Entity("API.Schema.Notification", b =>
-                {
-                    b.Property<string>("NotificationId")
-                        .HasMaxLength(64)
-                        .HasColumnType("character varying(64)");
-
-                    b.Property<DateTime>("Date")
-                        .HasColumnType("timestamp with time zone");
-
-                    b.Property<string>("Message")
-                        .IsRequired()
-                        .HasMaxLength(512)
-                        .HasColumnType("character varying(512)");
-
-                    b.Property<string>("Title")
-                        .IsRequired()
-                        .HasMaxLength(128)
-                        .HasColumnType("character varying(128)");
-
-                    b.Property<byte>("Urgency")
-                        .HasColumnType("smallint");
-
-                    b.HasKey("NotificationId");
-
-                    b.ToTable("Notifications");
-                });
-
-            modelBuilder.Entity("API.Schema.NotificationConnectors.NotificationConnector", b =>
-                {
-                    b.Property<string>("Name")
-                        .HasMaxLength(64)
-                        .HasColumnType("character varying(64)");
-
-                    b.Property<string>("Body")
-                        .IsRequired()
-                        .HasMaxLength(4096)
-                        .HasColumnType("character varying(4096)");
-
-                    b.Property<Dictionary<string, string>>("Headers")
-                        .IsRequired()
-                        .HasColumnType("hstore");
-
-                    b.Property<string>("HttpMethod")
-                        .IsRequired()
-                        .HasMaxLength(8)
-                        .HasColumnType("character varying(8)");
-
-                    b.Property<string>("Url")
-                        .IsRequired()
-                        .HasMaxLength(2048)
-                        .HasColumnType("character varying(2048)");
-
-                    b.HasKey("Name");
-
-                    b.ToTable("NotificationConnectors");
-                });
-
-            modelBuilder.Entity("AuthorToManga", b =>
-                {
-                    b.Property<string>("AuthorIds")
-                        .HasColumnType("character varying(64)");
-
-                    b.Property<string>("MangaIds")
-                        .HasColumnType("character varying(64)");
-
-                    b.HasKey("AuthorIds", "MangaIds");
-
-                    b.HasIndex("MangaIds");
-
-                    b.ToTable("AuthorToManga");
-                });
-
-            modelBuilder.Entity("JobJob", b =>
-                {
-                    b.Property<string>("DependsOnJobsJobId")
-                        .HasColumnType("character varying(64)");
-
-                    b.Property<string>("JobId")
-                        .HasColumnType("character varying(64)");
-
-                    b.HasKey("DependsOnJobsJobId", "JobId");
-
-                    b.HasIndex("JobId");
-
-                    b.ToTable("JobJob");
-                });
-
-            modelBuilder.Entity("MangaTagToManga", b =>
-                {
-                    b.Property<string>("MangaTagIds")
-                        .HasColumnType("character varying(64)");
-
-                    b.Property<string>("MangaIds")
-                        .HasColumnType("character varying(64)");
-
-                    b.HasKey("MangaTagIds", "MangaIds");
-
-                    b.HasIndex("MangaIds");
-
-                    b.ToTable("MangaTagToManga");
-                });
-
-            modelBuilder.Entity("API.Schema.Jobs.DownloadAvailableChaptersJob", b =>
-                {
-                    b.HasBaseType("API.Schema.Jobs.Job");
-
-                    b.Property<string>("MangaId")
-                        .IsRequired()
-                        .HasMaxLength(64)
-                        .HasColumnType("character varying(64)");
-
-                    b.HasIndex("MangaId");
-
-                    b.ToTable("Jobs", t =>
-                        {
-                            t.Property("MangaId")
-                                .HasColumnName("DownloadAvailableChaptersJob_MangaId");
-                        });
-
-                    b.HasDiscriminator().HasValue((byte)1);
-                });
-
-            modelBuilder.Entity("API.Schema.Jobs.DownloadMangaCoverJob", b =>
-                {
-                    b.HasBaseType("API.Schema.Jobs.Job");
-
-                    b.Property<string>("MangaId")
-                        .IsRequired()
-                        .HasMaxLength(64)
-                        .HasColumnType("character varying(64)");
-
-                    b.HasIndex("MangaId");
-
-                    b.HasDiscriminator().HasValue((byte)4);
-                });
-
-            modelBuilder.Entity("API.Schema.Jobs.DownloadSingleChapterJob", b =>
-                {
-                    b.HasBaseType("API.Schema.Jobs.Job");
-
-                    b.Property<string>("ChapterId")
-                        .IsRequired()
-                        .HasMaxLength(64)
-                        .HasColumnType("character varying(64)");
-
-                    b.HasIndex("ChapterId");
-
-                    b.HasDiscriminator().HasValue((byte)0);
-                });
-
-            modelBuilder.Entity("API.Schema.Jobs.MoveFileOrFolderJob", b =>
-                {
-                    b.HasBaseType("API.Schema.Jobs.Job");
-
-                    b.Property<string>("FromLocation")
-                        .IsRequired()
-                        .HasMaxLength(256)
-                        .HasColumnType("character varying(256)");
-
-                    b.Property<string>("ToLocation")
-                        .IsRequired()
-                        .HasMaxLength(256)
-                        .HasColumnType("character varying(256)");
-
-                    b.HasDiscriminator().HasValue((byte)3);
-                });
-
-            modelBuilder.Entity("API.Schema.Jobs.MoveMangaLibraryJob", b =>
-                {
-                    b.HasBaseType("API.Schema.Jobs.Job");
-
-                    b.Property<string>("MangaId")
-                        .IsRequired()
-                        .HasMaxLength(64)
-                        .HasColumnType("character varying(64)");
-
-                    b.Property<string>("ToLibraryId")
-                        .IsRequired()
-                        .HasMaxLength(64)
-                        .HasColumnType("character varying(64)");
-
-                    b.HasIndex("MangaId");
-
-                    b.HasIndex("ToLibraryId");
-
-                    b.ToTable("Jobs", t =>
-                        {
-                            t.Property("MangaId")
-                                .HasColumnName("MoveMangaLibraryJob_MangaId");
-                        });
-
-                    b.HasDiscriminator().HasValue((byte)7);
-                });
-
-            modelBuilder.Entity("API.Schema.Jobs.RetrieveChaptersJob", b =>
-                {
-                    b.HasBaseType("API.Schema.Jobs.Job");
-
-                    b.Property<string>("Language")
-                        .IsRequired()
-                        .HasMaxLength(8)
-                        .HasColumnType("character varying(8)");
-
-                    b.Property<string>("MangaId")
-                        .IsRequired()
-                        .HasMaxLength(64)
-                        .HasColumnType("character varying(64)");
-
-                    b.HasIndex("MangaId");
-
-                    b.ToTable("Jobs", t =>
-                        {
-                            t.Property("MangaId")
-                                .HasColumnName("RetrieveChaptersJob_MangaId");
-                        });
-
-                    b.HasDiscriminator().HasValue((byte)5);
-                });
-
-            modelBuilder.Entity("API.Schema.Jobs.UpdateFilesDownloadedJob", b =>
-                {
-                    b.HasBaseType("API.Schema.Jobs.Job");
-
-                    b.Property<string>("MangaId")
-                        .IsRequired()
-                        .HasMaxLength(64)
-                        .HasColumnType("character varying(64)");
-
-                    b.HasIndex("MangaId");
-
-                    b.ToTable("Jobs", t =>
-                        {
-                            t.Property("MangaId")
-                                .HasColumnName("UpdateFilesDownloadedJob_MangaId");
-                        });
-
-                    b.HasDiscriminator().HasValue((byte)6);
-                });
-
-            modelBuilder.Entity("API.Schema.LibraryConnectors.Kavita", b =>
-                {
-                    b.HasBaseType("API.Schema.LibraryConnectors.LibraryConnector");
-
-                    b.HasDiscriminator().HasValue((byte)1);
-                });
-
-            modelBuilder.Entity("API.Schema.LibraryConnectors.Komga", b =>
-                {
-                    b.HasBaseType("API.Schema.LibraryConnectors.LibraryConnector");
-
-                    b.HasDiscriminator().HasValue((byte)0);
-                });
-
-            modelBuilder.Entity("API.Schema.MangaConnectors.Global", b =>
-                {
-                    b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
-
-                    b.HasDiscriminator().HasValue("Global");
-                });
-
-            modelBuilder.Entity("API.Schema.MangaConnectors.MangaDex", b =>
-                {
-                    b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
-
-                    b.HasDiscriminator().HasValue("MangaDex");
-                });
-
-            modelBuilder.Entity("API.Schema.Chapter", b =>
-                {
-                    b.HasOne("API.Schema.Manga", "ParentManga")
-                        .WithMany("Chapters")
-                        .HasForeignKey("ParentMangaId")
-                        .OnDelete(DeleteBehavior.Cascade)
-                        .IsRequired();
-
-                    b.Navigation("ParentManga");
-                });
-
-            modelBuilder.Entity("API.Schema.Jobs.Job", b =>
-                {
-                    b.HasOne("API.Schema.Jobs.Job", "ParentJob")
-                        .WithMany()
-                        .HasForeignKey("ParentJobId")
-                        .OnDelete(DeleteBehavior.Cascade);
-
-                    b.Navigation("ParentJob");
-                });
-
-            modelBuilder.Entity("API.Schema.Manga", b =>
-                {
-                    b.HasOne("API.Schema.LocalLibrary", "Library")
-                        .WithMany()
-                        .HasForeignKey("LibraryId")
-                        .OnDelete(DeleteBehavior.SetNull)
-                        .IsRequired();
-
-                    b.HasOne("API.Schema.MangaConnectors.MangaConnector", "MangaConnector")
-                        .WithMany()
-                        .HasForeignKey("MangaConnectorName")
-                        .OnDelete(DeleteBehavior.Cascade)
-                        .IsRequired();
-
-                    b.OwnsMany("API.Schema.Link", "Links", b1 =>
-                        {
-                            b1.Property<string>("LinkId")
-                                .HasMaxLength(64)
-                                .HasColumnType("character varying(64)");
-
-                            b1.Property<string>("LinkProvider")
-                                .IsRequired()
-                                .HasMaxLength(64)
-                                .HasColumnType("character varying(64)");
-
-                            b1.Property<string>("LinkUrl")
-                                .IsRequired()
-                                .HasMaxLength(2048)
-                                .HasColumnType("character varying(2048)");
-
-                            b1.Property<string>("MangaId")
-                                .IsRequired()
-                                .HasColumnType("character varying(64)");
-
-                            b1.HasKey("LinkId");
-
-                            b1.HasIndex("MangaId");
-
-                            b1.ToTable("Link");
-
-                            b1.WithOwner()
-                                .HasForeignKey("MangaId");
-                        });
-
-                    b.OwnsMany("API.Schema.MangaAltTitle", "AltTitles", b1 =>
-                        {
-                            b1.Property<string>("AltTitleId")
-                                .HasMaxLength(64)
-                                .HasColumnType("character varying(64)");
-
-                            b1.Property<string>("Language")
-                                .IsRequired()
-                                .HasMaxLength(8)
-                                .HasColumnType("character varying(8)");
-
-                            b1.Property<string>("MangaId")
-                                .IsRequired()
-                                .HasColumnType("character varying(64)");
-
-                            b1.Property<string>("Title")
-                                .IsRequired()
-                                .HasMaxLength(256)
-                                .HasColumnType("character varying(256)");
-
-                            b1.HasKey("AltTitleId");
-
-                            b1.HasIndex("MangaId");
-
-                            b1.ToTable("MangaAltTitle");
-
-                            b1.WithOwner()
-                                .HasForeignKey("MangaId");
-                        });
-
-                    b.Navigation("AltTitles");
-
-                    b.Navigation("Library");
-
-                    b.Navigation("Links");
-
-                    b.Navigation("MangaConnector");
-                });
-
-            modelBuilder.Entity("AuthorToManga", b =>
-                {
-                    b.HasOne("API.Schema.Author", null)
-                        .WithMany()
-                        .HasForeignKey("AuthorIds")
-                        .OnDelete(DeleteBehavior.Cascade)
-                        .IsRequired();
-
-                    b.HasOne("API.Schema.Manga", null)
-                        .WithMany()
-                        .HasForeignKey("MangaIds")
-                        .OnDelete(DeleteBehavior.Cascade)
-                        .IsRequired();
-                });
-
-            modelBuilder.Entity("JobJob", b =>
-                {
-                    b.HasOne("API.Schema.Jobs.Job", null)
-                        .WithMany()
-                        .HasForeignKey("DependsOnJobsJobId")
-                        .OnDelete(DeleteBehavior.Cascade)
-                        .IsRequired();
-
-                    b.HasOne("API.Schema.Jobs.Job", null)
-                        .WithMany()
-                        .HasForeignKey("JobId")
-                        .OnDelete(DeleteBehavior.Cascade)
-                        .IsRequired();
-                });
-
-            modelBuilder.Entity("MangaTagToManga", b =>
-                {
-                    b.HasOne("API.Schema.Manga", null)
-                        .WithMany()
-                        .HasForeignKey("MangaIds")
-                        .OnDelete(DeleteBehavior.Cascade)
-                        .IsRequired();
-
-                    b.HasOne("API.Schema.MangaTag", null)
-                        .WithMany()
-                        .HasForeignKey("MangaTagIds")
-                        .OnDelete(DeleteBehavior.Cascade)
-                        .IsRequired();
-                });
-
-            modelBuilder.Entity("API.Schema.Jobs.DownloadAvailableChaptersJob", b =>
-                {
-                    b.HasOne("API.Schema.Manga", "Manga")
-                        .WithMany()
-                        .HasForeignKey("MangaId")
-                        .OnDelete(DeleteBehavior.Cascade)
-                        .IsRequired();
-
-                    b.Navigation("Manga");
-                });
-
-            modelBuilder.Entity("API.Schema.Jobs.DownloadMangaCoverJob", b =>
-                {
-                    b.HasOne("API.Schema.Manga", "Manga")
-                        .WithMany()
-                        .HasForeignKey("MangaId")
-                        .OnDelete(DeleteBehavior.Cascade)
-                        .IsRequired();
-
-                    b.Navigation("Manga");
-                });
-
-            modelBuilder.Entity("API.Schema.Jobs.DownloadSingleChapterJob", b =>
-                {
-                    b.HasOne("API.Schema.Chapter", "Chapter")
-                        .WithMany()
-                        .HasForeignKey("ChapterId")
-                        .OnDelete(DeleteBehavior.Cascade)
-                        .IsRequired();
-
-                    b.Navigation("Chapter");
-                });
-
-            modelBuilder.Entity("API.Schema.Jobs.MoveMangaLibraryJob", b =>
-                {
-                    b.HasOne("API.Schema.Manga", "Manga")
-                        .WithMany()
-                        .HasForeignKey("MangaId")
-                        .OnDelete(DeleteBehavior.Cascade)
-                        .IsRequired();
-
-                    b.HasOne("API.Schema.LocalLibrary", "ToLibrary")
-                        .WithMany()
-                        .HasForeignKey("ToLibraryId")
-                        .OnDelete(DeleteBehavior.Cascade)
-                        .IsRequired();
-
-                    b.Navigation("Manga");
-
-                    b.Navigation("ToLibrary");
-                });
-
-            modelBuilder.Entity("API.Schema.Jobs.RetrieveChaptersJob", b =>
-                {
-                    b.HasOne("API.Schema.Manga", "Manga")
-                        .WithMany()
-                        .HasForeignKey("MangaId")
-                        .OnDelete(DeleteBehavior.Cascade)
-                        .IsRequired();
-
-                    b.Navigation("Manga");
-                });
-
-            modelBuilder.Entity("API.Schema.Jobs.UpdateFilesDownloadedJob", b =>
-                {
-                    b.HasOne("API.Schema.Manga", "Manga")
-                        .WithMany()
-                        .HasForeignKey("MangaId")
-                        .OnDelete(DeleteBehavior.Cascade)
-                        .IsRequired();
-
-                    b.Navigation("Manga");
-                });
-
-            modelBuilder.Entity("API.Schema.Manga", b =>
-                {
-                    b.Navigation("Chapters");
-                });
-#pragma warning restore 612, 618
-        }
-    }
-}
diff --git a/API/Migrations/20250509035413_Initial-3.cs b/API/Migrations/20250509035413_Initial-3.cs
deleted file mode 100644
index 1b96dac..0000000
--- a/API/Migrations/20250509035413_Initial-3.cs
+++ /dev/null
@@ -1,130 +0,0 @@
-using Microsoft.EntityFrameworkCore.Migrations;
-
-#nullable disable
-
-namespace API.Migrations
-{
-    /// <inheritdoc />
-    public partial class Initial3 : Migration
-    {
-        /// <inheritdoc />
-        protected override void Up(MigrationBuilder migrationBuilder)
-        {
-            migrationBuilder.DropTable(
-                name: "AltTitles");
-
-            migrationBuilder.DropTable(
-                name: "Links");
-
-            migrationBuilder.CreateTable(
-                name: "Link",
-                columns: table => new
-                {
-                    LinkId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
-                    LinkProvider = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
-                    LinkUrl = table.Column<string>(type: "character varying(2048)", maxLength: 2048, nullable: false),
-                    MangaId = table.Column<string>(type: "character varying(64)", nullable: false)
-                },
-                constraints: table =>
-                {
-                    table.PrimaryKey("PK_Link", x => x.LinkId);
-                    table.ForeignKey(
-                        name: "FK_Link_Mangas_MangaId",
-                        column: x => x.MangaId,
-                        principalTable: "Mangas",
-                        principalColumn: "MangaId",
-                        onDelete: ReferentialAction.Cascade);
-                });
-
-            migrationBuilder.CreateTable(
-                name: "MangaAltTitle",
-                columns: table => new
-                {
-                    AltTitleId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
-                    Language = table.Column<string>(type: "character varying(8)", maxLength: 8, nullable: false),
-                    Title = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
-                    MangaId = table.Column<string>(type: "character varying(64)", nullable: false)
-                },
-                constraints: table =>
-                {
-                    table.PrimaryKey("PK_MangaAltTitle", x => x.AltTitleId);
-                    table.ForeignKey(
-                        name: "FK_MangaAltTitle_Mangas_MangaId",
-                        column: x => x.MangaId,
-                        principalTable: "Mangas",
-                        principalColumn: "MangaId",
-                        onDelete: ReferentialAction.Cascade);
-                });
-
-            migrationBuilder.CreateIndex(
-                name: "IX_Link_MangaId",
-                table: "Link",
-                column: "MangaId");
-
-            migrationBuilder.CreateIndex(
-                name: "IX_MangaAltTitle_MangaId",
-                table: "MangaAltTitle",
-                column: "MangaId");
-        }
-
-        /// <inheritdoc />
-        protected override void Down(MigrationBuilder migrationBuilder)
-        {
-            migrationBuilder.DropTable(
-                name: "Link");
-
-            migrationBuilder.DropTable(
-                name: "MangaAltTitle");
-
-            migrationBuilder.CreateTable(
-                name: "AltTitles",
-                columns: table => new
-                {
-                    AltTitleId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
-                    Language = table.Column<string>(type: "character varying(8)", maxLength: 8, nullable: false),
-                    MangaId = table.Column<string>(type: "character varying(64)", nullable: false),
-                    Title = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false)
-                },
-                constraints: table =>
-                {
-                    table.PrimaryKey("PK_AltTitles", x => x.AltTitleId);
-                    table.ForeignKey(
-                        name: "FK_AltTitles_Mangas_MangaId",
-                        column: x => x.MangaId,
-                        principalTable: "Mangas",
-                        principalColumn: "MangaId",
-                        onDelete: ReferentialAction.Cascade);
-                });
-
-            migrationBuilder.CreateTable(
-                name: "Links",
-                columns: table => new
-                {
-                    LinkId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
-                    LinkProvider = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
-                    LinkUrl = table.Column<string>(type: "character varying(2048)", maxLength: 2048, nullable: false),
-                    MangaId = table.Column<string>(type: "character varying(64)", nullable: false)
-                },
-                constraints: table =>
-                {
-                    table.PrimaryKey("PK_Links", x => x.LinkId);
-                    table.ForeignKey(
-                        name: "FK_Links_Mangas_MangaId",
-                        column: x => x.MangaId,
-                        principalTable: "Mangas",
-                        principalColumn: "MangaId",
-                        onDelete: ReferentialAction.Cascade);
-                });
-
-            migrationBuilder.CreateIndex(
-                name: "IX_AltTitles_MangaId",
-                table: "AltTitles",
-                column: "MangaId");
-
-            migrationBuilder.CreateIndex(
-                name: "IX_Links_MangaId",
-                table: "Links",
-                column: "MangaId");
-        }
-    }
-}
diff --git a/API/Migrations/20250509035606_Initial-4.Designer.cs b/API/Migrations/20250509035606_Initial-4.Designer.cs
deleted file mode 100644
index 07c02c3..0000000
--- a/API/Migrations/20250509035606_Initial-4.Designer.cs
+++ /dev/null
@@ -1,784 +0,0 @@
-// <auto-generated />
-using System;
-using System.Collections.Generic;
-using API.Schema;
-using Microsoft.EntityFrameworkCore;
-using Microsoft.EntityFrameworkCore.Infrastructure;
-using Microsoft.EntityFrameworkCore.Migrations;
-using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
-using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
-
-#nullable disable
-
-namespace API.Migrations
-{
-    [DbContext(typeof(PgsqlContext))]
-    [Migration("20250509035606_Initial-4")]
-    partial class Initial4
-    {
-        /// <inheritdoc />
-        protected override void BuildTargetModel(ModelBuilder modelBuilder)
-        {
-#pragma warning disable 612, 618
-            modelBuilder
-                .HasAnnotation("ProductVersion", "9.0.3")
-                .HasAnnotation("Relational:MaxIdentifierLength", 63);
-
-            NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "hstore");
-            NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
-
-            modelBuilder.Entity("API.Schema.Author", b =>
-                {
-                    b.Property<string>("AuthorId")
-                        .HasMaxLength(64)
-                        .HasColumnType("character varying(64)");
-
-                    b.Property<string>("AuthorName")
-                        .IsRequired()
-                        .HasMaxLength(128)
-                        .HasColumnType("character varying(128)");
-
-                    b.HasKey("AuthorId");
-
-                    b.ToTable("Authors");
-                });
-
-            modelBuilder.Entity("API.Schema.Chapter", b =>
-                {
-                    b.Property<string>("ChapterId")
-                        .HasMaxLength(64)
-                        .HasColumnType("character varying(64)");
-
-                    b.Property<string>("ChapterNumber")
-                        .IsRequired()
-                        .HasMaxLength(10)
-                        .HasColumnType("character varying(10)");
-
-                    b.Property<bool>("Downloaded")
-                        .HasColumnType("boolean");
-
-                    b.Property<string>("FileName")
-                        .IsRequired()
-                        .HasMaxLength(256)
-                        .HasColumnType("character varying(256)");
-
-                    b.Property<string>("ParentMangaId")
-                        .IsRequired()
-                        .HasColumnType("character varying(64)");
-
-                    b.Property<string>("Title")
-                        .HasMaxLength(256)
-                        .HasColumnType("character varying(256)");
-
-                    b.Property<string>("Url")
-                        .IsRequired()
-                        .HasMaxLength(2048)
-                        .HasColumnType("character varying(2048)");
-
-                    b.Property<int?>("VolumeNumber")
-                        .HasColumnType("integer");
-
-                    b.HasKey("ChapterId");
-
-                    b.HasIndex("ParentMangaId");
-
-                    b.ToTable("Chapters");
-                });
-
-            modelBuilder.Entity("API.Schema.Jobs.Job", b =>
-                {
-                    b.Property<string>("JobId")
-                        .HasMaxLength(64)
-                        .HasColumnType("character varying(64)");
-
-                    b.Property<bool>("Enabled")
-                        .HasColumnType("boolean");
-
-                    b.Property<byte>("JobType")
-                        .HasColumnType("smallint");
-
-                    b.Property<DateTime>("LastExecution")
-                        .HasColumnType("timestamp with time zone");
-
-                    b.Property<string>("ParentJobId")
-                        .IsRequired()
-                        .HasMaxLength(64)
-                        .HasColumnType("character varying(64)");
-
-                    b.Property<decimal>("RecurrenceMs")
-                        .HasColumnType("numeric(20,0)");
-
-                    b.Property<byte>("state")
-                        .HasColumnType("smallint");
-
-                    b.HasKey("JobId");
-
-                    b.HasIndex("ParentJobId");
-
-                    b.ToTable("Jobs");
-
-                    b.HasDiscriminator<byte>("JobType");
-
-                    b.UseTphMappingStrategy();
-                });
-
-            modelBuilder.Entity("API.Schema.LibraryConnectors.LibraryConnector", b =>
-                {
-                    b.Property<string>("LibraryConnectorId")
-                        .HasMaxLength(64)
-                        .HasColumnType("character varying(64)");
-
-                    b.Property<string>("Auth")
-                        .IsRequired()
-                        .HasMaxLength(256)
-                        .HasColumnType("character varying(256)");
-
-                    b.Property<string>("BaseUrl")
-                        .IsRequired()
-                        .HasMaxLength(256)
-                        .HasColumnType("character varying(256)");
-
-                    b.Property<byte>("LibraryType")
-                        .HasColumnType("smallint");
-
-                    b.HasKey("LibraryConnectorId");
-
-                    b.ToTable("LibraryConnectors");
-
-                    b.HasDiscriminator<byte>("LibraryType");
-
-                    b.UseTphMappingStrategy();
-                });
-
-            modelBuilder.Entity("API.Schema.LocalLibrary", b =>
-                {
-                    b.Property<string>("LocalLibraryId")
-                        .HasMaxLength(64)
-                        .HasColumnType("character varying(64)");
-
-                    b.Property<string>("BasePath")
-                        .IsRequired()
-                        .HasMaxLength(256)
-                        .HasColumnType("character varying(256)");
-
-                    b.Property<string>("LibraryName")
-                        .IsRequired()
-                        .HasMaxLength(512)
-                        .HasColumnType("character varying(512)");
-
-                    b.HasKey("LocalLibraryId");
-
-                    b.ToTable("LocalLibraries");
-                });
-
-            modelBuilder.Entity("API.Schema.Manga", b =>
-                {
-                    b.Property<string>("MangaId")
-                        .HasMaxLength(64)
-                        .HasColumnType("character varying(64)");
-
-                    b.Property<string>("CoverFileNameInCache")
-                        .HasMaxLength(512)
-                        .HasColumnType("character varying(512)");
-
-                    b.Property<string>("CoverUrl")
-                        .IsRequired()
-                        .HasMaxLength(512)
-                        .HasColumnType("character varying(512)");
-
-                    b.Property<string>("Description")
-                        .IsRequired()
-                        .HasColumnType("text");
-
-                    b.Property<string>("DirectoryName")
-                        .IsRequired()
-                        .HasMaxLength(1024)
-                        .HasColumnType("character varying(1024)");
-
-                    b.Property<string>("IdOnConnectorSite")
-                        .IsRequired()
-                        .HasMaxLength(256)
-                        .HasColumnType("character varying(256)");
-
-                    b.Property<float>("IgnoreChaptersBefore")
-                        .HasColumnType("real");
-
-                    b.Property<string>("LibraryId")
-                        .HasMaxLength(64)
-                        .HasColumnType("character varying(64)");
-
-                    b.Property<string>("MangaConnectorName")
-                        .IsRequired()
-                        .HasMaxLength(32)
-                        .HasColumnType("character varying(32)");
-
-                    b.Property<string>("Name")
-                        .IsRequired()
-                        .HasMaxLength(512)
-                        .HasColumnType("character varying(512)");
-
-                    b.Property<string>("OriginalLanguage")
-                        .HasMaxLength(8)
-                        .HasColumnType("character varying(8)");
-
-                    b.Property<byte>("ReleaseStatus")
-                        .HasColumnType("smallint");
-
-                    b.Property<string>("WebsiteUrl")
-                        .IsRequired()
-                        .HasMaxLength(512)
-                        .HasColumnType("character varying(512)");
-
-                    b.Property<long>("Year")
-                        .HasColumnType("bigint");
-
-                    b.HasKey("MangaId");
-
-                    b.HasIndex("LibraryId");
-
-                    b.HasIndex("MangaConnectorName");
-
-                    b.ToTable("Mangas");
-                });
-
-            modelBuilder.Entity("API.Schema.MangaConnectors.MangaConnector", b =>
-                {
-                    b.Property<string>("Name")
-                        .HasMaxLength(32)
-                        .HasColumnType("character varying(32)");
-
-                    b.PrimitiveCollection<string[]>("BaseUris")
-                        .IsRequired()
-                        .HasMaxLength(256)
-                        .HasColumnType("text[]");
-
-                    b.Property<bool>("Enabled")
-                        .HasColumnType("boolean");
-
-                    b.Property<string>("IconUrl")
-                        .IsRequired()
-                        .HasMaxLength(2048)
-                        .HasColumnType("character varying(2048)");
-
-                    b.PrimitiveCollection<string[]>("SupportedLanguages")
-                        .IsRequired()
-                        .HasMaxLength(8)
-                        .HasColumnType("text[]");
-
-                    b.HasKey("Name");
-
-                    b.ToTable("MangaConnectors");
-
-                    b.HasDiscriminator<string>("Name").HasValue("MangaConnector");
-
-                    b.UseTphMappingStrategy();
-                });
-
-            modelBuilder.Entity("API.Schema.MangaTag", b =>
-                {
-                    b.Property<string>("Tag")
-                        .HasMaxLength(64)
-                        .HasColumnType("character varying(64)");
-
-                    b.HasKey("Tag");
-
-                    b.ToTable("Tags");
-                });
-
-            modelBuilder.Entity("API.Schema.Notification", b =>
-                {
-                    b.Property<string>("NotificationId")
-                        .HasMaxLength(64)
-                        .HasColumnType("character varying(64)");
-
-                    b.Property<DateTime>("Date")
-                        .HasColumnType("timestamp with time zone");
-
-                    b.Property<string>("Message")
-                        .IsRequired()
-                        .HasMaxLength(512)
-                        .HasColumnType("character varying(512)");
-
-                    b.Property<string>("Title")
-                        .IsRequired()
-                        .HasMaxLength(128)
-                        .HasColumnType("character varying(128)");
-
-                    b.Property<byte>("Urgency")
-                        .HasColumnType("smallint");
-
-                    b.HasKey("NotificationId");
-
-                    b.ToTable("Notifications");
-                });
-
-            modelBuilder.Entity("API.Schema.NotificationConnectors.NotificationConnector", b =>
-                {
-                    b.Property<string>("Name")
-                        .HasMaxLength(64)
-                        .HasColumnType("character varying(64)");
-
-                    b.Property<string>("Body")
-                        .IsRequired()
-                        .HasMaxLength(4096)
-                        .HasColumnType("character varying(4096)");
-
-                    b.Property<Dictionary<string, string>>("Headers")
-                        .IsRequired()
-                        .HasColumnType("hstore");
-
-                    b.Property<string>("HttpMethod")
-                        .IsRequired()
-                        .HasMaxLength(8)
-                        .HasColumnType("character varying(8)");
-
-                    b.Property<string>("Url")
-                        .IsRequired()
-                        .HasMaxLength(2048)
-                        .HasColumnType("character varying(2048)");
-
-                    b.HasKey("Name");
-
-                    b.ToTable("NotificationConnectors");
-                });
-
-            modelBuilder.Entity("AuthorToManga", b =>
-                {
-                    b.Property<string>("AuthorIds")
-                        .HasColumnType("character varying(64)");
-
-                    b.Property<string>("MangaIds")
-                        .HasColumnType("character varying(64)");
-
-                    b.HasKey("AuthorIds", "MangaIds");
-
-                    b.HasIndex("MangaIds");
-
-                    b.ToTable("AuthorToManga");
-                });
-
-            modelBuilder.Entity("JobJob", b =>
-                {
-                    b.Property<string>("DependsOnJobsJobId")
-                        .HasColumnType("character varying(64)");
-
-                    b.Property<string>("JobId")
-                        .HasColumnType("character varying(64)");
-
-                    b.HasKey("DependsOnJobsJobId", "JobId");
-
-                    b.HasIndex("JobId");
-
-                    b.ToTable("JobJob");
-                });
-
-            modelBuilder.Entity("MangaTagToManga", b =>
-                {
-                    b.Property<string>("MangaTagIds")
-                        .HasColumnType("character varying(64)");
-
-                    b.Property<string>("MangaIds")
-                        .HasColumnType("character varying(64)");
-
-                    b.HasKey("MangaTagIds", "MangaIds");
-
-                    b.HasIndex("MangaIds");
-
-                    b.ToTable("MangaTagToManga");
-                });
-
-            modelBuilder.Entity("API.Schema.Jobs.DownloadAvailableChaptersJob", b =>
-                {
-                    b.HasBaseType("API.Schema.Jobs.Job");
-
-                    b.Property<string>("MangaId")
-                        .IsRequired()
-                        .HasMaxLength(64)
-                        .HasColumnType("character varying(64)");
-
-                    b.HasIndex("MangaId");
-
-                    b.ToTable("Jobs", t =>
-                        {
-                            t.Property("MangaId")
-                                .HasColumnName("DownloadAvailableChaptersJob_MangaId");
-                        });
-
-                    b.HasDiscriminator().HasValue((byte)1);
-                });
-
-            modelBuilder.Entity("API.Schema.Jobs.DownloadMangaCoverJob", b =>
-                {
-                    b.HasBaseType("API.Schema.Jobs.Job");
-
-                    b.Property<string>("MangaId")
-                        .IsRequired()
-                        .HasMaxLength(64)
-                        .HasColumnType("character varying(64)");
-
-                    b.HasIndex("MangaId");
-
-                    b.HasDiscriminator().HasValue((byte)4);
-                });
-
-            modelBuilder.Entity("API.Schema.Jobs.DownloadSingleChapterJob", b =>
-                {
-                    b.HasBaseType("API.Schema.Jobs.Job");
-
-                    b.Property<string>("ChapterId")
-                        .IsRequired()
-                        .HasMaxLength(64)
-                        .HasColumnType("character varying(64)");
-
-                    b.HasIndex("ChapterId");
-
-                    b.HasDiscriminator().HasValue((byte)0);
-                });
-
-            modelBuilder.Entity("API.Schema.Jobs.MoveFileOrFolderJob", b =>
-                {
-                    b.HasBaseType("API.Schema.Jobs.Job");
-
-                    b.Property<string>("FromLocation")
-                        .IsRequired()
-                        .HasMaxLength(256)
-                        .HasColumnType("character varying(256)");
-
-                    b.Property<string>("ToLocation")
-                        .IsRequired()
-                        .HasMaxLength(256)
-                        .HasColumnType("character varying(256)");
-
-                    b.HasDiscriminator().HasValue((byte)3);
-                });
-
-            modelBuilder.Entity("API.Schema.Jobs.MoveMangaLibraryJob", b =>
-                {
-                    b.HasBaseType("API.Schema.Jobs.Job");
-
-                    b.Property<string>("MangaId")
-                        .IsRequired()
-                        .HasMaxLength(64)
-                        .HasColumnType("character varying(64)");
-
-                    b.Property<string>("ToLibraryId")
-                        .IsRequired()
-                        .HasMaxLength(64)
-                        .HasColumnType("character varying(64)");
-
-                    b.HasIndex("MangaId");
-
-                    b.HasIndex("ToLibraryId");
-
-                    b.ToTable("Jobs", t =>
-                        {
-                            t.Property("MangaId")
-                                .HasColumnName("MoveMangaLibraryJob_MangaId");
-                        });
-
-                    b.HasDiscriminator().HasValue((byte)7);
-                });
-
-            modelBuilder.Entity("API.Schema.Jobs.RetrieveChaptersJob", b =>
-                {
-                    b.HasBaseType("API.Schema.Jobs.Job");
-
-                    b.Property<string>("Language")
-                        .IsRequired()
-                        .HasMaxLength(8)
-                        .HasColumnType("character varying(8)");
-
-                    b.Property<string>("MangaId")
-                        .IsRequired()
-                        .HasMaxLength(64)
-                        .HasColumnType("character varying(64)");
-
-                    b.HasIndex("MangaId");
-
-                    b.ToTable("Jobs", t =>
-                        {
-                            t.Property("MangaId")
-                                .HasColumnName("RetrieveChaptersJob_MangaId");
-                        });
-
-                    b.HasDiscriminator().HasValue((byte)5);
-                });
-
-            modelBuilder.Entity("API.Schema.Jobs.UpdateFilesDownloadedJob", b =>
-                {
-                    b.HasBaseType("API.Schema.Jobs.Job");
-
-                    b.Property<string>("MangaId")
-                        .IsRequired()
-                        .HasMaxLength(64)
-                        .HasColumnType("character varying(64)");
-
-                    b.HasIndex("MangaId");
-
-                    b.ToTable("Jobs", t =>
-                        {
-                            t.Property("MangaId")
-                                .HasColumnName("UpdateFilesDownloadedJob_MangaId");
-                        });
-
-                    b.HasDiscriminator().HasValue((byte)6);
-                });
-
-            modelBuilder.Entity("API.Schema.LibraryConnectors.Kavita", b =>
-                {
-                    b.HasBaseType("API.Schema.LibraryConnectors.LibraryConnector");
-
-                    b.HasDiscriminator().HasValue((byte)1);
-                });
-
-            modelBuilder.Entity("API.Schema.LibraryConnectors.Komga", b =>
-                {
-                    b.HasBaseType("API.Schema.LibraryConnectors.LibraryConnector");
-
-                    b.HasDiscriminator().HasValue((byte)0);
-                });
-
-            modelBuilder.Entity("API.Schema.MangaConnectors.Global", b =>
-                {
-                    b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
-
-                    b.HasDiscriminator().HasValue("Global");
-                });
-
-            modelBuilder.Entity("API.Schema.MangaConnectors.MangaDex", b =>
-                {
-                    b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
-
-                    b.HasDiscriminator().HasValue("MangaDex");
-                });
-
-            modelBuilder.Entity("API.Schema.Chapter", b =>
-                {
-                    b.HasOne("API.Schema.Manga", "ParentManga")
-                        .WithMany("Chapters")
-                        .HasForeignKey("ParentMangaId")
-                        .OnDelete(DeleteBehavior.Cascade)
-                        .IsRequired();
-
-                    b.Navigation("ParentManga");
-                });
-
-            modelBuilder.Entity("API.Schema.Jobs.Job", b =>
-                {
-                    b.HasOne("API.Schema.Jobs.Job", "ParentJob")
-                        .WithMany()
-                        .HasForeignKey("ParentJobId")
-                        .OnDelete(DeleteBehavior.Cascade);
-
-                    b.Navigation("ParentJob");
-                });
-
-            modelBuilder.Entity("API.Schema.Manga", b =>
-                {
-                    b.HasOne("API.Schema.LocalLibrary", "Library")
-                        .WithMany()
-                        .HasForeignKey("LibraryId")
-                        .OnDelete(DeleteBehavior.SetNull);
-
-                    b.HasOne("API.Schema.MangaConnectors.MangaConnector", "MangaConnector")
-                        .WithMany()
-                        .HasForeignKey("MangaConnectorName")
-                        .OnDelete(DeleteBehavior.Cascade)
-                        .IsRequired();
-
-                    b.OwnsMany("API.Schema.Link", "Links", b1 =>
-                        {
-                            b1.Property<string>("LinkId")
-                                .HasMaxLength(64)
-                                .HasColumnType("character varying(64)");
-
-                            b1.Property<string>("LinkProvider")
-                                .IsRequired()
-                                .HasMaxLength(64)
-                                .HasColumnType("character varying(64)");
-
-                            b1.Property<string>("LinkUrl")
-                                .IsRequired()
-                                .HasMaxLength(2048)
-                                .HasColumnType("character varying(2048)");
-
-                            b1.Property<string>("MangaId")
-                                .IsRequired()
-                                .HasColumnType("character varying(64)");
-
-                            b1.HasKey("LinkId");
-
-                            b1.HasIndex("MangaId");
-
-                            b1.ToTable("Link");
-
-                            b1.WithOwner()
-                                .HasForeignKey("MangaId");
-                        });
-
-                    b.OwnsMany("API.Schema.MangaAltTitle", "AltTitles", b1 =>
-                        {
-                            b1.Property<string>("AltTitleId")
-                                .HasMaxLength(64)
-                                .HasColumnType("character varying(64)");
-
-                            b1.Property<string>("Language")
-                                .IsRequired()
-                                .HasMaxLength(8)
-                                .HasColumnType("character varying(8)");
-
-                            b1.Property<string>("MangaId")
-                                .IsRequired()
-                                .HasColumnType("character varying(64)");
-
-                            b1.Property<string>("Title")
-                                .IsRequired()
-                                .HasMaxLength(256)
-                                .HasColumnType("character varying(256)");
-
-                            b1.HasKey("AltTitleId");
-
-                            b1.HasIndex("MangaId");
-
-                            b1.ToTable("MangaAltTitle");
-
-                            b1.WithOwner()
-                                .HasForeignKey("MangaId");
-                        });
-
-                    b.Navigation("AltTitles");
-
-                    b.Navigation("Library");
-
-                    b.Navigation("Links");
-
-                    b.Navigation("MangaConnector");
-                });
-
-            modelBuilder.Entity("AuthorToManga", b =>
-                {
-                    b.HasOne("API.Schema.Author", null)
-                        .WithMany()
-                        .HasForeignKey("AuthorIds")
-                        .OnDelete(DeleteBehavior.Cascade)
-                        .IsRequired();
-
-                    b.HasOne("API.Schema.Manga", null)
-                        .WithMany()
-                        .HasForeignKey("MangaIds")
-                        .OnDelete(DeleteBehavior.Cascade)
-                        .IsRequired();
-                });
-
-            modelBuilder.Entity("JobJob", b =>
-                {
-                    b.HasOne("API.Schema.Jobs.Job", null)
-                        .WithMany()
-                        .HasForeignKey("DependsOnJobsJobId")
-                        .OnDelete(DeleteBehavior.Cascade)
-                        .IsRequired();
-
-                    b.HasOne("API.Schema.Jobs.Job", null)
-                        .WithMany()
-                        .HasForeignKey("JobId")
-                        .OnDelete(DeleteBehavior.Cascade)
-                        .IsRequired();
-                });
-
-            modelBuilder.Entity("MangaTagToManga", b =>
-                {
-                    b.HasOne("API.Schema.Manga", null)
-                        .WithMany()
-                        .HasForeignKey("MangaIds")
-                        .OnDelete(DeleteBehavior.Cascade)
-                        .IsRequired();
-
-                    b.HasOne("API.Schema.MangaTag", null)
-                        .WithMany()
-                        .HasForeignKey("MangaTagIds")
-                        .OnDelete(DeleteBehavior.Cascade)
-                        .IsRequired();
-                });
-
-            modelBuilder.Entity("API.Schema.Jobs.DownloadAvailableChaptersJob", b =>
-                {
-                    b.HasOne("API.Schema.Manga", "Manga")
-                        .WithMany()
-                        .HasForeignKey("MangaId")
-                        .OnDelete(DeleteBehavior.Cascade)
-                        .IsRequired();
-
-                    b.Navigation("Manga");
-                });
-
-            modelBuilder.Entity("API.Schema.Jobs.DownloadMangaCoverJob", b =>
-                {
-                    b.HasOne("API.Schema.Manga", "Manga")
-                        .WithMany()
-                        .HasForeignKey("MangaId")
-                        .OnDelete(DeleteBehavior.Cascade)
-                        .IsRequired();
-
-                    b.Navigation("Manga");
-                });
-
-            modelBuilder.Entity("API.Schema.Jobs.DownloadSingleChapterJob", b =>
-                {
-                    b.HasOne("API.Schema.Chapter", "Chapter")
-                        .WithMany()
-                        .HasForeignKey("ChapterId")
-                        .OnDelete(DeleteBehavior.Cascade)
-                        .IsRequired();
-
-                    b.Navigation("Chapter");
-                });
-
-            modelBuilder.Entity("API.Schema.Jobs.MoveMangaLibraryJob", b =>
-                {
-                    b.HasOne("API.Schema.Manga", "Manga")
-                        .WithMany()
-                        .HasForeignKey("MangaId")
-                        .OnDelete(DeleteBehavior.Cascade)
-                        .IsRequired();
-
-                    b.HasOne("API.Schema.LocalLibrary", "ToLibrary")
-                        .WithMany()
-                        .HasForeignKey("ToLibraryId")
-                        .OnDelete(DeleteBehavior.Cascade)
-                        .IsRequired();
-
-                    b.Navigation("Manga");
-
-                    b.Navigation("ToLibrary");
-                });
-
-            modelBuilder.Entity("API.Schema.Jobs.RetrieveChaptersJob", b =>
-                {
-                    b.HasOne("API.Schema.Manga", "Manga")
-                        .WithMany()
-                        .HasForeignKey("MangaId")
-                        .OnDelete(DeleteBehavior.Cascade)
-                        .IsRequired();
-
-                    b.Navigation("Manga");
-                });
-
-            modelBuilder.Entity("API.Schema.Jobs.UpdateFilesDownloadedJob", b =>
-                {
-                    b.HasOne("API.Schema.Manga", "Manga")
-                        .WithMany()
-                        .HasForeignKey("MangaId")
-                        .OnDelete(DeleteBehavior.Cascade)
-                        .IsRequired();
-
-                    b.Navigation("Manga");
-                });
-
-            modelBuilder.Entity("API.Schema.Manga", b =>
-                {
-                    b.Navigation("Chapters");
-                });
-#pragma warning restore 612, 618
-        }
-    }
-}
diff --git a/API/Migrations/20250509035606_Initial-4.cs b/API/Migrations/20250509035606_Initial-4.cs
deleted file mode 100644
index f81421f..0000000
--- a/API/Migrations/20250509035606_Initial-4.cs
+++ /dev/null
@@ -1,40 +0,0 @@
-using Microsoft.EntityFrameworkCore.Migrations;
-
-#nullable disable
-
-namespace API.Migrations
-{
-    /// <inheritdoc />
-    public partial class Initial4 : Migration
-    {
-        /// <inheritdoc />
-        protected override void Up(MigrationBuilder migrationBuilder)
-        {
-            migrationBuilder.AlterColumn<string>(
-                name: "LibraryId",
-                table: "Mangas",
-                type: "character varying(64)",
-                maxLength: 64,
-                nullable: true,
-                oldClrType: typeof(string),
-                oldType: "character varying(64)",
-                oldMaxLength: 64);
-        }
-
-        /// <inheritdoc />
-        protected override void Down(MigrationBuilder migrationBuilder)
-        {
-            migrationBuilder.AlterColumn<string>(
-                name: "LibraryId",
-                table: "Mangas",
-                type: "character varying(64)",
-                maxLength: 64,
-                nullable: false,
-                defaultValue: "",
-                oldClrType: typeof(string),
-                oldType: "character varying(64)",
-                oldMaxLength: 64,
-                oldNullable: true);
-        }
-    }
-}
diff --git a/API/Migrations/20250509035754_Initial-5.cs b/API/Migrations/20250509035754_Initial-5.cs
deleted file mode 100644
index aadc22d..0000000
--- a/API/Migrations/20250509035754_Initial-5.cs
+++ /dev/null
@@ -1,40 +0,0 @@
-using Microsoft.EntityFrameworkCore.Migrations;
-
-#nullable disable
-
-namespace API.Migrations
-{
-    /// <inheritdoc />
-    public partial class Initial5 : Migration
-    {
-        /// <inheritdoc />
-        protected override void Up(MigrationBuilder migrationBuilder)
-        {
-            migrationBuilder.AlterColumn<string>(
-                name: "ParentJobId",
-                table: "Jobs",
-                type: "character varying(64)",
-                maxLength: 64,
-                nullable: true,
-                oldClrType: typeof(string),
-                oldType: "character varying(64)",
-                oldMaxLength: 64);
-        }
-
-        /// <inheritdoc />
-        protected override void Down(MigrationBuilder migrationBuilder)
-        {
-            migrationBuilder.AlterColumn<string>(
-                name: "ParentJobId",
-                table: "Jobs",
-                type: "character varying(64)",
-                maxLength: 64,
-                nullable: false,
-                defaultValue: "",
-                oldClrType: typeof(string),
-                oldType: "character varying(64)",
-                oldMaxLength: 64,
-                oldNullable: true);
-        }
-    }
-}
diff --git a/API/Migrations/library/20250515120732_Initial.Designer.cs b/API/Migrations/library/20250515120732_Initial.Designer.cs
new file mode 100644
index 0000000..c086b4e
--- /dev/null
+++ b/API/Migrations/library/20250515120732_Initial.Designer.cs
@@ -0,0 +1,71 @@
+// <auto-generated />
+using API.Schema.Contexts;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+
+#nullable disable
+
+namespace API.Migrations.library
+{
+    [DbContext(typeof(LibraryContext))]
+    [Migration("20250515120732_Initial")]
+    partial class Initial
+    {
+        /// <inheritdoc />
+        protected override void BuildTargetModel(ModelBuilder modelBuilder)
+        {
+#pragma warning disable 612, 618
+            modelBuilder
+                .HasAnnotation("ProductVersion", "9.0.3")
+                .HasAnnotation("Relational:MaxIdentifierLength", 63);
+
+            NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
+
+            modelBuilder.Entity("API.Schema.LibraryConnectors.LibraryConnector", b =>
+                {
+                    b.Property<string>("LibraryConnectorId")
+                        .HasMaxLength(64)
+                        .HasColumnType("character varying(64)");
+
+                    b.Property<string>("Auth")
+                        .IsRequired()
+                        .HasMaxLength(256)
+                        .HasColumnType("character varying(256)");
+
+                    b.Property<string>("BaseUrl")
+                        .IsRequired()
+                        .HasMaxLength(256)
+                        .HasColumnType("character varying(256)");
+
+                    b.Property<byte>("LibraryType")
+                        .HasColumnType("smallint");
+
+                    b.HasKey("LibraryConnectorId");
+
+                    b.ToTable("LibraryConnectors");
+
+                    b.HasDiscriminator<byte>("LibraryType");
+
+                    b.UseTphMappingStrategy();
+                });
+
+            modelBuilder.Entity("API.Schema.LibraryConnectors.Kavita", b =>
+                {
+                    b.HasBaseType("API.Schema.LibraryConnectors.LibraryConnector");
+
+                    b.HasDiscriminator().HasValue((byte)1);
+                });
+
+            modelBuilder.Entity("API.Schema.LibraryConnectors.Komga", b =>
+                {
+                    b.HasBaseType("API.Schema.LibraryConnectors.LibraryConnector");
+
+                    b.HasDiscriminator().HasValue((byte)0);
+                });
+#pragma warning restore 612, 618
+        }
+    }
+}
diff --git a/API/Migrations/library/20250515120732_Initial.cs b/API/Migrations/library/20250515120732_Initial.cs
new file mode 100644
index 0000000..3ad2851
--- /dev/null
+++ b/API/Migrations/library/20250515120732_Initial.cs
@@ -0,0 +1,35 @@
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace API.Migrations.library
+{
+    /// <inheritdoc />
+    public partial class Initial : Migration
+    {
+        /// <inheritdoc />
+        protected override void Up(MigrationBuilder migrationBuilder)
+        {
+            migrationBuilder.CreateTable(
+                name: "LibraryConnectors",
+                columns: table => new
+                {
+                    LibraryConnectorId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
+                    LibraryType = table.Column<byte>(type: "smallint", nullable: false),
+                    BaseUrl = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
+                    Auth = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false)
+                },
+                constraints: table =>
+                {
+                    table.PrimaryKey("PK_LibraryConnectors", x => x.LibraryConnectorId);
+                });
+        }
+
+        /// <inheritdoc />
+        protected override void Down(MigrationBuilder migrationBuilder)
+        {
+            migrationBuilder.DropTable(
+                name: "LibraryConnectors");
+        }
+    }
+}
diff --git a/API/Migrations/library/LibraryContextModelSnapshot.cs b/API/Migrations/library/LibraryContextModelSnapshot.cs
new file mode 100644
index 0000000..d79d7f0
--- /dev/null
+++ b/API/Migrations/library/LibraryContextModelSnapshot.cs
@@ -0,0 +1,68 @@
+// <auto-generated />
+using API.Schema.Contexts;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+
+#nullable disable
+
+namespace API.Migrations.library
+{
+    [DbContext(typeof(LibraryContext))]
+    partial class LibraryContextModelSnapshot : ModelSnapshot
+    {
+        protected override void BuildModel(ModelBuilder modelBuilder)
+        {
+#pragma warning disable 612, 618
+            modelBuilder
+                .HasAnnotation("ProductVersion", "9.0.3")
+                .HasAnnotation("Relational:MaxIdentifierLength", 63);
+
+            NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
+
+            modelBuilder.Entity("API.Schema.LibraryConnectors.LibraryConnector", b =>
+                {
+                    b.Property<string>("LibraryConnectorId")
+                        .HasMaxLength(64)
+                        .HasColumnType("character varying(64)");
+
+                    b.Property<string>("Auth")
+                        .IsRequired()
+                        .HasMaxLength(256)
+                        .HasColumnType("character varying(256)");
+
+                    b.Property<string>("BaseUrl")
+                        .IsRequired()
+                        .HasMaxLength(256)
+                        .HasColumnType("character varying(256)");
+
+                    b.Property<byte>("LibraryType")
+                        .HasColumnType("smallint");
+
+                    b.HasKey("LibraryConnectorId");
+
+                    b.ToTable("LibraryConnectors");
+
+                    b.HasDiscriminator<byte>("LibraryType");
+
+                    b.UseTphMappingStrategy();
+                });
+
+            modelBuilder.Entity("API.Schema.LibraryConnectors.Kavita", b =>
+                {
+                    b.HasBaseType("API.Schema.LibraryConnectors.LibraryConnector");
+
+                    b.HasDiscriminator().HasValue((byte)1);
+                });
+
+            modelBuilder.Entity("API.Schema.LibraryConnectors.Komga", b =>
+                {
+                    b.HasBaseType("API.Schema.LibraryConnectors.LibraryConnector");
+
+                    b.HasDiscriminator().HasValue((byte)0);
+                });
+#pragma warning restore 612, 618
+        }
+    }
+}
diff --git a/API/Migrations/notifications/20250515120746_Initial.Designer.cs b/API/Migrations/notifications/20250515120746_Initial.Designer.cs
new file mode 100644
index 0000000..396d022
--- /dev/null
+++ b/API/Migrations/notifications/20250515120746_Initial.Designer.cs
@@ -0,0 +1,89 @@
+// <auto-generated />
+using System;
+using System.Collections.Generic;
+using API.Schema.Contexts;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+
+#nullable disable
+
+namespace API.Migrations.notifications
+{
+    [DbContext(typeof(NotificationsContext))]
+    [Migration("20250515120746_Initial")]
+    partial class Initial
+    {
+        /// <inheritdoc />
+        protected override void BuildTargetModel(ModelBuilder modelBuilder)
+        {
+#pragma warning disable 612, 618
+            modelBuilder
+                .HasAnnotation("ProductVersion", "9.0.3")
+                .HasAnnotation("Relational:MaxIdentifierLength", 63);
+
+            NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "hstore");
+            NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
+
+            modelBuilder.Entity("API.Schema.Notification", b =>
+                {
+                    b.Property<string>("NotificationId")
+                        .HasMaxLength(64)
+                        .HasColumnType("character varying(64)");
+
+                    b.Property<DateTime>("Date")
+                        .HasColumnType("timestamp with time zone");
+
+                    b.Property<string>("Message")
+                        .IsRequired()
+                        .HasMaxLength(512)
+                        .HasColumnType("character varying(512)");
+
+                    b.Property<string>("Title")
+                        .IsRequired()
+                        .HasMaxLength(128)
+                        .HasColumnType("character varying(128)");
+
+                    b.Property<byte>("Urgency")
+                        .HasColumnType("smallint");
+
+                    b.HasKey("NotificationId");
+
+                    b.ToTable("Notifications");
+                });
+
+            modelBuilder.Entity("API.Schema.NotificationConnectors.NotificationConnector", b =>
+                {
+                    b.Property<string>("Name")
+                        .HasMaxLength(64)
+                        .HasColumnType("character varying(64)");
+
+                    b.Property<string>("Body")
+                        .IsRequired()
+                        .HasMaxLength(4096)
+                        .HasColumnType("character varying(4096)");
+
+                    b.Property<Dictionary<string, string>>("Headers")
+                        .IsRequired()
+                        .HasColumnType("hstore");
+
+                    b.Property<string>("HttpMethod")
+                        .IsRequired()
+                        .HasMaxLength(8)
+                        .HasColumnType("character varying(8)");
+
+                    b.Property<string>("Url")
+                        .IsRequired()
+                        .HasMaxLength(2048)
+                        .HasColumnType("character varying(2048)");
+
+                    b.HasKey("Name");
+
+                    b.ToTable("NotificationConnectors");
+                });
+#pragma warning restore 612, 618
+        }
+    }
+}
diff --git a/API/Migrations/notifications/20250515120746_Initial.cs b/API/Migrations/notifications/20250515120746_Initial.cs
new file mode 100644
index 0000000..8ab8590
--- /dev/null
+++ b/API/Migrations/notifications/20250515120746_Initial.cs
@@ -0,0 +1,59 @@
+using System;
+using System.Collections.Generic;
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace API.Migrations.notifications
+{
+    /// <inheritdoc />
+    public partial class Initial : Migration
+    {
+        /// <inheritdoc />
+        protected override void Up(MigrationBuilder migrationBuilder)
+        {
+            migrationBuilder.AlterDatabase()
+                .Annotation("Npgsql:PostgresExtension:hstore", ",,");
+
+            migrationBuilder.CreateTable(
+                name: "NotificationConnectors",
+                columns: table => new
+                {
+                    Name = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
+                    Url = table.Column<string>(type: "character varying(2048)", maxLength: 2048, nullable: false),
+                    Headers = table.Column<Dictionary<string, string>>(type: "hstore", nullable: false),
+                    HttpMethod = table.Column<string>(type: "character varying(8)", maxLength: 8, nullable: false),
+                    Body = table.Column<string>(type: "character varying(4096)", maxLength: 4096, nullable: false)
+                },
+                constraints: table =>
+                {
+                    table.PrimaryKey("PK_NotificationConnectors", x => x.Name);
+                });
+
+            migrationBuilder.CreateTable(
+                name: "Notifications",
+                columns: table => new
+                {
+                    NotificationId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
+                    Urgency = table.Column<byte>(type: "smallint", nullable: false),
+                    Title = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: false),
+                    Message = table.Column<string>(type: "character varying(512)", maxLength: 512, nullable: false),
+                    Date = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
+                },
+                constraints: table =>
+                {
+                    table.PrimaryKey("PK_Notifications", x => x.NotificationId);
+                });
+        }
+
+        /// <inheritdoc />
+        protected override void Down(MigrationBuilder migrationBuilder)
+        {
+            migrationBuilder.DropTable(
+                name: "NotificationConnectors");
+
+            migrationBuilder.DropTable(
+                name: "Notifications");
+        }
+    }
+}
diff --git a/API/Migrations/notifications/NotificationsContextModelSnapshot.cs b/API/Migrations/notifications/NotificationsContextModelSnapshot.cs
new file mode 100644
index 0000000..a0f7e0a
--- /dev/null
+++ b/API/Migrations/notifications/NotificationsContextModelSnapshot.cs
@@ -0,0 +1,86 @@
+// <auto-generated />
+using System;
+using System.Collections.Generic;
+using API.Schema.Contexts;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+
+#nullable disable
+
+namespace API.Migrations.notifications
+{
+    [DbContext(typeof(NotificationsContext))]
+    partial class NotificationsContextModelSnapshot : ModelSnapshot
+    {
+        protected override void BuildModel(ModelBuilder modelBuilder)
+        {
+#pragma warning disable 612, 618
+            modelBuilder
+                .HasAnnotation("ProductVersion", "9.0.3")
+                .HasAnnotation("Relational:MaxIdentifierLength", 63);
+
+            NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "hstore");
+            NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
+
+            modelBuilder.Entity("API.Schema.Notification", b =>
+                {
+                    b.Property<string>("NotificationId")
+                        .HasMaxLength(64)
+                        .HasColumnType("character varying(64)");
+
+                    b.Property<DateTime>("Date")
+                        .HasColumnType("timestamp with time zone");
+
+                    b.Property<string>("Message")
+                        .IsRequired()
+                        .HasMaxLength(512)
+                        .HasColumnType("character varying(512)");
+
+                    b.Property<string>("Title")
+                        .IsRequired()
+                        .HasMaxLength(128)
+                        .HasColumnType("character varying(128)");
+
+                    b.Property<byte>("Urgency")
+                        .HasColumnType("smallint");
+
+                    b.HasKey("NotificationId");
+
+                    b.ToTable("Notifications");
+                });
+
+            modelBuilder.Entity("API.Schema.NotificationConnectors.NotificationConnector", b =>
+                {
+                    b.Property<string>("Name")
+                        .HasMaxLength(64)
+                        .HasColumnType("character varying(64)");
+
+                    b.Property<string>("Body")
+                        .IsRequired()
+                        .HasMaxLength(4096)
+                        .HasColumnType("character varying(4096)");
+
+                    b.Property<Dictionary<string, string>>("Headers")
+                        .IsRequired()
+                        .HasColumnType("hstore");
+
+                    b.Property<string>("HttpMethod")
+                        .IsRequired()
+                        .HasMaxLength(8)
+                        .HasColumnType("character varying(8)");
+
+                    b.Property<string>("Url")
+                        .IsRequired()
+                        .HasMaxLength(2048)
+                        .HasColumnType("character varying(2048)");
+
+                    b.HasKey("Name");
+
+                    b.ToTable("NotificationConnectors");
+                });
+#pragma warning restore 612, 618
+        }
+    }
+}
diff --git a/API/Migrations/20250509035754_Initial-5.Designer.cs b/API/Migrations/pgsql/20250515120724_Initial-1.Designer.cs
similarity index 86%
rename from API/Migrations/20250509035754_Initial-5.Designer.cs
rename to API/Migrations/pgsql/20250515120724_Initial-1.Designer.cs
index 22b2b57..9b0185b 100644
--- a/API/Migrations/20250509035754_Initial-5.Designer.cs
+++ b/API/Migrations/pgsql/20250515120724_Initial-1.Designer.cs
@@ -1,7 +1,6 @@
 // <auto-generated />
 using System;
-using System.Collections.Generic;
-using API.Schema;
+using API.Schema.Contexts;
 using Microsoft.EntityFrameworkCore;
 using Microsoft.EntityFrameworkCore.Infrastructure;
 using Microsoft.EntityFrameworkCore.Migrations;
@@ -10,11 +9,11 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
 
 #nullable disable
 
-namespace API.Migrations
+namespace API.Migrations.pgsql
 {
     [DbContext(typeof(PgsqlContext))]
-    [Migration("20250509035754_Initial-5")]
-    partial class Initial5
+    [Migration("20250515120724_Initial-1")]
+    partial class Initial1
     {
         /// <inheritdoc />
         protected override void BuildTargetModel(ModelBuilder modelBuilder)
@@ -24,7 +23,6 @@ namespace API.Migrations
                 .HasAnnotation("ProductVersion", "9.0.3")
                 .HasAnnotation("Relational:MaxIdentifierLength", 63);
 
-            NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "hstore");
             NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
 
             modelBuilder.Entity("API.Schema.Author", b =>
@@ -121,34 +119,6 @@ namespace API.Migrations
                     b.UseTphMappingStrategy();
                 });
 
-            modelBuilder.Entity("API.Schema.LibraryConnectors.LibraryConnector", b =>
-                {
-                    b.Property<string>("LibraryConnectorId")
-                        .HasMaxLength(64)
-                        .HasColumnType("character varying(64)");
-
-                    b.Property<string>("Auth")
-                        .IsRequired()
-                        .HasMaxLength(256)
-                        .HasColumnType("character varying(256)");
-
-                    b.Property<string>("BaseUrl")
-                        .IsRequired()
-                        .HasMaxLength(256)
-                        .HasColumnType("character varying(256)");
-
-                    b.Property<byte>("LibraryType")
-                        .HasColumnType("smallint");
-
-                    b.HasKey("LibraryConnectorId");
-
-                    b.ToTable("LibraryConnectors");
-
-                    b.HasDiscriminator<byte>("LibraryType");
-
-                    b.UseTphMappingStrategy();
-                });
-
             modelBuilder.Entity("API.Schema.LocalLibrary", b =>
                 {
                     b.Property<string>("LocalLibraryId")
@@ -284,63 +254,6 @@ namespace API.Migrations
                     b.ToTable("Tags");
                 });
 
-            modelBuilder.Entity("API.Schema.Notification", b =>
-                {
-                    b.Property<string>("NotificationId")
-                        .HasMaxLength(64)
-                        .HasColumnType("character varying(64)");
-
-                    b.Property<DateTime>("Date")
-                        .HasColumnType("timestamp with time zone");
-
-                    b.Property<string>("Message")
-                        .IsRequired()
-                        .HasMaxLength(512)
-                        .HasColumnType("character varying(512)");
-
-                    b.Property<string>("Title")
-                        .IsRequired()
-                        .HasMaxLength(128)
-                        .HasColumnType("character varying(128)");
-
-                    b.Property<byte>("Urgency")
-                        .HasColumnType("smallint");
-
-                    b.HasKey("NotificationId");
-
-                    b.ToTable("Notifications");
-                });
-
-            modelBuilder.Entity("API.Schema.NotificationConnectors.NotificationConnector", b =>
-                {
-                    b.Property<string>("Name")
-                        .HasMaxLength(64)
-                        .HasColumnType("character varying(64)");
-
-                    b.Property<string>("Body")
-                        .IsRequired()
-                        .HasMaxLength(4096)
-                        .HasColumnType("character varying(4096)");
-
-                    b.Property<Dictionary<string, string>>("Headers")
-                        .IsRequired()
-                        .HasColumnType("hstore");
-
-                    b.Property<string>("HttpMethod")
-                        .IsRequired()
-                        .HasMaxLength(8)
-                        .HasColumnType("character varying(8)");
-
-                    b.Property<string>("Url")
-                        .IsRequired()
-                        .HasMaxLength(2048)
-                        .HasColumnType("character varying(2048)");
-
-                    b.HasKey("Name");
-
-                    b.ToTable("NotificationConnectors");
-                });
-
             modelBuilder.Entity("AuthorToManga", b =>
                 {
                     b.Property<string>("AuthorIds")
@@ -523,20 +436,6 @@ namespace API.Migrations
                     b.HasDiscriminator().HasValue((byte)6);
                 });
 
-            modelBuilder.Entity("API.Schema.LibraryConnectors.Kavita", b =>
-                {
-                    b.HasBaseType("API.Schema.LibraryConnectors.LibraryConnector");
-
-                    b.HasDiscriminator().HasValue((byte)1);
-                });
-
-            modelBuilder.Entity("API.Schema.LibraryConnectors.Komga", b =>
-                {
-                    b.HasBaseType("API.Schema.LibraryConnectors.LibraryConnector");
-
-                    b.HasDiscriminator().HasValue((byte)0);
-                });
-
             modelBuilder.Entity("API.Schema.MangaConnectors.Global", b =>
                 {
                     b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
diff --git a/API/Migrations/20250509033915_Initial.cs b/API/Migrations/pgsql/20250515120724_Initial-1.cs
similarity index 84%
rename from API/Migrations/20250509033915_Initial.cs
rename to API/Migrations/pgsql/20250515120724_Initial-1.cs
index 509a34d..97295f0 100644
--- a/API/Migrations/20250509033915_Initial.cs
+++ b/API/Migrations/pgsql/20250515120724_Initial-1.cs
@@ -1,20 +1,16 @@
 using System;
-using System.Collections.Generic;
 using Microsoft.EntityFrameworkCore.Migrations;
 
 #nullable disable
 
-namespace API.Migrations
+namespace API.Migrations.pgsql
 {
     /// <inheritdoc />
-    public partial class Initial : Migration
+    public partial class Initial1 : Migration
     {
         /// <inheritdoc />
         protected override void Up(MigrationBuilder migrationBuilder)
         {
-            migrationBuilder.AlterDatabase()
-                .Annotation("Npgsql:PostgresExtension:hstore", ",,");
-
             migrationBuilder.CreateTable(
                 name: "Authors",
                 columns: table => new
@@ -27,20 +23,6 @@ namespace API.Migrations
                     table.PrimaryKey("PK_Authors", x => x.AuthorId);
                 });
 
-            migrationBuilder.CreateTable(
-                name: "LibraryConnectors",
-                columns: table => new
-                {
-                    LibraryConnectorId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
-                    LibraryType = table.Column<byte>(type: "smallint", nullable: false),
-                    BaseUrl = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
-                    Auth = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false)
-                },
-                constraints: table =>
-                {
-                    table.PrimaryKey("PK_LibraryConnectors", x => x.LibraryConnectorId);
-                });
-
             migrationBuilder.CreateTable(
                 name: "LocalLibraries",
                 columns: table => new
@@ -69,36 +51,6 @@ namespace API.Migrations
                     table.PrimaryKey("PK_MangaConnectors", x => x.Name);
                 });
 
-            migrationBuilder.CreateTable(
-                name: "NotificationConnectors",
-                columns: table => new
-                {
-                    Name = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
-                    Url = table.Column<string>(type: "character varying(2048)", maxLength: 2048, nullable: false),
-                    Headers = table.Column<Dictionary<string, string>>(type: "hstore", nullable: false),
-                    HttpMethod = table.Column<string>(type: "character varying(8)", maxLength: 8, nullable: false),
-                    Body = table.Column<string>(type: "character varying(4096)", maxLength: 4096, nullable: false)
-                },
-                constraints: table =>
-                {
-                    table.PrimaryKey("PK_NotificationConnectors", x => x.Name);
-                });
-
-            migrationBuilder.CreateTable(
-                name: "Notifications",
-                columns: table => new
-                {
-                    NotificationId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
-                    Urgency = table.Column<byte>(type: "smallint", nullable: false),
-                    Title = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: false),
-                    Message = table.Column<string>(type: "character varying(512)", maxLength: 512, nullable: false),
-                    Date = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
-                },
-                constraints: table =>
-                {
-                    table.PrimaryKey("PK_Notifications", x => x.NotificationId);
-                });
-
             migrationBuilder.CreateTable(
                 name: "Tags",
                 columns: table => new
@@ -121,7 +73,7 @@ namespace API.Migrations
                     WebsiteUrl = table.Column<string>(type: "character varying(512)", maxLength: 512, nullable: false),
                     CoverUrl = table.Column<string>(type: "character varying(512)", maxLength: 512, nullable: false),
                     ReleaseStatus = table.Column<byte>(type: "smallint", nullable: false),
-                    LibraryId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
+                    LibraryId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: true),
                     MangaConnectorName = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: false),
                     IgnoreChaptersBefore = table.Column<float>(type: "real", nullable: false),
                     DirectoryName = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: false),
@@ -146,26 +98,6 @@ namespace API.Migrations
                         onDelete: ReferentialAction.Cascade);
                 });
 
-            migrationBuilder.CreateTable(
-                name: "AltTitles",
-                columns: table => new
-                {
-                    AltTitleId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
-                    Language = table.Column<string>(type: "character varying(8)", maxLength: 8, nullable: false),
-                    Title = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
-                    MangaId = table.Column<string>(type: "character varying(64)", nullable: false)
-                },
-                constraints: table =>
-                {
-                    table.PrimaryKey("PK_AltTitles", x => x.AltTitleId);
-                    table.ForeignKey(
-                        name: "FK_AltTitles_Mangas_MangaId",
-                        column: x => x.MangaId,
-                        principalTable: "Mangas",
-                        principalColumn: "MangaId",
-                        onDelete: ReferentialAction.Cascade);
-                });
-
             migrationBuilder.CreateTable(
                 name: "AuthorToManga",
                 columns: table => new
@@ -215,7 +147,7 @@ namespace API.Migrations
                 });
 
             migrationBuilder.CreateTable(
-                name: "Links",
+                name: "Link",
                 columns: table => new
                 {
                     LinkId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
@@ -225,9 +157,29 @@ namespace API.Migrations
                 },
                 constraints: table =>
                 {
-                    table.PrimaryKey("PK_Links", x => x.LinkId);
+                    table.PrimaryKey("PK_Link", x => x.LinkId);
                     table.ForeignKey(
-                        name: "FK_Links_Mangas_MangaId",
+                        name: "FK_Link_Mangas_MangaId",
+                        column: x => x.MangaId,
+                        principalTable: "Mangas",
+                        principalColumn: "MangaId",
+                        onDelete: ReferentialAction.Cascade);
+                });
+
+            migrationBuilder.CreateTable(
+                name: "MangaAltTitle",
+                columns: table => new
+                {
+                    AltTitleId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
+                    Language = table.Column<string>(type: "character varying(8)", maxLength: 8, nullable: false),
+                    Title = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
+                    MangaId = table.Column<string>(type: "character varying(64)", nullable: false)
+                },
+                constraints: table =>
+                {
+                    table.PrimaryKey("PK_MangaAltTitle", x => x.AltTitleId);
+                    table.ForeignKey(
+                        name: "FK_MangaAltTitle_Mangas_MangaId",
                         column: x => x.MangaId,
                         principalTable: "Mangas",
                         principalColumn: "MangaId",
@@ -263,7 +215,7 @@ namespace API.Migrations
                 columns: table => new
                 {
                     JobId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
-                    ParentJobId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
+                    ParentJobId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: true),
                     JobType = table.Column<byte>(type: "smallint", nullable: false),
                     RecurrenceMs = table.Column<decimal>(type: "numeric(20,0)", nullable: false),
                     LastExecution = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
@@ -357,11 +309,6 @@ namespace API.Migrations
                         onDelete: ReferentialAction.Cascade);
                 });
 
-            migrationBuilder.CreateIndex(
-                name: "IX_AltTitles_MangaId",
-                table: "AltTitles",
-                column: "MangaId");
-
             migrationBuilder.CreateIndex(
                 name: "IX_AuthorToManga_MangaIds",
                 table: "AuthorToManga",
@@ -418,8 +365,13 @@ namespace API.Migrations
                 column: "UpdateFilesDownloadedJob_MangaId");
 
             migrationBuilder.CreateIndex(
-                name: "IX_Links_MangaId",
-                table: "Links",
+                name: "IX_Link_MangaId",
+                table: "Link",
+                column: "MangaId");
+
+            migrationBuilder.CreateIndex(
+                name: "IX_MangaAltTitle_MangaId",
+                table: "MangaAltTitle",
                 column: "MangaId");
 
             migrationBuilder.CreateIndex(
@@ -441,9 +393,6 @@ namespace API.Migrations
         /// <inheritdoc />
         protected override void Down(MigrationBuilder migrationBuilder)
         {
-            migrationBuilder.DropTable(
-                name: "AltTitles");
-
             migrationBuilder.DropTable(
                 name: "AuthorToManga");
 
@@ -451,20 +400,14 @@ namespace API.Migrations
                 name: "JobJob");
 
             migrationBuilder.DropTable(
-                name: "LibraryConnectors");
+                name: "Link");
 
             migrationBuilder.DropTable(
-                name: "Links");
+                name: "MangaAltTitle");
 
             migrationBuilder.DropTable(
                 name: "MangaTagToManga");
 
-            migrationBuilder.DropTable(
-                name: "NotificationConnectors");
-
-            migrationBuilder.DropTable(
-                name: "Notifications");
-
             migrationBuilder.DropTable(
                 name: "Authors");
 
diff --git a/API/Migrations/PgsqlContextModelSnapshot.cs b/API/Migrations/pgsql/PgsqlContextModelSnapshot.cs
similarity index 86%
rename from API/Migrations/PgsqlContextModelSnapshot.cs
rename to API/Migrations/pgsql/PgsqlContextModelSnapshot.cs
index a17d8c2..d7dd6c6 100644
--- a/API/Migrations/PgsqlContextModelSnapshot.cs
+++ b/API/Migrations/pgsql/PgsqlContextModelSnapshot.cs
@@ -1,7 +1,6 @@
 // <auto-generated />
 using System;
-using System.Collections.Generic;
-using API.Schema;
+using API.Schema.Contexts;
 using Microsoft.EntityFrameworkCore;
 using Microsoft.EntityFrameworkCore.Infrastructure;
 using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
@@ -9,7 +8,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
 
 #nullable disable
 
-namespace API.Migrations
+namespace API.Migrations.pgsql
 {
     [DbContext(typeof(PgsqlContext))]
     partial class PgsqlContextModelSnapshot : ModelSnapshot
@@ -21,7 +20,6 @@ namespace API.Migrations
                 .HasAnnotation("ProductVersion", "9.0.3")
                 .HasAnnotation("Relational:MaxIdentifierLength", 63);
 
-            NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "hstore");
             NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
 
             modelBuilder.Entity("API.Schema.Author", b =>
@@ -118,34 +116,6 @@ namespace API.Migrations
                     b.UseTphMappingStrategy();
                 });
 
-            modelBuilder.Entity("API.Schema.LibraryConnectors.LibraryConnector", b =>
-                {
-                    b.Property<string>("LibraryConnectorId")
-                        .HasMaxLength(64)
-                        .HasColumnType("character varying(64)");
-
-                    b.Property<string>("Auth")
-                        .IsRequired()
-                        .HasMaxLength(256)
-                        .HasColumnType("character varying(256)");
-
-                    b.Property<string>("BaseUrl")
-                        .IsRequired()
-                        .HasMaxLength(256)
-                        .HasColumnType("character varying(256)");
-
-                    b.Property<byte>("LibraryType")
-                        .HasColumnType("smallint");
-
-                    b.HasKey("LibraryConnectorId");
-
-                    b.ToTable("LibraryConnectors");
-
-                    b.HasDiscriminator<byte>("LibraryType");
-
-                    b.UseTphMappingStrategy();
-                });
-
             modelBuilder.Entity("API.Schema.LocalLibrary", b =>
                 {
                     b.Property<string>("LocalLibraryId")
@@ -281,63 +251,6 @@ namespace API.Migrations
                     b.ToTable("Tags");
                 });
 
-            modelBuilder.Entity("API.Schema.Notification", b =>
-                {
-                    b.Property<string>("NotificationId")
-                        .HasMaxLength(64)
-                        .HasColumnType("character varying(64)");
-
-                    b.Property<DateTime>("Date")
-                        .HasColumnType("timestamp with time zone");
-
-                    b.Property<string>("Message")
-                        .IsRequired()
-                        .HasMaxLength(512)
-                        .HasColumnType("character varying(512)");
-
-                    b.Property<string>("Title")
-                        .IsRequired()
-                        .HasMaxLength(128)
-                        .HasColumnType("character varying(128)");
-
-                    b.Property<byte>("Urgency")
-                        .HasColumnType("smallint");
-
-                    b.HasKey("NotificationId");
-
-                    b.ToTable("Notifications");
-                });
-
-            modelBuilder.Entity("API.Schema.NotificationConnectors.NotificationConnector", b =>
-                {
-                    b.Property<string>("Name")
-                        .HasMaxLength(64)
-                        .HasColumnType("character varying(64)");
-
-                    b.Property<string>("Body")
-                        .IsRequired()
-                        .HasMaxLength(4096)
-                        .HasColumnType("character varying(4096)");
-
-                    b.Property<Dictionary<string, string>>("Headers")
-                        .IsRequired()
-                        .HasColumnType("hstore");
-
-                    b.Property<string>("HttpMethod")
-                        .IsRequired()
-                        .HasMaxLength(8)
-                        .HasColumnType("character varying(8)");
-
-                    b.Property<string>("Url")
-                        .IsRequired()
-                        .HasMaxLength(2048)
-                        .HasColumnType("character varying(2048)");
-
-                    b.HasKey("Name");
-
-                    b.ToTable("NotificationConnectors");
-                });
-
             modelBuilder.Entity("AuthorToManga", b =>
                 {
                     b.Property<string>("AuthorIds")
@@ -520,20 +433,6 @@ namespace API.Migrations
                     b.HasDiscriminator().HasValue((byte)6);
                 });
 
-            modelBuilder.Entity("API.Schema.LibraryConnectors.Kavita", b =>
-                {
-                    b.HasBaseType("API.Schema.LibraryConnectors.LibraryConnector");
-
-                    b.HasDiscriminator().HasValue((byte)1);
-                });
-
-            modelBuilder.Entity("API.Schema.LibraryConnectors.Komga", b =>
-                {
-                    b.HasBaseType("API.Schema.LibraryConnectors.LibraryConnector");
-
-                    b.HasDiscriminator().HasValue((byte)0);
-                });
-
             modelBuilder.Entity("API.Schema.MangaConnectors.Global", b =>
                 {
                     b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");

From a1c2942208cb444b272e0102ffb42a3982fe09b2 Mon Sep 17 00:00:00 2001
From: Glax <johanna@bernloehr.eu>
Date: Fri, 16 May 2025 14:21:14 +0200
Subject: [PATCH 09/50] SearchAddMangaToContext fix

---
 API/Controllers/SearchController.cs | 20 ++++----------------
 1 file changed, 4 insertions(+), 16 deletions(-)

diff --git a/API/Controllers/SearchController.cs b/API/Controllers/SearchController.cs
index 86daa71..d4b8103 100644
--- a/API/Controllers/SearchController.cs
+++ b/API/Controllers/SearchController.cs
@@ -96,7 +96,10 @@ public class SearchController(PgsqlContext context, ILog Log) : Controller
     
     private Manga? AddMangaToContext(Manga manga)
     {
-        Manga? existing = context.Mangas.Find(manga.MangaId);
+        context.Mangas.Load();
+        context.Authors.Load();
+        context.Tags.Load();
+        context.MangaConnectors.Load();
         
         IEnumerable<MangaTag> mergedTags = manga.MangaTags.Select(mt =>
         {
@@ -112,21 +115,6 @@ public class SearchController(PgsqlContext context, ILog Log) : Controller
         });
         manga.Authors = mergedAuthors.ToList();
 
-        /*
-        IEnumerable<Link> mergedLinks = manga.Links.Select(ml =>
-        {
-            Link? inDb = context.Links.Find(ml.LinkId);
-            return inDb ?? ml;
-        });
-        manga.Links = mergedLinks.ToList();
-
-        IEnumerable<MangaAltTitle> mergedAltTitles = manga.AltTitles.Select(mat =>
-        {
-            MangaAltTitle? inDb = context.AltTitles.Find(mat.AltTitleId);
-            return inDb ?? mat;
-        });
-        manga.AltTitles = mergedAltTitles.ToList();
-*/
         try
         {
 

From 205f0a1629812045cb2e0f84c0a73bca724a2ec3 Mon Sep 17 00:00:00 2001
From: Glax <johanna@bernloehr.eu>
Date: Fri, 16 May 2025 14:23:20 +0200
Subject: [PATCH 10/50] MangaAltTitle change Id to random

---
 API/Schema/MangaAltTitle.cs | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/API/Schema/MangaAltTitle.cs b/API/Schema/MangaAltTitle.cs
index 23e5854..5775a6c 100644
--- a/API/Schema/MangaAltTitle.cs
+++ b/API/Schema/MangaAltTitle.cs
@@ -8,7 +8,7 @@ public class MangaAltTitle(string language, string title)
 {
     [StringLength(64)]
     [Required]
-    public string AltTitleId { get; init; } = TokenGen.CreateToken("AltTitle", language, title);
+    public string AltTitleId { get; init; } = TokenGen.CreateToken("AltTitle");
     [StringLength(8)]
     [Required]
     public string Language { get; init; } = language;

From 83bc3b418bbc5ecdeb2296a9ee057a6d2c0f4ad2 Mon Sep 17 00:00:00 2001
From: Glax <johanna@bernloehr.eu>
Date: Fri, 16 May 2025 14:23:33 +0200
Subject: [PATCH 11/50] Manga Year is not required (nullable)

---
 API/Schema/Manga.cs | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/API/Schema/Manga.cs b/API/Schema/Manga.cs
index 867a82f..2374da1 100644
--- a/API/Schema/Manga.cs
+++ b/API/Schema/Manga.cs
@@ -39,7 +39,7 @@ public class Manga
     [StringLength(1024)] [Required] public string DirectoryName { get; private set; }
 
     [JsonIgnore] [StringLength(512)] public string? CoverFileNameInCache { get; internal set; } = null;
-    [Required] public uint? Year { get; internal init; }
+    public uint? Year { get; internal init; }
     [StringLength(8)] public string? OriginalLanguage { get; internal init; }
 
     [JsonIgnore]

From d5d9f44a5f6e92e9a1e9763f515a870fc7012aca Mon Sep 17 00:00:00 2001
From: Glax <johanna@bernloehr.eu>
Date: Fri, 16 May 2025 14:24:18 +0200
Subject: [PATCH 12/50] Add Comick.Io
 https://github.com/C9Glax/tranga/issues/253

---
 .../20250516121442_AltTitle-Owned.Designer.cs | 688 +++++++++++++++++
 .../pgsql/20250516121442_AltTitle-Owned.cs    |  70 ++
 ...0516121725_Manga-Year-Nullable.Designer.cs | 688 +++++++++++++++++
 .../20250516121725_Manga-Year-Nullable.cs     |  36 +
 ...16122242_AltTitle-Owned-WithId.Designer.cs | 689 ++++++++++++++++++
 .../20250516122242_AltTitle-Owned-WithId.cs   |  70 ++
 .../pgsql/PgsqlContextModelSnapshot.cs        |   9 +-
 API/Program.cs                                |   1 +
 API/Schema/Contexts/PgsqlContext.cs           |   4 +-
 API/Schema/MangaConnectors/ComickIo.cs        | 246 +++++++
 10 files changed, 2498 insertions(+), 3 deletions(-)
 create mode 100644 API/Migrations/pgsql/20250516121442_AltTitle-Owned.Designer.cs
 create mode 100644 API/Migrations/pgsql/20250516121442_AltTitle-Owned.cs
 create mode 100644 API/Migrations/pgsql/20250516121725_Manga-Year-Nullable.Designer.cs
 create mode 100644 API/Migrations/pgsql/20250516121725_Manga-Year-Nullable.cs
 create mode 100644 API/Migrations/pgsql/20250516122242_AltTitle-Owned-WithId.Designer.cs
 create mode 100644 API/Migrations/pgsql/20250516122242_AltTitle-Owned-WithId.cs
 create mode 100644 API/Schema/MangaConnectors/ComickIo.cs

diff --git a/API/Migrations/pgsql/20250516121442_AltTitle-Owned.Designer.cs b/API/Migrations/pgsql/20250516121442_AltTitle-Owned.Designer.cs
new file mode 100644
index 0000000..29f52b0
--- /dev/null
+++ b/API/Migrations/pgsql/20250516121442_AltTitle-Owned.Designer.cs
@@ -0,0 +1,688 @@
+// <auto-generated />
+using System;
+using API.Schema.Contexts;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+
+#nullable disable
+
+namespace API.Migrations.pgsql
+{
+    [DbContext(typeof(PgsqlContext))]
+    [Migration("20250516121442_AltTitle-Owned")]
+    partial class AltTitleOwned
+    {
+        /// <inheritdoc />
+        protected override void BuildTargetModel(ModelBuilder modelBuilder)
+        {
+#pragma warning disable 612, 618
+            modelBuilder
+                .HasAnnotation("ProductVersion", "9.0.3")
+                .HasAnnotation("Relational:MaxIdentifierLength", 63);
+
+            NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
+
+            modelBuilder.Entity("API.Schema.Author", b =>
+                {
+                    b.Property<string>("AuthorId")
+                        .HasMaxLength(64)
+                        .HasColumnType("character varying(64)");
+
+                    b.Property<string>("AuthorName")
+                        .IsRequired()
+                        .HasMaxLength(128)
+                        .HasColumnType("character varying(128)");
+
+                    b.HasKey("AuthorId");
+
+                    b.ToTable("Authors");
+                });
+
+            modelBuilder.Entity("API.Schema.Chapter", b =>
+                {
+                    b.Property<string>("ChapterId")
+                        .HasMaxLength(64)
+                        .HasColumnType("character varying(64)");
+
+                    b.Property<string>("ChapterNumber")
+                        .IsRequired()
+                        .HasMaxLength(10)
+                        .HasColumnType("character varying(10)");
+
+                    b.Property<bool>("Downloaded")
+                        .HasColumnType("boolean");
+
+                    b.Property<string>("FileName")
+                        .IsRequired()
+                        .HasMaxLength(256)
+                        .HasColumnType("character varying(256)");
+
+                    b.Property<string>("ParentMangaId")
+                        .IsRequired()
+                        .HasColumnType("character varying(64)");
+
+                    b.Property<string>("Title")
+                        .HasMaxLength(256)
+                        .HasColumnType("character varying(256)");
+
+                    b.Property<string>("Url")
+                        .IsRequired()
+                        .HasMaxLength(2048)
+                        .HasColumnType("character varying(2048)");
+
+                    b.Property<int?>("VolumeNumber")
+                        .HasColumnType("integer");
+
+                    b.HasKey("ChapterId");
+
+                    b.HasIndex("ParentMangaId");
+
+                    b.ToTable("Chapters");
+                });
+
+            modelBuilder.Entity("API.Schema.Jobs.Job", b =>
+                {
+                    b.Property<string>("JobId")
+                        .HasMaxLength(64)
+                        .HasColumnType("character varying(64)");
+
+                    b.Property<bool>("Enabled")
+                        .HasColumnType("boolean");
+
+                    b.Property<byte>("JobType")
+                        .HasColumnType("smallint");
+
+                    b.Property<DateTime>("LastExecution")
+                        .HasColumnType("timestamp with time zone");
+
+                    b.Property<string>("ParentJobId")
+                        .HasMaxLength(64)
+                        .HasColumnType("character varying(64)");
+
+                    b.Property<decimal>("RecurrenceMs")
+                        .HasColumnType("numeric(20,0)");
+
+                    b.Property<byte>("state")
+                        .HasColumnType("smallint");
+
+                    b.HasKey("JobId");
+
+                    b.HasIndex("ParentJobId");
+
+                    b.ToTable("Jobs");
+
+                    b.HasDiscriminator<byte>("JobType");
+
+                    b.UseTphMappingStrategy();
+                });
+
+            modelBuilder.Entity("API.Schema.LocalLibrary", b =>
+                {
+                    b.Property<string>("LocalLibraryId")
+                        .HasMaxLength(64)
+                        .HasColumnType("character varying(64)");
+
+                    b.Property<string>("BasePath")
+                        .IsRequired()
+                        .HasMaxLength(256)
+                        .HasColumnType("character varying(256)");
+
+                    b.Property<string>("LibraryName")
+                        .IsRequired()
+                        .HasMaxLength(512)
+                        .HasColumnType("character varying(512)");
+
+                    b.HasKey("LocalLibraryId");
+
+                    b.ToTable("LocalLibraries");
+                });
+
+            modelBuilder.Entity("API.Schema.Manga", b =>
+                {
+                    b.Property<string>("MangaId")
+                        .HasMaxLength(64)
+                        .HasColumnType("character varying(64)");
+
+                    b.Property<string>("CoverFileNameInCache")
+                        .HasMaxLength(512)
+                        .HasColumnType("character varying(512)");
+
+                    b.Property<string>("CoverUrl")
+                        .IsRequired()
+                        .HasMaxLength(512)
+                        .HasColumnType("character varying(512)");
+
+                    b.Property<string>("Description")
+                        .IsRequired()
+                        .HasColumnType("text");
+
+                    b.Property<string>("DirectoryName")
+                        .IsRequired()
+                        .HasMaxLength(1024)
+                        .HasColumnType("character varying(1024)");
+
+                    b.Property<string>("IdOnConnectorSite")
+                        .IsRequired()
+                        .HasMaxLength(256)
+                        .HasColumnType("character varying(256)");
+
+                    b.Property<float>("IgnoreChaptersBefore")
+                        .HasColumnType("real");
+
+                    b.Property<string>("LibraryId")
+                        .HasMaxLength(64)
+                        .HasColumnType("character varying(64)");
+
+                    b.Property<string>("MangaConnectorName")
+                        .IsRequired()
+                        .HasMaxLength(32)
+                        .HasColumnType("character varying(32)");
+
+                    b.Property<string>("Name")
+                        .IsRequired()
+                        .HasMaxLength(512)
+                        .HasColumnType("character varying(512)");
+
+                    b.Property<string>("OriginalLanguage")
+                        .HasMaxLength(8)
+                        .HasColumnType("character varying(8)");
+
+                    b.Property<byte>("ReleaseStatus")
+                        .HasColumnType("smallint");
+
+                    b.Property<string>("WebsiteUrl")
+                        .IsRequired()
+                        .HasMaxLength(512)
+                        .HasColumnType("character varying(512)");
+
+                    b.Property<long>("Year")
+                        .HasColumnType("bigint");
+
+                    b.HasKey("MangaId");
+
+                    b.HasIndex("LibraryId");
+
+                    b.HasIndex("MangaConnectorName");
+
+                    b.ToTable("Mangas");
+                });
+
+            modelBuilder.Entity("API.Schema.MangaConnectors.MangaConnector", b =>
+                {
+                    b.Property<string>("Name")
+                        .HasMaxLength(32)
+                        .HasColumnType("character varying(32)");
+
+                    b.PrimitiveCollection<string[]>("BaseUris")
+                        .IsRequired()
+                        .HasMaxLength(256)
+                        .HasColumnType("text[]");
+
+                    b.Property<bool>("Enabled")
+                        .HasColumnType("boolean");
+
+                    b.Property<string>("IconUrl")
+                        .IsRequired()
+                        .HasMaxLength(2048)
+                        .HasColumnType("character varying(2048)");
+
+                    b.PrimitiveCollection<string[]>("SupportedLanguages")
+                        .IsRequired()
+                        .HasMaxLength(8)
+                        .HasColumnType("text[]");
+
+                    b.HasKey("Name");
+
+                    b.ToTable("MangaConnectors");
+
+                    b.HasDiscriminator<string>("Name").HasValue("MangaConnector");
+
+                    b.UseTphMappingStrategy();
+                });
+
+            modelBuilder.Entity("API.Schema.MangaTag", b =>
+                {
+                    b.Property<string>("Tag")
+                        .HasMaxLength(64)
+                        .HasColumnType("character varying(64)");
+
+                    b.HasKey("Tag");
+
+                    b.ToTable("Tags");
+                });
+
+            modelBuilder.Entity("AuthorToManga", b =>
+                {
+                    b.Property<string>("AuthorIds")
+                        .HasColumnType("character varying(64)");
+
+                    b.Property<string>("MangaIds")
+                        .HasColumnType("character varying(64)");
+
+                    b.HasKey("AuthorIds", "MangaIds");
+
+                    b.HasIndex("MangaIds");
+
+                    b.ToTable("AuthorToManga");
+                });
+
+            modelBuilder.Entity("JobJob", b =>
+                {
+                    b.Property<string>("DependsOnJobsJobId")
+                        .HasColumnType("character varying(64)");
+
+                    b.Property<string>("JobId")
+                        .HasColumnType("character varying(64)");
+
+                    b.HasKey("DependsOnJobsJobId", "JobId");
+
+                    b.HasIndex("JobId");
+
+                    b.ToTable("JobJob");
+                });
+
+            modelBuilder.Entity("MangaTagToManga", b =>
+                {
+                    b.Property<string>("MangaTagIds")
+                        .HasColumnType("character varying(64)");
+
+                    b.Property<string>("MangaIds")
+                        .HasColumnType("character varying(64)");
+
+                    b.HasKey("MangaTagIds", "MangaIds");
+
+                    b.HasIndex("MangaIds");
+
+                    b.ToTable("MangaTagToManga");
+                });
+
+            modelBuilder.Entity("API.Schema.Jobs.DownloadAvailableChaptersJob", b =>
+                {
+                    b.HasBaseType("API.Schema.Jobs.Job");
+
+                    b.Property<string>("MangaId")
+                        .IsRequired()
+                        .HasMaxLength(64)
+                        .HasColumnType("character varying(64)");
+
+                    b.HasIndex("MangaId");
+
+                    b.ToTable("Jobs", t =>
+                        {
+                            t.Property("MangaId")
+                                .HasColumnName("DownloadAvailableChaptersJob_MangaId");
+                        });
+
+                    b.HasDiscriminator().HasValue((byte)1);
+                });
+
+            modelBuilder.Entity("API.Schema.Jobs.DownloadMangaCoverJob", b =>
+                {
+                    b.HasBaseType("API.Schema.Jobs.Job");
+
+                    b.Property<string>("MangaId")
+                        .IsRequired()
+                        .HasMaxLength(64)
+                        .HasColumnType("character varying(64)");
+
+                    b.HasIndex("MangaId");
+
+                    b.HasDiscriminator().HasValue((byte)4);
+                });
+
+            modelBuilder.Entity("API.Schema.Jobs.DownloadSingleChapterJob", b =>
+                {
+                    b.HasBaseType("API.Schema.Jobs.Job");
+
+                    b.Property<string>("ChapterId")
+                        .IsRequired()
+                        .HasMaxLength(64)
+                        .HasColumnType("character varying(64)");
+
+                    b.HasIndex("ChapterId");
+
+                    b.HasDiscriminator().HasValue((byte)0);
+                });
+
+            modelBuilder.Entity("API.Schema.Jobs.MoveFileOrFolderJob", b =>
+                {
+                    b.HasBaseType("API.Schema.Jobs.Job");
+
+                    b.Property<string>("FromLocation")
+                        .IsRequired()
+                        .HasMaxLength(256)
+                        .HasColumnType("character varying(256)");
+
+                    b.Property<string>("ToLocation")
+                        .IsRequired()
+                        .HasMaxLength(256)
+                        .HasColumnType("character varying(256)");
+
+                    b.HasDiscriminator().HasValue((byte)3);
+                });
+
+            modelBuilder.Entity("API.Schema.Jobs.MoveMangaLibraryJob", b =>
+                {
+                    b.HasBaseType("API.Schema.Jobs.Job");
+
+                    b.Property<string>("MangaId")
+                        .IsRequired()
+                        .HasMaxLength(64)
+                        .HasColumnType("character varying(64)");
+
+                    b.Property<string>("ToLibraryId")
+                        .IsRequired()
+                        .HasMaxLength(64)
+                        .HasColumnType("character varying(64)");
+
+                    b.HasIndex("MangaId");
+
+                    b.HasIndex("ToLibraryId");
+
+                    b.ToTable("Jobs", t =>
+                        {
+                            t.Property("MangaId")
+                                .HasColumnName("MoveMangaLibraryJob_MangaId");
+                        });
+
+                    b.HasDiscriminator().HasValue((byte)7);
+                });
+
+            modelBuilder.Entity("API.Schema.Jobs.RetrieveChaptersJob", b =>
+                {
+                    b.HasBaseType("API.Schema.Jobs.Job");
+
+                    b.Property<string>("Language")
+                        .IsRequired()
+                        .HasMaxLength(8)
+                        .HasColumnType("character varying(8)");
+
+                    b.Property<string>("MangaId")
+                        .IsRequired()
+                        .HasMaxLength(64)
+                        .HasColumnType("character varying(64)");
+
+                    b.HasIndex("MangaId");
+
+                    b.ToTable("Jobs", t =>
+                        {
+                            t.Property("MangaId")
+                                .HasColumnName("RetrieveChaptersJob_MangaId");
+                        });
+
+                    b.HasDiscriminator().HasValue((byte)5);
+                });
+
+            modelBuilder.Entity("API.Schema.Jobs.UpdateFilesDownloadedJob", b =>
+                {
+                    b.HasBaseType("API.Schema.Jobs.Job");
+
+                    b.Property<string>("MangaId")
+                        .IsRequired()
+                        .HasMaxLength(64)
+                        .HasColumnType("character varying(64)");
+
+                    b.HasIndex("MangaId");
+
+                    b.ToTable("Jobs", t =>
+                        {
+                            t.Property("MangaId")
+                                .HasColumnName("UpdateFilesDownloadedJob_MangaId");
+                        });
+
+                    b.HasDiscriminator().HasValue((byte)6);
+                });
+
+            modelBuilder.Entity("API.Schema.MangaConnectors.ComickIo", b =>
+                {
+                    b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
+
+                    b.HasDiscriminator().HasValue("ComickIo");
+                });
+
+            modelBuilder.Entity("API.Schema.MangaConnectors.Global", b =>
+                {
+                    b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
+
+                    b.HasDiscriminator().HasValue("Global");
+                });
+
+            modelBuilder.Entity("API.Schema.MangaConnectors.MangaDex", b =>
+                {
+                    b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
+
+                    b.HasDiscriminator().HasValue("MangaDex");
+                });
+
+            modelBuilder.Entity("API.Schema.Chapter", b =>
+                {
+                    b.HasOne("API.Schema.Manga", "ParentManga")
+                        .WithMany("Chapters")
+                        .HasForeignKey("ParentMangaId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("ParentManga");
+                });
+
+            modelBuilder.Entity("API.Schema.Jobs.Job", b =>
+                {
+                    b.HasOne("API.Schema.Jobs.Job", "ParentJob")
+                        .WithMany()
+                        .HasForeignKey("ParentJobId")
+                        .OnDelete(DeleteBehavior.Cascade);
+
+                    b.Navigation("ParentJob");
+                });
+
+            modelBuilder.Entity("API.Schema.Manga", b =>
+                {
+                    b.HasOne("API.Schema.LocalLibrary", "Library")
+                        .WithMany()
+                        .HasForeignKey("LibraryId")
+                        .OnDelete(DeleteBehavior.SetNull);
+
+                    b.HasOne("API.Schema.MangaConnectors.MangaConnector", "MangaConnector")
+                        .WithMany()
+                        .HasForeignKey("MangaConnectorName")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.OwnsMany("API.Schema.Link", "Links", b1 =>
+                        {
+                            b1.Property<string>("LinkId")
+                                .HasMaxLength(64)
+                                .HasColumnType("character varying(64)");
+
+                            b1.Property<string>("LinkProvider")
+                                .IsRequired()
+                                .HasMaxLength(64)
+                                .HasColumnType("character varying(64)");
+
+                            b1.Property<string>("LinkUrl")
+                                .IsRequired()
+                                .HasMaxLength(2048)
+                                .HasColumnType("character varying(2048)");
+
+                            b1.Property<string>("MangaId")
+                                .IsRequired()
+                                .HasColumnType("character varying(64)");
+
+                            b1.HasKey("LinkId");
+
+                            b1.HasIndex("MangaId");
+
+                            b1.ToTable("Link");
+
+                            b1.WithOwner()
+                                .HasForeignKey("MangaId");
+                        });
+
+                    b.OwnsMany("API.Schema.MangaAltTitle", "AltTitles", b1 =>
+                        {
+                            b1.Property<string>("MangaId")
+                                .HasColumnType("character varying(64)");
+
+                            b1.Property<int>("Id")
+                                .ValueGeneratedOnAdd()
+                                .HasColumnType("integer");
+
+                            NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property<int>("Id"));
+
+                            b1.Property<string>("Language")
+                                .IsRequired()
+                                .HasMaxLength(8)
+                                .HasColumnType("character varying(8)");
+
+                            b1.Property<string>("Title")
+                                .IsRequired()
+                                .HasMaxLength(256)
+                                .HasColumnType("character varying(256)");
+
+                            b1.HasKey("MangaId", "Id");
+
+                            b1.ToTable("MangaAltTitle");
+
+                            b1.WithOwner()
+                                .HasForeignKey("MangaId");
+                        });
+
+                    b.Navigation("AltTitles");
+
+                    b.Navigation("Library");
+
+                    b.Navigation("Links");
+
+                    b.Navigation("MangaConnector");
+                });
+
+            modelBuilder.Entity("AuthorToManga", b =>
+                {
+                    b.HasOne("API.Schema.Author", null)
+                        .WithMany()
+                        .HasForeignKey("AuthorIds")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.HasOne("API.Schema.Manga", null)
+                        .WithMany()
+                        .HasForeignKey("MangaIds")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+                });
+
+            modelBuilder.Entity("JobJob", b =>
+                {
+                    b.HasOne("API.Schema.Jobs.Job", null)
+                        .WithMany()
+                        .HasForeignKey("DependsOnJobsJobId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.HasOne("API.Schema.Jobs.Job", null)
+                        .WithMany()
+                        .HasForeignKey("JobId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+                });
+
+            modelBuilder.Entity("MangaTagToManga", b =>
+                {
+                    b.HasOne("API.Schema.Manga", null)
+                        .WithMany()
+                        .HasForeignKey("MangaIds")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.HasOne("API.Schema.MangaTag", null)
+                        .WithMany()
+                        .HasForeignKey("MangaTagIds")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+                });
+
+            modelBuilder.Entity("API.Schema.Jobs.DownloadAvailableChaptersJob", b =>
+                {
+                    b.HasOne("API.Schema.Manga", "Manga")
+                        .WithMany()
+                        .HasForeignKey("MangaId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("Manga");
+                });
+
+            modelBuilder.Entity("API.Schema.Jobs.DownloadMangaCoverJob", b =>
+                {
+                    b.HasOne("API.Schema.Manga", "Manga")
+                        .WithMany()
+                        .HasForeignKey("MangaId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("Manga");
+                });
+
+            modelBuilder.Entity("API.Schema.Jobs.DownloadSingleChapterJob", b =>
+                {
+                    b.HasOne("API.Schema.Chapter", "Chapter")
+                        .WithMany()
+                        .HasForeignKey("ChapterId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("Chapter");
+                });
+
+            modelBuilder.Entity("API.Schema.Jobs.MoveMangaLibraryJob", b =>
+                {
+                    b.HasOne("API.Schema.Manga", "Manga")
+                        .WithMany()
+                        .HasForeignKey("MangaId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.HasOne("API.Schema.LocalLibrary", "ToLibrary")
+                        .WithMany()
+                        .HasForeignKey("ToLibraryId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("Manga");
+
+                    b.Navigation("ToLibrary");
+                });
+
+            modelBuilder.Entity("API.Schema.Jobs.RetrieveChaptersJob", b =>
+                {
+                    b.HasOne("API.Schema.Manga", "Manga")
+                        .WithMany()
+                        .HasForeignKey("MangaId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("Manga");
+                });
+
+            modelBuilder.Entity("API.Schema.Jobs.UpdateFilesDownloadedJob", b =>
+                {
+                    b.HasOne("API.Schema.Manga", "Manga")
+                        .WithMany()
+                        .HasForeignKey("MangaId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("Manga");
+                });
+
+            modelBuilder.Entity("API.Schema.Manga", b =>
+                {
+                    b.Navigation("Chapters");
+                });
+#pragma warning restore 612, 618
+        }
+    }
+}
diff --git a/API/Migrations/pgsql/20250516121442_AltTitle-Owned.cs b/API/Migrations/pgsql/20250516121442_AltTitle-Owned.cs
new file mode 100644
index 0000000..bde9cb6
--- /dev/null
+++ b/API/Migrations/pgsql/20250516121442_AltTitle-Owned.cs
@@ -0,0 +1,70 @@
+using Microsoft.EntityFrameworkCore.Migrations;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+
+#nullable disable
+
+namespace API.Migrations.pgsql
+{
+    /// <inheritdoc />
+    public partial class AltTitleOwned : Migration
+    {
+        /// <inheritdoc />
+        protected override void Up(MigrationBuilder migrationBuilder)
+        {
+            migrationBuilder.DropPrimaryKey(
+                name: "PK_MangaAltTitle",
+                table: "MangaAltTitle");
+
+            migrationBuilder.DropIndex(
+                name: "IX_MangaAltTitle_MangaId",
+                table: "MangaAltTitle");
+
+            migrationBuilder.DropColumn(
+                name: "AltTitleId",
+                table: "MangaAltTitle");
+
+            migrationBuilder.AddColumn<int>(
+                name: "Id",
+                table: "MangaAltTitle",
+                type: "integer",
+                nullable: false,
+                defaultValue: 0)
+                .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
+
+            migrationBuilder.AddPrimaryKey(
+                name: "PK_MangaAltTitle",
+                table: "MangaAltTitle",
+                columns: new[] { "MangaId", "Id" });
+        }
+
+        /// <inheritdoc />
+        protected override void Down(MigrationBuilder migrationBuilder)
+        {
+            migrationBuilder.DropPrimaryKey(
+                name: "PK_MangaAltTitle",
+                table: "MangaAltTitle");
+
+            migrationBuilder.DropColumn(
+                name: "Id",
+                table: "MangaAltTitle");
+
+            migrationBuilder.AddColumn<string>(
+                name: "AltTitleId",
+                table: "MangaAltTitle",
+                type: "character varying(64)",
+                maxLength: 64,
+                nullable: false,
+                defaultValue: "");
+
+            migrationBuilder.AddPrimaryKey(
+                name: "PK_MangaAltTitle",
+                table: "MangaAltTitle",
+                column: "AltTitleId");
+
+            migrationBuilder.CreateIndex(
+                name: "IX_MangaAltTitle_MangaId",
+                table: "MangaAltTitle",
+                column: "MangaId");
+        }
+    }
+}
diff --git a/API/Migrations/pgsql/20250516121725_Manga-Year-Nullable.Designer.cs b/API/Migrations/pgsql/20250516121725_Manga-Year-Nullable.Designer.cs
new file mode 100644
index 0000000..afe97c3
--- /dev/null
+++ b/API/Migrations/pgsql/20250516121725_Manga-Year-Nullable.Designer.cs
@@ -0,0 +1,688 @@
+// <auto-generated />
+using System;
+using API.Schema.Contexts;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+
+#nullable disable
+
+namespace API.Migrations.pgsql
+{
+    [DbContext(typeof(PgsqlContext))]
+    [Migration("20250516121725_Manga-Year-Nullable")]
+    partial class MangaYearNullable
+    {
+        /// <inheritdoc />
+        protected override void BuildTargetModel(ModelBuilder modelBuilder)
+        {
+#pragma warning disable 612, 618
+            modelBuilder
+                .HasAnnotation("ProductVersion", "9.0.3")
+                .HasAnnotation("Relational:MaxIdentifierLength", 63);
+
+            NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
+
+            modelBuilder.Entity("API.Schema.Author", b =>
+                {
+                    b.Property<string>("AuthorId")
+                        .HasMaxLength(64)
+                        .HasColumnType("character varying(64)");
+
+                    b.Property<string>("AuthorName")
+                        .IsRequired()
+                        .HasMaxLength(128)
+                        .HasColumnType("character varying(128)");
+
+                    b.HasKey("AuthorId");
+
+                    b.ToTable("Authors");
+                });
+
+            modelBuilder.Entity("API.Schema.Chapter", b =>
+                {
+                    b.Property<string>("ChapterId")
+                        .HasMaxLength(64)
+                        .HasColumnType("character varying(64)");
+
+                    b.Property<string>("ChapterNumber")
+                        .IsRequired()
+                        .HasMaxLength(10)
+                        .HasColumnType("character varying(10)");
+
+                    b.Property<bool>("Downloaded")
+                        .HasColumnType("boolean");
+
+                    b.Property<string>("FileName")
+                        .IsRequired()
+                        .HasMaxLength(256)
+                        .HasColumnType("character varying(256)");
+
+                    b.Property<string>("ParentMangaId")
+                        .IsRequired()
+                        .HasColumnType("character varying(64)");
+
+                    b.Property<string>("Title")
+                        .HasMaxLength(256)
+                        .HasColumnType("character varying(256)");
+
+                    b.Property<string>("Url")
+                        .IsRequired()
+                        .HasMaxLength(2048)
+                        .HasColumnType("character varying(2048)");
+
+                    b.Property<int?>("VolumeNumber")
+                        .HasColumnType("integer");
+
+                    b.HasKey("ChapterId");
+
+                    b.HasIndex("ParentMangaId");
+
+                    b.ToTable("Chapters");
+                });
+
+            modelBuilder.Entity("API.Schema.Jobs.Job", b =>
+                {
+                    b.Property<string>("JobId")
+                        .HasMaxLength(64)
+                        .HasColumnType("character varying(64)");
+
+                    b.Property<bool>("Enabled")
+                        .HasColumnType("boolean");
+
+                    b.Property<byte>("JobType")
+                        .HasColumnType("smallint");
+
+                    b.Property<DateTime>("LastExecution")
+                        .HasColumnType("timestamp with time zone");
+
+                    b.Property<string>("ParentJobId")
+                        .HasMaxLength(64)
+                        .HasColumnType("character varying(64)");
+
+                    b.Property<decimal>("RecurrenceMs")
+                        .HasColumnType("numeric(20,0)");
+
+                    b.Property<byte>("state")
+                        .HasColumnType("smallint");
+
+                    b.HasKey("JobId");
+
+                    b.HasIndex("ParentJobId");
+
+                    b.ToTable("Jobs");
+
+                    b.HasDiscriminator<byte>("JobType");
+
+                    b.UseTphMappingStrategy();
+                });
+
+            modelBuilder.Entity("API.Schema.LocalLibrary", b =>
+                {
+                    b.Property<string>("LocalLibraryId")
+                        .HasMaxLength(64)
+                        .HasColumnType("character varying(64)");
+
+                    b.Property<string>("BasePath")
+                        .IsRequired()
+                        .HasMaxLength(256)
+                        .HasColumnType("character varying(256)");
+
+                    b.Property<string>("LibraryName")
+                        .IsRequired()
+                        .HasMaxLength(512)
+                        .HasColumnType("character varying(512)");
+
+                    b.HasKey("LocalLibraryId");
+
+                    b.ToTable("LocalLibraries");
+                });
+
+            modelBuilder.Entity("API.Schema.Manga", b =>
+                {
+                    b.Property<string>("MangaId")
+                        .HasMaxLength(64)
+                        .HasColumnType("character varying(64)");
+
+                    b.Property<string>("CoverFileNameInCache")
+                        .HasMaxLength(512)
+                        .HasColumnType("character varying(512)");
+
+                    b.Property<string>("CoverUrl")
+                        .IsRequired()
+                        .HasMaxLength(512)
+                        .HasColumnType("character varying(512)");
+
+                    b.Property<string>("Description")
+                        .IsRequired()
+                        .HasColumnType("text");
+
+                    b.Property<string>("DirectoryName")
+                        .IsRequired()
+                        .HasMaxLength(1024)
+                        .HasColumnType("character varying(1024)");
+
+                    b.Property<string>("IdOnConnectorSite")
+                        .IsRequired()
+                        .HasMaxLength(256)
+                        .HasColumnType("character varying(256)");
+
+                    b.Property<float>("IgnoreChaptersBefore")
+                        .HasColumnType("real");
+
+                    b.Property<string>("LibraryId")
+                        .HasMaxLength(64)
+                        .HasColumnType("character varying(64)");
+
+                    b.Property<string>("MangaConnectorName")
+                        .IsRequired()
+                        .HasMaxLength(32)
+                        .HasColumnType("character varying(32)");
+
+                    b.Property<string>("Name")
+                        .IsRequired()
+                        .HasMaxLength(512)
+                        .HasColumnType("character varying(512)");
+
+                    b.Property<string>("OriginalLanguage")
+                        .HasMaxLength(8)
+                        .HasColumnType("character varying(8)");
+
+                    b.Property<byte>("ReleaseStatus")
+                        .HasColumnType("smallint");
+
+                    b.Property<string>("WebsiteUrl")
+                        .IsRequired()
+                        .HasMaxLength(512)
+                        .HasColumnType("character varying(512)");
+
+                    b.Property<long?>("Year")
+                        .HasColumnType("bigint");
+
+                    b.HasKey("MangaId");
+
+                    b.HasIndex("LibraryId");
+
+                    b.HasIndex("MangaConnectorName");
+
+                    b.ToTable("Mangas");
+                });
+
+            modelBuilder.Entity("API.Schema.MangaConnectors.MangaConnector", b =>
+                {
+                    b.Property<string>("Name")
+                        .HasMaxLength(32)
+                        .HasColumnType("character varying(32)");
+
+                    b.PrimitiveCollection<string[]>("BaseUris")
+                        .IsRequired()
+                        .HasMaxLength(256)
+                        .HasColumnType("text[]");
+
+                    b.Property<bool>("Enabled")
+                        .HasColumnType("boolean");
+
+                    b.Property<string>("IconUrl")
+                        .IsRequired()
+                        .HasMaxLength(2048)
+                        .HasColumnType("character varying(2048)");
+
+                    b.PrimitiveCollection<string[]>("SupportedLanguages")
+                        .IsRequired()
+                        .HasMaxLength(8)
+                        .HasColumnType("text[]");
+
+                    b.HasKey("Name");
+
+                    b.ToTable("MangaConnectors");
+
+                    b.HasDiscriminator<string>("Name").HasValue("MangaConnector");
+
+                    b.UseTphMappingStrategy();
+                });
+
+            modelBuilder.Entity("API.Schema.MangaTag", b =>
+                {
+                    b.Property<string>("Tag")
+                        .HasMaxLength(64)
+                        .HasColumnType("character varying(64)");
+
+                    b.HasKey("Tag");
+
+                    b.ToTable("Tags");
+                });
+
+            modelBuilder.Entity("AuthorToManga", b =>
+                {
+                    b.Property<string>("AuthorIds")
+                        .HasColumnType("character varying(64)");
+
+                    b.Property<string>("MangaIds")
+                        .HasColumnType("character varying(64)");
+
+                    b.HasKey("AuthorIds", "MangaIds");
+
+                    b.HasIndex("MangaIds");
+
+                    b.ToTable("AuthorToManga");
+                });
+
+            modelBuilder.Entity("JobJob", b =>
+                {
+                    b.Property<string>("DependsOnJobsJobId")
+                        .HasColumnType("character varying(64)");
+
+                    b.Property<string>("JobId")
+                        .HasColumnType("character varying(64)");
+
+                    b.HasKey("DependsOnJobsJobId", "JobId");
+
+                    b.HasIndex("JobId");
+
+                    b.ToTable("JobJob");
+                });
+
+            modelBuilder.Entity("MangaTagToManga", b =>
+                {
+                    b.Property<string>("MangaTagIds")
+                        .HasColumnType("character varying(64)");
+
+                    b.Property<string>("MangaIds")
+                        .HasColumnType("character varying(64)");
+
+                    b.HasKey("MangaTagIds", "MangaIds");
+
+                    b.HasIndex("MangaIds");
+
+                    b.ToTable("MangaTagToManga");
+                });
+
+            modelBuilder.Entity("API.Schema.Jobs.DownloadAvailableChaptersJob", b =>
+                {
+                    b.HasBaseType("API.Schema.Jobs.Job");
+
+                    b.Property<string>("MangaId")
+                        .IsRequired()
+                        .HasMaxLength(64)
+                        .HasColumnType("character varying(64)");
+
+                    b.HasIndex("MangaId");
+
+                    b.ToTable("Jobs", t =>
+                        {
+                            t.Property("MangaId")
+                                .HasColumnName("DownloadAvailableChaptersJob_MangaId");
+                        });
+
+                    b.HasDiscriminator().HasValue((byte)1);
+                });
+
+            modelBuilder.Entity("API.Schema.Jobs.DownloadMangaCoverJob", b =>
+                {
+                    b.HasBaseType("API.Schema.Jobs.Job");
+
+                    b.Property<string>("MangaId")
+                        .IsRequired()
+                        .HasMaxLength(64)
+                        .HasColumnType("character varying(64)");
+
+                    b.HasIndex("MangaId");
+
+                    b.HasDiscriminator().HasValue((byte)4);
+                });
+
+            modelBuilder.Entity("API.Schema.Jobs.DownloadSingleChapterJob", b =>
+                {
+                    b.HasBaseType("API.Schema.Jobs.Job");
+
+                    b.Property<string>("ChapterId")
+                        .IsRequired()
+                        .HasMaxLength(64)
+                        .HasColumnType("character varying(64)");
+
+                    b.HasIndex("ChapterId");
+
+                    b.HasDiscriminator().HasValue((byte)0);
+                });
+
+            modelBuilder.Entity("API.Schema.Jobs.MoveFileOrFolderJob", b =>
+                {
+                    b.HasBaseType("API.Schema.Jobs.Job");
+
+                    b.Property<string>("FromLocation")
+                        .IsRequired()
+                        .HasMaxLength(256)
+                        .HasColumnType("character varying(256)");
+
+                    b.Property<string>("ToLocation")
+                        .IsRequired()
+                        .HasMaxLength(256)
+                        .HasColumnType("character varying(256)");
+
+                    b.HasDiscriminator().HasValue((byte)3);
+                });
+
+            modelBuilder.Entity("API.Schema.Jobs.MoveMangaLibraryJob", b =>
+                {
+                    b.HasBaseType("API.Schema.Jobs.Job");
+
+                    b.Property<string>("MangaId")
+                        .IsRequired()
+                        .HasMaxLength(64)
+                        .HasColumnType("character varying(64)");
+
+                    b.Property<string>("ToLibraryId")
+                        .IsRequired()
+                        .HasMaxLength(64)
+                        .HasColumnType("character varying(64)");
+
+                    b.HasIndex("MangaId");
+
+                    b.HasIndex("ToLibraryId");
+
+                    b.ToTable("Jobs", t =>
+                        {
+                            t.Property("MangaId")
+                                .HasColumnName("MoveMangaLibraryJob_MangaId");
+                        });
+
+                    b.HasDiscriminator().HasValue((byte)7);
+                });
+
+            modelBuilder.Entity("API.Schema.Jobs.RetrieveChaptersJob", b =>
+                {
+                    b.HasBaseType("API.Schema.Jobs.Job");
+
+                    b.Property<string>("Language")
+                        .IsRequired()
+                        .HasMaxLength(8)
+                        .HasColumnType("character varying(8)");
+
+                    b.Property<string>("MangaId")
+                        .IsRequired()
+                        .HasMaxLength(64)
+                        .HasColumnType("character varying(64)");
+
+                    b.HasIndex("MangaId");
+
+                    b.ToTable("Jobs", t =>
+                        {
+                            t.Property("MangaId")
+                                .HasColumnName("RetrieveChaptersJob_MangaId");
+                        });
+
+                    b.HasDiscriminator().HasValue((byte)5);
+                });
+
+            modelBuilder.Entity("API.Schema.Jobs.UpdateFilesDownloadedJob", b =>
+                {
+                    b.HasBaseType("API.Schema.Jobs.Job");
+
+                    b.Property<string>("MangaId")
+                        .IsRequired()
+                        .HasMaxLength(64)
+                        .HasColumnType("character varying(64)");
+
+                    b.HasIndex("MangaId");
+
+                    b.ToTable("Jobs", t =>
+                        {
+                            t.Property("MangaId")
+                                .HasColumnName("UpdateFilesDownloadedJob_MangaId");
+                        });
+
+                    b.HasDiscriminator().HasValue((byte)6);
+                });
+
+            modelBuilder.Entity("API.Schema.MangaConnectors.ComickIo", b =>
+                {
+                    b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
+
+                    b.HasDiscriminator().HasValue("ComickIo");
+                });
+
+            modelBuilder.Entity("API.Schema.MangaConnectors.Global", b =>
+                {
+                    b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
+
+                    b.HasDiscriminator().HasValue("Global");
+                });
+
+            modelBuilder.Entity("API.Schema.MangaConnectors.MangaDex", b =>
+                {
+                    b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
+
+                    b.HasDiscriminator().HasValue("MangaDex");
+                });
+
+            modelBuilder.Entity("API.Schema.Chapter", b =>
+                {
+                    b.HasOne("API.Schema.Manga", "ParentManga")
+                        .WithMany("Chapters")
+                        .HasForeignKey("ParentMangaId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("ParentManga");
+                });
+
+            modelBuilder.Entity("API.Schema.Jobs.Job", b =>
+                {
+                    b.HasOne("API.Schema.Jobs.Job", "ParentJob")
+                        .WithMany()
+                        .HasForeignKey("ParentJobId")
+                        .OnDelete(DeleteBehavior.Cascade);
+
+                    b.Navigation("ParentJob");
+                });
+
+            modelBuilder.Entity("API.Schema.Manga", b =>
+                {
+                    b.HasOne("API.Schema.LocalLibrary", "Library")
+                        .WithMany()
+                        .HasForeignKey("LibraryId")
+                        .OnDelete(DeleteBehavior.SetNull);
+
+                    b.HasOne("API.Schema.MangaConnectors.MangaConnector", "MangaConnector")
+                        .WithMany()
+                        .HasForeignKey("MangaConnectorName")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.OwnsMany("API.Schema.Link", "Links", b1 =>
+                        {
+                            b1.Property<string>("LinkId")
+                                .HasMaxLength(64)
+                                .HasColumnType("character varying(64)");
+
+                            b1.Property<string>("LinkProvider")
+                                .IsRequired()
+                                .HasMaxLength(64)
+                                .HasColumnType("character varying(64)");
+
+                            b1.Property<string>("LinkUrl")
+                                .IsRequired()
+                                .HasMaxLength(2048)
+                                .HasColumnType("character varying(2048)");
+
+                            b1.Property<string>("MangaId")
+                                .IsRequired()
+                                .HasColumnType("character varying(64)");
+
+                            b1.HasKey("LinkId");
+
+                            b1.HasIndex("MangaId");
+
+                            b1.ToTable("Link");
+
+                            b1.WithOwner()
+                                .HasForeignKey("MangaId");
+                        });
+
+                    b.OwnsMany("API.Schema.MangaAltTitle", "AltTitles", b1 =>
+                        {
+                            b1.Property<string>("MangaId")
+                                .HasColumnType("character varying(64)");
+
+                            b1.Property<int>("Id")
+                                .ValueGeneratedOnAdd()
+                                .HasColumnType("integer");
+
+                            NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property<int>("Id"));
+
+                            b1.Property<string>("Language")
+                                .IsRequired()
+                                .HasMaxLength(8)
+                                .HasColumnType("character varying(8)");
+
+                            b1.Property<string>("Title")
+                                .IsRequired()
+                                .HasMaxLength(256)
+                                .HasColumnType("character varying(256)");
+
+                            b1.HasKey("MangaId", "Id");
+
+                            b1.ToTable("MangaAltTitle");
+
+                            b1.WithOwner()
+                                .HasForeignKey("MangaId");
+                        });
+
+                    b.Navigation("AltTitles");
+
+                    b.Navigation("Library");
+
+                    b.Navigation("Links");
+
+                    b.Navigation("MangaConnector");
+                });
+
+            modelBuilder.Entity("AuthorToManga", b =>
+                {
+                    b.HasOne("API.Schema.Author", null)
+                        .WithMany()
+                        .HasForeignKey("AuthorIds")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.HasOne("API.Schema.Manga", null)
+                        .WithMany()
+                        .HasForeignKey("MangaIds")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+                });
+
+            modelBuilder.Entity("JobJob", b =>
+                {
+                    b.HasOne("API.Schema.Jobs.Job", null)
+                        .WithMany()
+                        .HasForeignKey("DependsOnJobsJobId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.HasOne("API.Schema.Jobs.Job", null)
+                        .WithMany()
+                        .HasForeignKey("JobId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+                });
+
+            modelBuilder.Entity("MangaTagToManga", b =>
+                {
+                    b.HasOne("API.Schema.Manga", null)
+                        .WithMany()
+                        .HasForeignKey("MangaIds")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.HasOne("API.Schema.MangaTag", null)
+                        .WithMany()
+                        .HasForeignKey("MangaTagIds")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+                });
+
+            modelBuilder.Entity("API.Schema.Jobs.DownloadAvailableChaptersJob", b =>
+                {
+                    b.HasOne("API.Schema.Manga", "Manga")
+                        .WithMany()
+                        .HasForeignKey("MangaId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("Manga");
+                });
+
+            modelBuilder.Entity("API.Schema.Jobs.DownloadMangaCoverJob", b =>
+                {
+                    b.HasOne("API.Schema.Manga", "Manga")
+                        .WithMany()
+                        .HasForeignKey("MangaId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("Manga");
+                });
+
+            modelBuilder.Entity("API.Schema.Jobs.DownloadSingleChapterJob", b =>
+                {
+                    b.HasOne("API.Schema.Chapter", "Chapter")
+                        .WithMany()
+                        .HasForeignKey("ChapterId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("Chapter");
+                });
+
+            modelBuilder.Entity("API.Schema.Jobs.MoveMangaLibraryJob", b =>
+                {
+                    b.HasOne("API.Schema.Manga", "Manga")
+                        .WithMany()
+                        .HasForeignKey("MangaId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.HasOne("API.Schema.LocalLibrary", "ToLibrary")
+                        .WithMany()
+                        .HasForeignKey("ToLibraryId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("Manga");
+
+                    b.Navigation("ToLibrary");
+                });
+
+            modelBuilder.Entity("API.Schema.Jobs.RetrieveChaptersJob", b =>
+                {
+                    b.HasOne("API.Schema.Manga", "Manga")
+                        .WithMany()
+                        .HasForeignKey("MangaId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("Manga");
+                });
+
+            modelBuilder.Entity("API.Schema.Jobs.UpdateFilesDownloadedJob", b =>
+                {
+                    b.HasOne("API.Schema.Manga", "Manga")
+                        .WithMany()
+                        .HasForeignKey("MangaId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("Manga");
+                });
+
+            modelBuilder.Entity("API.Schema.Manga", b =>
+                {
+                    b.Navigation("Chapters");
+                });
+#pragma warning restore 612, 618
+        }
+    }
+}
diff --git a/API/Migrations/pgsql/20250516121725_Manga-Year-Nullable.cs b/API/Migrations/pgsql/20250516121725_Manga-Year-Nullable.cs
new file mode 100644
index 0000000..9c0b7a8
--- /dev/null
+++ b/API/Migrations/pgsql/20250516121725_Manga-Year-Nullable.cs
@@ -0,0 +1,36 @@
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace API.Migrations.pgsql
+{
+    /// <inheritdoc />
+    public partial class MangaYearNullable : Migration
+    {
+        /// <inheritdoc />
+        protected override void Up(MigrationBuilder migrationBuilder)
+        {
+            migrationBuilder.AlterColumn<long>(
+                name: "Year",
+                table: "Mangas",
+                type: "bigint",
+                nullable: true,
+                oldClrType: typeof(long),
+                oldType: "bigint");
+        }
+
+        /// <inheritdoc />
+        protected override void Down(MigrationBuilder migrationBuilder)
+        {
+            migrationBuilder.AlterColumn<long>(
+                name: "Year",
+                table: "Mangas",
+                type: "bigint",
+                nullable: false,
+                defaultValue: 0L,
+                oldClrType: typeof(long),
+                oldType: "bigint",
+                oldNullable: true);
+        }
+    }
+}
diff --git a/API/Migrations/pgsql/20250516122242_AltTitle-Owned-WithId.Designer.cs b/API/Migrations/pgsql/20250516122242_AltTitle-Owned-WithId.Designer.cs
new file mode 100644
index 0000000..30b595c
--- /dev/null
+++ b/API/Migrations/pgsql/20250516122242_AltTitle-Owned-WithId.Designer.cs
@@ -0,0 +1,689 @@
+// <auto-generated />
+using System;
+using API.Schema.Contexts;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+
+#nullable disable
+
+namespace API.Migrations.pgsql
+{
+    [DbContext(typeof(PgsqlContext))]
+    [Migration("20250516122242_AltTitle-Owned-WithId")]
+    partial class AltTitleOwnedWithId
+    {
+        /// <inheritdoc />
+        protected override void BuildTargetModel(ModelBuilder modelBuilder)
+        {
+#pragma warning disable 612, 618
+            modelBuilder
+                .HasAnnotation("ProductVersion", "9.0.3")
+                .HasAnnotation("Relational:MaxIdentifierLength", 63);
+
+            NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
+
+            modelBuilder.Entity("API.Schema.Author", b =>
+                {
+                    b.Property<string>("AuthorId")
+                        .HasMaxLength(64)
+                        .HasColumnType("character varying(64)");
+
+                    b.Property<string>("AuthorName")
+                        .IsRequired()
+                        .HasMaxLength(128)
+                        .HasColumnType("character varying(128)");
+
+                    b.HasKey("AuthorId");
+
+                    b.ToTable("Authors");
+                });
+
+            modelBuilder.Entity("API.Schema.Chapter", b =>
+                {
+                    b.Property<string>("ChapterId")
+                        .HasMaxLength(64)
+                        .HasColumnType("character varying(64)");
+
+                    b.Property<string>("ChapterNumber")
+                        .IsRequired()
+                        .HasMaxLength(10)
+                        .HasColumnType("character varying(10)");
+
+                    b.Property<bool>("Downloaded")
+                        .HasColumnType("boolean");
+
+                    b.Property<string>("FileName")
+                        .IsRequired()
+                        .HasMaxLength(256)
+                        .HasColumnType("character varying(256)");
+
+                    b.Property<string>("ParentMangaId")
+                        .IsRequired()
+                        .HasColumnType("character varying(64)");
+
+                    b.Property<string>("Title")
+                        .HasMaxLength(256)
+                        .HasColumnType("character varying(256)");
+
+                    b.Property<string>("Url")
+                        .IsRequired()
+                        .HasMaxLength(2048)
+                        .HasColumnType("character varying(2048)");
+
+                    b.Property<int?>("VolumeNumber")
+                        .HasColumnType("integer");
+
+                    b.HasKey("ChapterId");
+
+                    b.HasIndex("ParentMangaId");
+
+                    b.ToTable("Chapters");
+                });
+
+            modelBuilder.Entity("API.Schema.Jobs.Job", b =>
+                {
+                    b.Property<string>("JobId")
+                        .HasMaxLength(64)
+                        .HasColumnType("character varying(64)");
+
+                    b.Property<bool>("Enabled")
+                        .HasColumnType("boolean");
+
+                    b.Property<byte>("JobType")
+                        .HasColumnType("smallint");
+
+                    b.Property<DateTime>("LastExecution")
+                        .HasColumnType("timestamp with time zone");
+
+                    b.Property<string>("ParentJobId")
+                        .HasMaxLength(64)
+                        .HasColumnType("character varying(64)");
+
+                    b.Property<decimal>("RecurrenceMs")
+                        .HasColumnType("numeric(20,0)");
+
+                    b.Property<byte>("state")
+                        .HasColumnType("smallint");
+
+                    b.HasKey("JobId");
+
+                    b.HasIndex("ParentJobId");
+
+                    b.ToTable("Jobs");
+
+                    b.HasDiscriminator<byte>("JobType");
+
+                    b.UseTphMappingStrategy();
+                });
+
+            modelBuilder.Entity("API.Schema.LocalLibrary", b =>
+                {
+                    b.Property<string>("LocalLibraryId")
+                        .HasMaxLength(64)
+                        .HasColumnType("character varying(64)");
+
+                    b.Property<string>("BasePath")
+                        .IsRequired()
+                        .HasMaxLength(256)
+                        .HasColumnType("character varying(256)");
+
+                    b.Property<string>("LibraryName")
+                        .IsRequired()
+                        .HasMaxLength(512)
+                        .HasColumnType("character varying(512)");
+
+                    b.HasKey("LocalLibraryId");
+
+                    b.ToTable("LocalLibraries");
+                });
+
+            modelBuilder.Entity("API.Schema.Manga", b =>
+                {
+                    b.Property<string>("MangaId")
+                        .HasMaxLength(64)
+                        .HasColumnType("character varying(64)");
+
+                    b.Property<string>("CoverFileNameInCache")
+                        .HasMaxLength(512)
+                        .HasColumnType("character varying(512)");
+
+                    b.Property<string>("CoverUrl")
+                        .IsRequired()
+                        .HasMaxLength(512)
+                        .HasColumnType("character varying(512)");
+
+                    b.Property<string>("Description")
+                        .IsRequired()
+                        .HasColumnType("text");
+
+                    b.Property<string>("DirectoryName")
+                        .IsRequired()
+                        .HasMaxLength(1024)
+                        .HasColumnType("character varying(1024)");
+
+                    b.Property<string>("IdOnConnectorSite")
+                        .IsRequired()
+                        .HasMaxLength(256)
+                        .HasColumnType("character varying(256)");
+
+                    b.Property<float>("IgnoreChaptersBefore")
+                        .HasColumnType("real");
+
+                    b.Property<string>("LibraryId")
+                        .HasMaxLength(64)
+                        .HasColumnType("character varying(64)");
+
+                    b.Property<string>("MangaConnectorName")
+                        .IsRequired()
+                        .HasMaxLength(32)
+                        .HasColumnType("character varying(32)");
+
+                    b.Property<string>("Name")
+                        .IsRequired()
+                        .HasMaxLength(512)
+                        .HasColumnType("character varying(512)");
+
+                    b.Property<string>("OriginalLanguage")
+                        .HasMaxLength(8)
+                        .HasColumnType("character varying(8)");
+
+                    b.Property<byte>("ReleaseStatus")
+                        .HasColumnType("smallint");
+
+                    b.Property<string>("WebsiteUrl")
+                        .IsRequired()
+                        .HasMaxLength(512)
+                        .HasColumnType("character varying(512)");
+
+                    b.Property<long?>("Year")
+                        .HasColumnType("bigint");
+
+                    b.HasKey("MangaId");
+
+                    b.HasIndex("LibraryId");
+
+                    b.HasIndex("MangaConnectorName");
+
+                    b.ToTable("Mangas");
+                });
+
+            modelBuilder.Entity("API.Schema.MangaConnectors.MangaConnector", b =>
+                {
+                    b.Property<string>("Name")
+                        .HasMaxLength(32)
+                        .HasColumnType("character varying(32)");
+
+                    b.PrimitiveCollection<string[]>("BaseUris")
+                        .IsRequired()
+                        .HasMaxLength(256)
+                        .HasColumnType("text[]");
+
+                    b.Property<bool>("Enabled")
+                        .HasColumnType("boolean");
+
+                    b.Property<string>("IconUrl")
+                        .IsRequired()
+                        .HasMaxLength(2048)
+                        .HasColumnType("character varying(2048)");
+
+                    b.PrimitiveCollection<string[]>("SupportedLanguages")
+                        .IsRequired()
+                        .HasMaxLength(8)
+                        .HasColumnType("text[]");
+
+                    b.HasKey("Name");
+
+                    b.ToTable("MangaConnectors");
+
+                    b.HasDiscriminator<string>("Name").HasValue("MangaConnector");
+
+                    b.UseTphMappingStrategy();
+                });
+
+            modelBuilder.Entity("API.Schema.MangaTag", b =>
+                {
+                    b.Property<string>("Tag")
+                        .HasMaxLength(64)
+                        .HasColumnType("character varying(64)");
+
+                    b.HasKey("Tag");
+
+                    b.ToTable("Tags");
+                });
+
+            modelBuilder.Entity("AuthorToManga", b =>
+                {
+                    b.Property<string>("AuthorIds")
+                        .HasColumnType("character varying(64)");
+
+                    b.Property<string>("MangaIds")
+                        .HasColumnType("character varying(64)");
+
+                    b.HasKey("AuthorIds", "MangaIds");
+
+                    b.HasIndex("MangaIds");
+
+                    b.ToTable("AuthorToManga");
+                });
+
+            modelBuilder.Entity("JobJob", b =>
+                {
+                    b.Property<string>("DependsOnJobsJobId")
+                        .HasColumnType("character varying(64)");
+
+                    b.Property<string>("JobId")
+                        .HasColumnType("character varying(64)");
+
+                    b.HasKey("DependsOnJobsJobId", "JobId");
+
+                    b.HasIndex("JobId");
+
+                    b.ToTable("JobJob");
+                });
+
+            modelBuilder.Entity("MangaTagToManga", b =>
+                {
+                    b.Property<string>("MangaTagIds")
+                        .HasColumnType("character varying(64)");
+
+                    b.Property<string>("MangaIds")
+                        .HasColumnType("character varying(64)");
+
+                    b.HasKey("MangaTagIds", "MangaIds");
+
+                    b.HasIndex("MangaIds");
+
+                    b.ToTable("MangaTagToManga");
+                });
+
+            modelBuilder.Entity("API.Schema.Jobs.DownloadAvailableChaptersJob", b =>
+                {
+                    b.HasBaseType("API.Schema.Jobs.Job");
+
+                    b.Property<string>("MangaId")
+                        .IsRequired()
+                        .HasMaxLength(64)
+                        .HasColumnType("character varying(64)");
+
+                    b.HasIndex("MangaId");
+
+                    b.ToTable("Jobs", t =>
+                        {
+                            t.Property("MangaId")
+                                .HasColumnName("DownloadAvailableChaptersJob_MangaId");
+                        });
+
+                    b.HasDiscriminator().HasValue((byte)1);
+                });
+
+            modelBuilder.Entity("API.Schema.Jobs.DownloadMangaCoverJob", b =>
+                {
+                    b.HasBaseType("API.Schema.Jobs.Job");
+
+                    b.Property<string>("MangaId")
+                        .IsRequired()
+                        .HasMaxLength(64)
+                        .HasColumnType("character varying(64)");
+
+                    b.HasIndex("MangaId");
+
+                    b.HasDiscriminator().HasValue((byte)4);
+                });
+
+            modelBuilder.Entity("API.Schema.Jobs.DownloadSingleChapterJob", b =>
+                {
+                    b.HasBaseType("API.Schema.Jobs.Job");
+
+                    b.Property<string>("ChapterId")
+                        .IsRequired()
+                        .HasMaxLength(64)
+                        .HasColumnType("character varying(64)");
+
+                    b.HasIndex("ChapterId");
+
+                    b.HasDiscriminator().HasValue((byte)0);
+                });
+
+            modelBuilder.Entity("API.Schema.Jobs.MoveFileOrFolderJob", b =>
+                {
+                    b.HasBaseType("API.Schema.Jobs.Job");
+
+                    b.Property<string>("FromLocation")
+                        .IsRequired()
+                        .HasMaxLength(256)
+                        .HasColumnType("character varying(256)");
+
+                    b.Property<string>("ToLocation")
+                        .IsRequired()
+                        .HasMaxLength(256)
+                        .HasColumnType("character varying(256)");
+
+                    b.HasDiscriminator().HasValue((byte)3);
+                });
+
+            modelBuilder.Entity("API.Schema.Jobs.MoveMangaLibraryJob", b =>
+                {
+                    b.HasBaseType("API.Schema.Jobs.Job");
+
+                    b.Property<string>("MangaId")
+                        .IsRequired()
+                        .HasMaxLength(64)
+                        .HasColumnType("character varying(64)");
+
+                    b.Property<string>("ToLibraryId")
+                        .IsRequired()
+                        .HasMaxLength(64)
+                        .HasColumnType("character varying(64)");
+
+                    b.HasIndex("MangaId");
+
+                    b.HasIndex("ToLibraryId");
+
+                    b.ToTable("Jobs", t =>
+                        {
+                            t.Property("MangaId")
+                                .HasColumnName("MoveMangaLibraryJob_MangaId");
+                        });
+
+                    b.HasDiscriminator().HasValue((byte)7);
+                });
+
+            modelBuilder.Entity("API.Schema.Jobs.RetrieveChaptersJob", b =>
+                {
+                    b.HasBaseType("API.Schema.Jobs.Job");
+
+                    b.Property<string>("Language")
+                        .IsRequired()
+                        .HasMaxLength(8)
+                        .HasColumnType("character varying(8)");
+
+                    b.Property<string>("MangaId")
+                        .IsRequired()
+                        .HasMaxLength(64)
+                        .HasColumnType("character varying(64)");
+
+                    b.HasIndex("MangaId");
+
+                    b.ToTable("Jobs", t =>
+                        {
+                            t.Property("MangaId")
+                                .HasColumnName("RetrieveChaptersJob_MangaId");
+                        });
+
+                    b.HasDiscriminator().HasValue((byte)5);
+                });
+
+            modelBuilder.Entity("API.Schema.Jobs.UpdateFilesDownloadedJob", b =>
+                {
+                    b.HasBaseType("API.Schema.Jobs.Job");
+
+                    b.Property<string>("MangaId")
+                        .IsRequired()
+                        .HasMaxLength(64)
+                        .HasColumnType("character varying(64)");
+
+                    b.HasIndex("MangaId");
+
+                    b.ToTable("Jobs", t =>
+                        {
+                            t.Property("MangaId")
+                                .HasColumnName("UpdateFilesDownloadedJob_MangaId");
+                        });
+
+                    b.HasDiscriminator().HasValue((byte)6);
+                });
+
+            modelBuilder.Entity("API.Schema.MangaConnectors.ComickIo", b =>
+                {
+                    b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
+
+                    b.HasDiscriminator().HasValue("ComickIo");
+                });
+
+            modelBuilder.Entity("API.Schema.MangaConnectors.Global", b =>
+                {
+                    b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
+
+                    b.HasDiscriminator().HasValue("Global");
+                });
+
+            modelBuilder.Entity("API.Schema.MangaConnectors.MangaDex", b =>
+                {
+                    b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
+
+                    b.HasDiscriminator().HasValue("MangaDex");
+                });
+
+            modelBuilder.Entity("API.Schema.Chapter", b =>
+                {
+                    b.HasOne("API.Schema.Manga", "ParentManga")
+                        .WithMany("Chapters")
+                        .HasForeignKey("ParentMangaId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("ParentManga");
+                });
+
+            modelBuilder.Entity("API.Schema.Jobs.Job", b =>
+                {
+                    b.HasOne("API.Schema.Jobs.Job", "ParentJob")
+                        .WithMany()
+                        .HasForeignKey("ParentJobId")
+                        .OnDelete(DeleteBehavior.Cascade);
+
+                    b.Navigation("ParentJob");
+                });
+
+            modelBuilder.Entity("API.Schema.Manga", b =>
+                {
+                    b.HasOne("API.Schema.LocalLibrary", "Library")
+                        .WithMany()
+                        .HasForeignKey("LibraryId")
+                        .OnDelete(DeleteBehavior.SetNull);
+
+                    b.HasOne("API.Schema.MangaConnectors.MangaConnector", "MangaConnector")
+                        .WithMany()
+                        .HasForeignKey("MangaConnectorName")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.OwnsMany("API.Schema.Link", "Links", b1 =>
+                        {
+                            b1.Property<string>("LinkId")
+                                .HasMaxLength(64)
+                                .HasColumnType("character varying(64)");
+
+                            b1.Property<string>("LinkProvider")
+                                .IsRequired()
+                                .HasMaxLength(64)
+                                .HasColumnType("character varying(64)");
+
+                            b1.Property<string>("LinkUrl")
+                                .IsRequired()
+                                .HasMaxLength(2048)
+                                .HasColumnType("character varying(2048)");
+
+                            b1.Property<string>("MangaId")
+                                .IsRequired()
+                                .HasColumnType("character varying(64)");
+
+                            b1.HasKey("LinkId");
+
+                            b1.HasIndex("MangaId");
+
+                            b1.ToTable("Link");
+
+                            b1.WithOwner()
+                                .HasForeignKey("MangaId");
+                        });
+
+                    b.OwnsMany("API.Schema.MangaAltTitle", "AltTitles", b1 =>
+                        {
+                            b1.Property<string>("AltTitleId")
+                                .HasMaxLength(64)
+                                .HasColumnType("character varying(64)");
+
+                            b1.Property<string>("Language")
+                                .IsRequired()
+                                .HasMaxLength(8)
+                                .HasColumnType("character varying(8)");
+
+                            b1.Property<string>("MangaId")
+                                .IsRequired()
+                                .HasColumnType("character varying(64)");
+
+                            b1.Property<string>("Title")
+                                .IsRequired()
+                                .HasMaxLength(256)
+                                .HasColumnType("character varying(256)");
+
+                            b1.HasKey("AltTitleId");
+
+                            b1.HasIndex("MangaId");
+
+                            b1.ToTable("MangaAltTitle");
+
+                            b1.WithOwner()
+                                .HasForeignKey("MangaId");
+                        });
+
+                    b.Navigation("AltTitles");
+
+                    b.Navigation("Library");
+
+                    b.Navigation("Links");
+
+                    b.Navigation("MangaConnector");
+                });
+
+            modelBuilder.Entity("AuthorToManga", b =>
+                {
+                    b.HasOne("API.Schema.Author", null)
+                        .WithMany()
+                        .HasForeignKey("AuthorIds")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.HasOne("API.Schema.Manga", null)
+                        .WithMany()
+                        .HasForeignKey("MangaIds")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+                });
+
+            modelBuilder.Entity("JobJob", b =>
+                {
+                    b.HasOne("API.Schema.Jobs.Job", null)
+                        .WithMany()
+                        .HasForeignKey("DependsOnJobsJobId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.HasOne("API.Schema.Jobs.Job", null)
+                        .WithMany()
+                        .HasForeignKey("JobId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+                });
+
+            modelBuilder.Entity("MangaTagToManga", b =>
+                {
+                    b.HasOne("API.Schema.Manga", null)
+                        .WithMany()
+                        .HasForeignKey("MangaIds")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.HasOne("API.Schema.MangaTag", null)
+                        .WithMany()
+                        .HasForeignKey("MangaTagIds")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+                });
+
+            modelBuilder.Entity("API.Schema.Jobs.DownloadAvailableChaptersJob", b =>
+                {
+                    b.HasOne("API.Schema.Manga", "Manga")
+                        .WithMany()
+                        .HasForeignKey("MangaId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("Manga");
+                });
+
+            modelBuilder.Entity("API.Schema.Jobs.DownloadMangaCoverJob", b =>
+                {
+                    b.HasOne("API.Schema.Manga", "Manga")
+                        .WithMany()
+                        .HasForeignKey("MangaId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("Manga");
+                });
+
+            modelBuilder.Entity("API.Schema.Jobs.DownloadSingleChapterJob", b =>
+                {
+                    b.HasOne("API.Schema.Chapter", "Chapter")
+                        .WithMany()
+                        .HasForeignKey("ChapterId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("Chapter");
+                });
+
+            modelBuilder.Entity("API.Schema.Jobs.MoveMangaLibraryJob", b =>
+                {
+                    b.HasOne("API.Schema.Manga", "Manga")
+                        .WithMany()
+                        .HasForeignKey("MangaId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.HasOne("API.Schema.LocalLibrary", "ToLibrary")
+                        .WithMany()
+                        .HasForeignKey("ToLibraryId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("Manga");
+
+                    b.Navigation("ToLibrary");
+                });
+
+            modelBuilder.Entity("API.Schema.Jobs.RetrieveChaptersJob", b =>
+                {
+                    b.HasOne("API.Schema.Manga", "Manga")
+                        .WithMany()
+                        .HasForeignKey("MangaId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("Manga");
+                });
+
+            modelBuilder.Entity("API.Schema.Jobs.UpdateFilesDownloadedJob", b =>
+                {
+                    b.HasOne("API.Schema.Manga", "Manga")
+                        .WithMany()
+                        .HasForeignKey("MangaId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("Manga");
+                });
+
+            modelBuilder.Entity("API.Schema.Manga", b =>
+                {
+                    b.Navigation("Chapters");
+                });
+#pragma warning restore 612, 618
+        }
+    }
+}
diff --git a/API/Migrations/pgsql/20250516122242_AltTitle-Owned-WithId.cs b/API/Migrations/pgsql/20250516122242_AltTitle-Owned-WithId.cs
new file mode 100644
index 0000000..3814d95
--- /dev/null
+++ b/API/Migrations/pgsql/20250516122242_AltTitle-Owned-WithId.cs
@@ -0,0 +1,70 @@
+using Microsoft.EntityFrameworkCore.Migrations;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+
+#nullable disable
+
+namespace API.Migrations.pgsql
+{
+    /// <inheritdoc />
+    public partial class AltTitleOwnedWithId : Migration
+    {
+        /// <inheritdoc />
+        protected override void Up(MigrationBuilder migrationBuilder)
+        {
+            migrationBuilder.DropPrimaryKey(
+                name: "PK_MangaAltTitle",
+                table: "MangaAltTitle");
+
+            migrationBuilder.DropColumn(
+                name: "Id",
+                table: "MangaAltTitle");
+
+            migrationBuilder.AddColumn<string>(
+                name: "AltTitleId",
+                table: "MangaAltTitle",
+                type: "character varying(64)",
+                maxLength: 64,
+                nullable: false,
+                defaultValue: "");
+
+            migrationBuilder.AddPrimaryKey(
+                name: "PK_MangaAltTitle",
+                table: "MangaAltTitle",
+                column: "AltTitleId");
+
+            migrationBuilder.CreateIndex(
+                name: "IX_MangaAltTitle_MangaId",
+                table: "MangaAltTitle",
+                column: "MangaId");
+        }
+
+        /// <inheritdoc />
+        protected override void Down(MigrationBuilder migrationBuilder)
+        {
+            migrationBuilder.DropPrimaryKey(
+                name: "PK_MangaAltTitle",
+                table: "MangaAltTitle");
+
+            migrationBuilder.DropIndex(
+                name: "IX_MangaAltTitle_MangaId",
+                table: "MangaAltTitle");
+
+            migrationBuilder.DropColumn(
+                name: "AltTitleId",
+                table: "MangaAltTitle");
+
+            migrationBuilder.AddColumn<int>(
+                name: "Id",
+                table: "MangaAltTitle",
+                type: "integer",
+                nullable: false,
+                defaultValue: 0)
+                .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
+
+            migrationBuilder.AddPrimaryKey(
+                name: "PK_MangaAltTitle",
+                table: "MangaAltTitle",
+                columns: new[] { "MangaId", "Id" });
+        }
+    }
+}
diff --git a/API/Migrations/pgsql/PgsqlContextModelSnapshot.cs b/API/Migrations/pgsql/PgsqlContextModelSnapshot.cs
index d7dd6c6..623bec6 100644
--- a/API/Migrations/pgsql/PgsqlContextModelSnapshot.cs
+++ b/API/Migrations/pgsql/PgsqlContextModelSnapshot.cs
@@ -195,7 +195,7 @@ namespace API.Migrations.pgsql
                         .HasMaxLength(512)
                         .HasColumnType("character varying(512)");
 
-                    b.Property<long>("Year")
+                    b.Property<long?>("Year")
                         .HasColumnType("bigint");
 
                     b.HasKey("MangaId");
@@ -433,6 +433,13 @@ namespace API.Migrations.pgsql
                     b.HasDiscriminator().HasValue((byte)6);
                 });
 
+            modelBuilder.Entity("API.Schema.MangaConnectors.ComickIo", b =>
+                {
+                    b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
+
+                    b.HasDiscriminator().HasValue("ComickIo");
+                });
+
             modelBuilder.Entity("API.Schema.MangaConnectors.Global", b =>
                 {
                     b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
diff --git a/API/Program.cs b/API/Program.cs
index e9f59a5..1953c47 100644
--- a/API/Program.cs
+++ b/API/Program.cs
@@ -113,6 +113,7 @@ using (IServiceScope scope = app.Services.CreateScope())
     MangaConnector[] connectors =
     [
         new MangaDex(),
+        new ComickIo(),
         new Global(scope.ServiceProvider.GetService<PgsqlContext>()!)
     ];
     MangaConnector[] newConnectors = connectors.Where(c => !context.MangaConnectors.Contains(c)).ToArray();
diff --git a/API/Schema/Contexts/PgsqlContext.cs b/API/Schema/Contexts/PgsqlContext.cs
index fc1f1f9..2b138e2 100644
--- a/API/Schema/Contexts/PgsqlContext.cs
+++ b/API/Schema/Contexts/PgsqlContext.cs
@@ -1,5 +1,4 @@
 using API.Schema.Jobs;
-using API.Schema.LibraryConnectors;
 using API.Schema.MangaConnectors;
 using Microsoft.EntityFrameworkCore;
 
@@ -112,7 +111,8 @@ public class PgsqlContext(DbContextOptions<PgsqlContext> options) : DbContext(op
         modelBuilder.Entity<MangaConnector>()
             .HasDiscriminator(c => c.Name)
             .HasValue<Global>("Global")
-            .HasValue<MangaDex>("MangaDex");
+            .HasValue<MangaDex>("MangaDex")
+            .HasValue<ComickIo>("ComickIo");
         //MangaConnector is responsible for many Manga
         modelBuilder.Entity<MangaConnector>()
             .HasMany<Manga>()
diff --git a/API/Schema/MangaConnectors/ComickIo.cs b/API/Schema/MangaConnectors/ComickIo.cs
new file mode 100644
index 0000000..cb8a5c2
--- /dev/null
+++ b/API/Schema/MangaConnectors/ComickIo.cs
@@ -0,0 +1,246 @@
+using System.Text.RegularExpressions;
+using API.MangaDownloadClients;
+using Newtonsoft.Json.Linq;
+
+namespace API.Schema.MangaConnectors;
+
+public class ComickIo : MangaConnector
+{
+    //https://api.comick.io/docs/
+    //https://en.wikipedia.org/wiki/List_of_ISO_639_language_codes
+    
+    public ComickIo() : base("ComickIo", 
+        ["en","pt","pt-br","it","de","ru","aa","ab","ae","af","ak","am","an","ar-ae","ar-bh","ar-dz","ar-eg","ar-iq","ar-jo","ar-kw","ar-lb","ar-ly","ar-ma","ar-om","ar-qa","ar-sa","ar-sy","ar-tn","ar-ye","ar","as","av","ay","az","ba","be","bg","bh","bi","bm","bn","bo","br","bs","ca","ce","ch","co","cr","cs","cu","cv","cy","da","de-at","de-ch","de-de","de-li","de-lu","div","dv","dz","ee","el","en-au","en-bz","en-ca","en-cb","en-gb","en-ie","en-jm","en-nz","en-ph","en-tt","en-us","en-za","en-zw","eo","es-ar","es-bo","es-cl","es-co","es-cr","es-do","es-ec","es-es","es-gt","es-hn","es-la","es-mx","es-ni","es-pa","es-pe","es-pr","es-py","es-sv","es-us","es-uy","es-ve","es","et","eu","fa","ff","fi","fj","fo","fr-be","fr-ca","fr-ch","fr-fr","fr-lu","fr-mc","fr","fy","ga","gd","gl","gn","gu","gv","ha","he","hi","ho","hr-ba","hr-hr","hr","ht","hu","hy","hz","ia","id","ie","ig","ii","ik","in","io","is","it-ch","it-it","iu","iw","ja","ja-ro","ji","jv","jw","ka","kg","ki","kj","kk","kl","km","kn","ko","ko-ro","kr","ks","ku","kv","kw","ky","kz","la","lb","lg","li","ln","lo","ls","lt","lu","lv","mg","mh","mi","mk","ml","mn","mo","mr","ms-bn","ms-my","ms","mt","my","na","nb","nd","ne","ng","nl-be","nl-nl","nl","nn","no","nr","ns","nv","ny","oc","oj","om","or","os","pa","pi","pl","ps","pt-pt","qu-bo","qu-ec","qu-pe","qu","rm","rn","ro","rw","sa","sb","sc","sd","se-fi","se-no","se-se","se","sg","sh","si","sk","sl","sm","sn","so","sq","sr-ba","sr-sp","sr","ss","st","su","sv-fi","sv-se","sv","sw","sx","syr","ta","te","tg","th","ti","tk","tl","tn","to","tr","ts","tt","tw","ty","ug","uk","ur","us","uz","ve","vi","vo","wa","wo","xh","yi","yo","za","zh-cn","zh-hk","zh-mo","zh-ro","zh-sg","zh-tw","zh","zu"],
+        ["comick.io"], 
+        "https://comick.io/static/icons/unicorn-64.png")
+    {
+        this.downloadClient = new HttpDownloadClient();
+    }
+
+    public override Manga[] SearchManga(string mangaSearchName)
+    {
+        Log.Info($"Searching Manga: {mangaSearchName}");
+
+        List<string> slugs = new();
+        int page = 1;
+        while(page < 50)
+        {
+            string requestUrl = $"https://api.comick.fun/v1.0/search/?type=comic&t=false&limit=100&showall=true&" +
+                                $"page={page}&q={mangaSearchName}";
+
+            RequestResult result = downloadClient.MakeRequest(requestUrl, RequestType.Default);
+            if ((int)result.statusCode < 200 || (int)result.statusCode >= 300)
+            {
+                Log.Error("Request failed");
+                return [];
+            }
+
+            using StreamReader sr = new (result.result);
+            JArray data = JArray.Parse(sr.ReadToEnd());
+
+            if (data.Count < 1)
+                break;
+            
+            slugs.AddRange(data.Select(token => token.Value<string>("slug")!));
+            page++;
+        }
+        Log.Debug($"Search {mangaSearchName} yielded {slugs.Count} slugs. Requesting mangas now...");
+
+        List<Manga> mangas = slugs.Select(GetMangaFromId).ToList()!;
+        
+        Log.Info($"Search {mangaSearchName} yielded {mangas.Count} results.");
+        return mangas.ToArray();
+    }
+
+    private readonly Regex _getSlugFromTitleRex = new(@"https?:\/\/comick\.io\/comic\/(.+)(?:\/.*)*");
+    public override Manga? GetMangaFromUrl(string url)
+    {
+        Match m = _getSlugFromTitleRex.Match(url);
+        return m.Groups[1].Success ? GetMangaFromId(m.Groups[1].Value) : null;
+    }
+
+    public override Manga? GetMangaFromId(string mangaIdOnSite)
+    {
+        string requestUrl = $"https://api.comick.fun/comic/{mangaIdOnSite}";
+
+        RequestResult result = downloadClient.MakeRequest(requestUrl, RequestType.Default);
+        if ((int)result.statusCode < 200 || (int)result.statusCode >= 300)
+        {
+            Log.Error("Request failed");
+            return null;
+        }
+        using StreamReader sr = new (result.result);
+        JToken data = JToken.Parse(sr.ReadToEnd());
+
+        return ParseMangaFromJToken(data);
+    }
+
+    public override Chapter[] GetChapters(Manga manga, string? language = null)
+    {
+        Log.Info($"Getting Chapters: {manga.IdOnConnectorSite}");
+        List<string> chapterHids = new();
+        int page = 1;
+        while(page < 50)
+        {
+            string requestUrl = $"https://api.comick.fun/comic/{manga.IdOnConnectorSite}/chapters?limit=100&page={page}";
+
+            RequestResult result = downloadClient.MakeRequest(requestUrl, RequestType.Default);
+            if ((int)result.statusCode < 200 || (int)result.statusCode >= 300)
+            {
+                Log.Error("Request failed");
+                return [];
+            }
+
+            using StreamReader sr = new (result.result);
+            JArray data = JArray.Parse(sr.ReadToEnd());
+
+            if (data.Count < 1)
+                break;
+            
+            chapterHids.AddRange(data.Select(token => token.Value<string>("hid")!));
+
+            page++;
+        }
+        Log.Debug($"Getting chapters for {manga.Name} yielded {chapterHids.Count} hids. Requesting chapters now...");
+
+        List<Chapter> chapters = chapterHids.Select(hid => ChapterFromHid(manga, hid)).ToList();
+
+        return chapters.ToArray();
+    }
+
+    private readonly Regex _hidFromUrl = new(@"https?:\/\/comick\.io\/comic\/.+\/([^-]+).*");
+    internal override string[] GetChapterImageUrls(Chapter chapter)
+    {
+        Match m = _hidFromUrl.Match(chapter.Url);
+        if (!m.Groups[1].Success)
+            return [];
+        
+        string hid = m.Groups[1].Value;
+        
+        string requestUrl = $"https://api.comick.fun/chapter/{hid}/get_images";
+        RequestResult result = downloadClient.MakeRequest(requestUrl, RequestType.Default);
+        if ((int)result.statusCode < 200 || (int)result.statusCode >= 300)
+        {
+            Log.Error("Request failed");
+            return [];
+        }
+        
+        using StreamReader sr = new (result.result);
+        JArray data = JArray.Parse(sr.ReadToEnd());
+
+        return data.Select(token =>
+        {
+            string url = $"https://meo.comick.pictures/{token.Value<string>("b2key")}";
+            return url;
+        }).ToArray();
+    }
+
+    private Manga ParseMangaFromJToken(JToken json)
+    {
+        string? hid = json["comic"]?.Value<string>("hid");
+        string? slug = json["comic"]?.Value<string>("slug");
+        string? name = json["comic"]?.Value<string>("title");
+        string? description = json["comic"]?.Value<string>("desc");
+        string? originalLanguage = json["comic"]?.Value<string>("country");
+        string url = $"https://comick.io/comic/{slug}";
+        string? coverName = json["comic"]?["md_covers"]?.First?.Value<string>("b2key");
+        string coverUrl = $"https://meo.comick.pictures/{coverName}";
+        int? releaseStatusStr = json["comic"]?.Value<int>("status");
+        MangaReleaseStatus status = releaseStatusStr switch
+        {
+            1 => MangaReleaseStatus.Continuing,
+            2 => MangaReleaseStatus.Completed,
+            3 => MangaReleaseStatus.Cancelled,
+            4 => MangaReleaseStatus.OnHiatus,
+            _ => MangaReleaseStatus.Unreleased
+        };
+        uint? year = json["comic"]?.Value<uint?>("year");
+        JArray? altTitlesArray = json["comic"]?["md_titles"] as JArray;
+        //Cant let language be null, so fill with whatever.
+        byte whatever = 0;
+        List<MangaAltTitle> altTitles = altTitlesArray?
+            .Select(token => new MangaAltTitle(token.Value<string>("lang")??whatever++.ToString(), token.Value<string>("title")!))
+            .ToList()!;
+        
+        JArray? authorsArray = json["authors"] as JArray;
+        JArray? artistsArray = json["artists"] as JArray;
+        List<Author> authors = authorsArray?.Concat(artistsArray!)
+            .Select(token => new Author(token.Value<string>("name")!))
+            .DistinctBy(a => a.AuthorId)
+            .ToList()!;
+        
+        JArray? genreArray = json["comic"]?["md_comic_md_genres"] as JArray;
+        List<MangaTag> tags = genreArray?
+            .Select(token => new MangaTag(token["md_genres"]?.Value<string>("name")!))
+            .ToList()!;
+        
+        JArray? linksArray = json["comic"]?["links"] as JArray;
+        List<Link> links = linksArray?
+            .ToObject<Dictionary<string,string>>()?
+            .Select(kv =>
+            {
+                string fullUrl = kv.Key switch
+                {
+                    "al" => $"https://anilist.co/manga/{kv.Value}",
+                    "ap" => $"https://www.anime-planet.com/manga/{kv.Value}",
+                    "bw" => $"https://bookwalker.jp/{kv.Value}",
+                    "mu" => $"https://www.mangaupdates.com/series.html?id={kv.Value}",
+                    "nu" => $"https://www.novelupdates.com/series/{kv.Value}",
+                    "mal" => $"https://myanimelist.net/manga/{kv.Value}",
+                    _ => kv.Value
+                };
+                string key = kv.Key switch
+                {
+                    "al" => "AniList",
+                    "ap" => "Anime Planet",
+                    "bw" => "BookWalker",
+                    "mu" => "Manga Updates",
+                    "nu" => "Novel Updates",
+                    "kt" => "Kitsu.io",
+                    "amz" => "Amazon",
+                    "ebj" => "eBookJapan",
+                    "mal" => "MyAnimeList",
+                    "cdj" => "CDJapan",
+                    _ => kv.Key
+                };
+                return new Link(key, fullUrl);
+            }).ToList()!;
+        
+        if(hid is null)
+            throw new Exception("hid is null");
+        if(slug is null)
+            throw new Exception("slug is null");
+        if(name is null)
+            throw new Exception("name is null");
+        
+        return new Manga(hid, name, description??"", url, coverUrl, status, this,
+            authors, tags, links, altTitles,
+            year: year, originalLanguage: originalLanguage);
+    }
+
+    private Chapter ChapterFromHid(Manga parentManga, string hid)
+    {
+        string requestUrl = $"https://api.comick.fun/chapter/{hid}";
+        RequestResult result = downloadClient.MakeRequest(requestUrl, RequestType.Default);
+        if ((int)result.statusCode < 200 || (int)result.statusCode >= 300)
+        {
+            Log.Error("Request failed");
+            throw new Exception("Request failed");
+        }
+        
+        using StreamReader sr = new (result.result);
+        JToken data = JToken.Parse(sr.ReadToEnd());
+
+        string? canonical = data.Value<string>("canonical");
+        string? chapterNum = data["chapter"]?.Value<string>("chap");
+        string? volumeNumStr = data["chapter"]?.Value<string>("vol");
+        int? volumeNum = volumeNumStr is null ? null : int.Parse(volumeNumStr);
+        string? title = data["chapter"]?.Value<string>("title");
+        
+        if(chapterNum is null)
+            throw new Exception("chapterNum is null");
+
+        string url = $"https://comick.io{canonical}";
+        return new Chapter(parentManga, url, chapterNum, volumeNum, title);
+    }
+}
\ No newline at end of file

From 16f5817a31c2260d8bab93800ae31123f83f0ab2 Mon Sep 17 00:00:00 2001
From: Glax <johanna@bernloehr.eu>
Date: Fri, 16 May 2025 14:36:29 +0200
Subject: [PATCH 13/50] Fix ComickIo Chapter-Loading

---
 API/Schema/MangaConnectors/ComickIo.cs | 9 +++++----
 1 file changed, 5 insertions(+), 4 deletions(-)

diff --git a/API/Schema/MangaConnectors/ComickIo.cs b/API/Schema/MangaConnectors/ComickIo.cs
index cb8a5c2..67a1f65 100644
--- a/API/Schema/MangaConnectors/ComickIo.cs
+++ b/API/Schema/MangaConnectors/ComickIo.cs
@@ -82,7 +82,7 @@ public class ComickIo : MangaConnector
         int page = 1;
         while(page < 50)
         {
-            string requestUrl = $"https://api.comick.fun/comic/{manga.IdOnConnectorSite}/chapters?limit=100&page={page}";
+            string requestUrl = $"https://api.comick.fun/comic/{manga.IdOnConnectorSite}/chapters?limit=100&page={page}&lang={language}";
 
             RequestResult result = downloadClient.MakeRequest(requestUrl, RequestType.Default);
             if ((int)result.statusCode < 200 || (int)result.statusCode >= 300)
@@ -92,12 +92,13 @@ public class ComickIo : MangaConnector
             }
 
             using StreamReader sr = new (result.result);
-            JArray data = JArray.Parse(sr.ReadToEnd());
+            JToken data = JToken.Parse(sr.ReadToEnd());
+            JArray? chaptersArray = data["chapters"] as JArray;
 
-            if (data.Count < 1)
+            if (chaptersArray?.Count < 1)
                 break;
             
-            chapterHids.AddRange(data.Select(token => token.Value<string>("hid")!));
+            chapterHids.AddRange(chaptersArray?.Select(token => token.Value<string>("hid")!)!);
 
             page++;
         }

From f6f86deb7f1d9d8bf296043b6d9e7391f965e2fc Mon Sep 17 00:00:00 2001
From: Glax <johanna@bernloehr.eu>
Date: Fri, 16 May 2025 14:36:48 +0200
Subject: [PATCH 14/50] Add Debug output

---
 API/Tranga.cs | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/API/Tranga.cs b/API/Tranga.cs
index 1024568..d844a95 100644
--- a/API/Tranga.cs
+++ b/API/Tranga.cs
@@ -191,7 +191,9 @@ public static class Tranga
         dueJobs
             .Where(j =>
             {
-                context.Entry(j).Collection(j => j.DependsOnJobs).Load(LoadOptions.ForceIdentityResolution);
+                Log.Debug($"Loading Job Preconditions {j}...");
+                context.Entry(j).Collection(j => j.DependsOnJobs).Load();
+                Log.Debug($"Loaded Job Preconditions {j}!");
                 return j.DependenciesFulfilled;
             })
             .Where(j =>

From d08544b892549d0deae84cf57fd2f3ba90d4e341 Mon Sep 17 00:00:00 2001
From: Glax <johanna@bernloehr.eu>
Date: Fri, 16 May 2025 14:38:47 +0200
Subject: [PATCH 15/50] Sending notifications for -> Debug instead of Info

---
 API/Tranga.cs | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/API/Tranga.cs b/API/Tranga.cs
index d844a95..150a12c 100644
--- a/API/Tranga.cs
+++ b/API/Tranga.cs
@@ -58,7 +58,7 @@ public static class Tranga
 
     private static void SendNotifications(IServiceProvider serviceProvider, NotificationUrgency urgency)
     {
-        Log.Info($"Sending notifications for {urgency}");
+        Log.Debug($"Sending notifications for {urgency}");
         using IServiceScope scope = serviceProvider.CreateScope();
         NotificationsContext context = scope.ServiceProvider.GetRequiredService<NotificationsContext>();
         

From a5954ed5c86672b2bd35471f654e043d4ebb2ff0 Mon Sep 17 00:00:00 2001
From: Glax <johanna@bernloehr.eu>
Date: Fri, 16 May 2025 14:46:36 +0200
Subject: [PATCH 16/50] DownloadSingleChapterJob only spawn one Job per Queue
 for Manga

---
 API/Schema/Jobs/DownloadSingleChapterJob.cs | 11 ++++++++++-
 1 file changed, 10 insertions(+), 1 deletion(-)

diff --git a/API/Schema/Jobs/DownloadSingleChapterJob.cs b/API/Schema/Jobs/DownloadSingleChapterJob.cs
index 9e4f94f..e7a4d1d 100644
--- a/API/Schema/Jobs/DownloadSingleChapterJob.cs
+++ b/API/Schema/Jobs/DownloadSingleChapterJob.cs
@@ -97,7 +97,16 @@ public class DownloadSingleChapterJob : Job
         Chapter.Downloaded = true;
         context.SaveChanges();
 
-        return [new UpdateFilesDownloadedJob(Chapter.ParentManga, 0, this)];
+        if (context.Jobs.AsEnumerable().Any(j =>
+            {
+                if (j.JobType != JobType.UpdateFilesDownloadedJob)
+                    return false;
+                UpdateFilesDownloadedJob job = (UpdateFilesDownloadedJob)j;
+                return job.MangaId == this.Chapter.ParentMangaId;
+            }))
+            return [];
+
+        return [new UpdateFilesDownloadedJob(Chapter.ParentManga, 0, this.ParentJob)];
     }
     
     private void ProcessImage(string imagePath)

From 4247ae7740fb5adaabb57ddb81d5976b2b00c2f7 Mon Sep 17 00:00:00 2001
From: Glax <johanna@bernloehr.eu>
Date: Fri, 16 May 2025 15:15:11 +0200
Subject: [PATCH 17/50] Do not autoinclude chapters for Manga

---
 API/Schema/Contexts/PgsqlContext.cs         | 2 +-
 API/Schema/Jobs/MoveMangaLibraryJob.cs      | 1 +
 API/Schema/Jobs/UpdateFilesDownloadedJob.cs | 1 +
 3 files changed, 3 insertions(+), 1 deletion(-)

diff --git a/API/Schema/Contexts/PgsqlContext.cs b/API/Schema/Contexts/PgsqlContext.cs
index 2b138e2..7308472 100644
--- a/API/Schema/Contexts/PgsqlContext.cs
+++ b/API/Schema/Contexts/PgsqlContext.cs
@@ -136,7 +136,7 @@ public class PgsqlContext(DbContextOptions<PgsqlContext> options) : DbContext(op
             .AutoInclude();
         modelBuilder.Entity<Manga>()
             .Navigation(m => m.Chapters)
-            .AutoInclude();
+            .AutoInclude(false);
         //Manga owns MangaAltTitles
         modelBuilder.Entity<Manga>()
             .OwnsMany<MangaAltTitle>(m => m.AltTitles)
diff --git a/API/Schema/Jobs/MoveMangaLibraryJob.cs b/API/Schema/Jobs/MoveMangaLibraryJob.cs
index a533e9c..76dcafa 100644
--- a/API/Schema/Jobs/MoveMangaLibraryJob.cs
+++ b/API/Schema/Jobs/MoveMangaLibraryJob.cs
@@ -34,6 +34,7 @@ public class MoveMangaLibraryJob : Job
     protected override IEnumerable<Job> RunInternal(PgsqlContext context)
     {
         context.Attach(Manga);
+        context.Entry(Manga).Collection<Chapter>(m => m.Chapters).Load();
         Dictionary<Chapter, string> oldPath = Manga.Chapters.ToDictionary(c => c, c => c.FullArchiveFilePath);
         Manga.Library = ToLibrary;
         try
diff --git a/API/Schema/Jobs/UpdateFilesDownloadedJob.cs b/API/Schema/Jobs/UpdateFilesDownloadedJob.cs
index b0497ee..962b0d2 100644
--- a/API/Schema/Jobs/UpdateFilesDownloadedJob.cs
+++ b/API/Schema/Jobs/UpdateFilesDownloadedJob.cs
@@ -29,6 +29,7 @@ public class UpdateFilesDownloadedJob : Job
     protected override IEnumerable<Job> RunInternal(PgsqlContext context)
     {
         context.Attach(Manga);
+        context.Entry(Manga).Collection<Chapter>(m => m.Chapters).Load();
         foreach (Chapter chapter in Manga.Chapters)
             chapter.Downloaded = chapter.CheckDownloaded();
 

From 7e1c65b470c43f3c0265cd9e8f6c5f3ecc5fd174 Mon Sep 17 00:00:00 2001
From: Glax <johanna@bernloehr.eu>
Date: Fri, 16 May 2025 15:15:34 +0200
Subject: [PATCH 18/50] Optimize requests for JobStarter

---
 API/Tranga.cs | 10 ++++++----
 1 file changed, 6 insertions(+), 4 deletions(-)

diff --git a/API/Tranga.cs b/API/Tranga.cs
index 150a12c..3cc65b8 100644
--- a/API/Tranga.cs
+++ b/API/Tranga.cs
@@ -116,7 +116,8 @@ public static class Tranga
             foreach (EntityEntry entityEntry in context.ChangeTracker.Entries().ToArray())
                 entityEntry.Reload();
             //Update finished Jobs to new states
-            List<Job> completedJobs = context.Jobs.Where(j => j.state == JobState.Completed).ToList();
+            context.Jobs.Load();
+            List<Job> completedJobs = context.Jobs.Local.Where(j => j.state == JobState.Completed).ToList();
             foreach (Job completedJob in completedJobs)
                 if (completedJob.RecurrenceMs <= 0)
                     context.Jobs.Remove(completedJob);
@@ -125,7 +126,7 @@ public static class Tranga
                     completedJob.state = JobState.CompletedWaiting;
                     completedJob.LastExecution = DateTime.UtcNow;
                 }
-            List<Job> failedJobs = context.Jobs.Where(j => j.state == JobState.Failed).ToList();
+            List<Job> failedJobs = context.Jobs.Local.Where(j => j.state == JobState.Failed).ToList();
             foreach (Job failedJob in failedJobs)
             {
                 failedJob.Enabled = false;
@@ -133,9 +134,9 @@ public static class Tranga
             }
 
             //Retrieve waiting and due Jobs
-            List<Job> waitingJobs = context.Jobs.Where(j =>
+            List<Job> waitingJobs = context.Jobs.Local.Where(j =>
                 j.Enabled && (j.state == JobState.FirstExecution || j.state == JobState.CompletedWaiting)).ToList();
-            List<Job> runningJobs = context.Jobs.Where(j => j.state == JobState.Running).ToList();
+            List<Job> runningJobs = context.Jobs.Local.Where(j => j.state == JobState.Running).ToList();
             List<Job> dueJobs = waitingJobs.Where(j => j.NextExecution < DateTime.UtcNow).ToList();
 
             List<MangaConnector> busyConnectors = GetBusyConnectors(runningJobs);
@@ -203,6 +204,7 @@ public static class Tranga
                     return busyConnectors.Contains(mangaConnector) == false;
                 return true;
             })
+            .DistinctBy(j => j.JobType)
             .ToList();
 
     private static MangaConnector? GetJobConnector(Job job)

From f3c4b012b06b2e71a28322e272e393d0cd569404 Mon Sep 17 00:00:00 2001
From: Glax <johanna@bernloehr.eu>
Date: Fri, 16 May 2025 19:17:13 +0200
Subject: [PATCH 19/50] ToString Override for RequestResult

---
 API/MangaDownloadClients/RequestResult.cs | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/API/MangaDownloadClients/RequestResult.cs b/API/MangaDownloadClients/RequestResult.cs
index b42cfba..ae9cec1 100644
--- a/API/MangaDownloadClients/RequestResult.cs
+++ b/API/MangaDownloadClients/RequestResult.cs
@@ -24,4 +24,10 @@ public struct RequestResult
         this.hasBeenRedirected = hasBeenRedirected;
         redirectedToUrl = redirectedTo;
     }
+
+    public override string ToString()
+    {
+        return
+            $"{(int)statusCode} {statusCode.ToString()} {(hasBeenRedirected ? "Redirected: " : "")} {redirectedToUrl}";
+    }
 }
\ No newline at end of file

From 9e62eb53cb44ba6db0efde65e85e98197cf220f0 Mon Sep 17 00:00:00 2001
From: Glax <johanna@bernloehr.eu>
Date: Fri, 16 May 2025 19:17:29 +0200
Subject: [PATCH 20/50] Log-Output for DownloadClient improved

---
 API/MangaDownloadClients/DownloadClient.cs | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

diff --git a/API/MangaDownloadClients/DownloadClient.cs b/API/MangaDownloadClients/DownloadClient.cs
index 7356fde..542d552 100644
--- a/API/MangaDownloadClients/DownloadClient.cs
+++ b/API/MangaDownloadClients/DownloadClient.cs
@@ -15,7 +15,7 @@ internal abstract class DownloadClient
     
     public RequestResult MakeRequest(string url, RequestType requestType, string? referrer = null, string? clickButton = null)
     {
-        Log.Debug($"Requesting {url}");
+        Log.Debug($"Requesting {requestType} {url}");
         if (!TrangaSettings.requestLimits.ContainsKey(requestType))
         {
             return new RequestResult(HttpStatusCode.NotAcceptable, null, Stream.Null);
@@ -30,7 +30,7 @@ internal abstract class DownloadClient
         LastExecutedRateLimit.TryAdd(requestType, now.Subtract(timeBetweenRequests));
 
         TimeSpan rateLimitTimeout = timeBetweenRequests.Subtract(now.Subtract(LastExecutedRateLimit[requestType]));
-        Log.Debug($"Request limit {rateLimit}/Minute timeBetweenRequests: {timeBetweenRequests:ss'.'fffff} Timeout: {rateLimitTimeout:ss'.'fffff}");
+        Log.Debug($"Request limit {requestType} {rateLimit}/Minute timeBetweenRequests: {timeBetweenRequests:ss'.'fffff} Timeout: {rateLimitTimeout:ss'.'fffff}");
         
         if (rateLimitTimeout > TimeSpan.Zero)
         {
@@ -39,6 +39,7 @@ internal abstract class DownloadClient
 
         RequestResult result = MakeRequestInternal(url, referrer, clickButton);
         LastExecutedRateLimit[requestType] = DateTime.UtcNow;
+        Log.Debug($"Result {url}: {result}");
         return result;
     }
 

From 1792952039ebd61c2a9beda8573e27b15cee83f7 Mon Sep 17 00:00:00 2001
From: Glax <johanna@bernloehr.eu>
Date: Fri, 16 May 2025 19:17:45 +0200
Subject: [PATCH 21/50] Fix RequestTypes for ComickIo

---
 API/Schema/MangaConnectors/ComickIo.cs | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/API/Schema/MangaConnectors/ComickIo.cs b/API/Schema/MangaConnectors/ComickIo.cs
index 67a1f65..707e421 100644
--- a/API/Schema/MangaConnectors/ComickIo.cs
+++ b/API/Schema/MangaConnectors/ComickIo.cs
@@ -63,7 +63,7 @@ public class ComickIo : MangaConnector
     {
         string requestUrl = $"https://api.comick.fun/comic/{mangaIdOnSite}";
 
-        RequestResult result = downloadClient.MakeRequest(requestUrl, RequestType.Default);
+        RequestResult result = downloadClient.MakeRequest(requestUrl, RequestType.MangaInfo);
         if ((int)result.statusCode < 200 || (int)result.statusCode >= 300)
         {
             Log.Error("Request failed");
@@ -84,7 +84,7 @@ public class ComickIo : MangaConnector
         {
             string requestUrl = $"https://api.comick.fun/comic/{manga.IdOnConnectorSite}/chapters?limit=100&page={page}&lang={language}";
 
-            RequestResult result = downloadClient.MakeRequest(requestUrl, RequestType.Default);
+            RequestResult result = downloadClient.MakeRequest(requestUrl, RequestType.MangaInfo);
             if ((int)result.statusCode < 200 || (int)result.statusCode >= 300)
             {
                 Log.Error("Request failed");
@@ -119,7 +119,7 @@ public class ComickIo : MangaConnector
         string hid = m.Groups[1].Value;
         
         string requestUrl = $"https://api.comick.fun/chapter/{hid}/get_images";
-        RequestResult result = downloadClient.MakeRequest(requestUrl, RequestType.Default);
+        RequestResult result = downloadClient.MakeRequest(requestUrl, RequestType.MangaInfo);
         if ((int)result.statusCode < 200 || (int)result.statusCode >= 300)
         {
             Log.Error("Request failed");

From a1a5028858053ee201812a278c702c640c9f0236 Mon Sep 17 00:00:00 2001
From: Glax <johanna@bernloehr.eu>
Date: Fri, 16 May 2025 19:18:07 +0200
Subject: [PATCH 22/50] Ordering of DownloadChapterJobs (start at first chapter
 and work up)

---
 API/Tranga.cs | 55 ++++++++++++++++++++++++++++++++++++++++++---------
 1 file changed, 46 insertions(+), 9 deletions(-)

diff --git a/API/Tranga.cs b/API/Tranga.cs
index 3cc65b8..48773d9 100644
--- a/API/Tranga.cs
+++ b/API/Tranga.cs
@@ -134,13 +134,35 @@ public static class Tranga
             }
 
             //Retrieve waiting and due Jobs
-            List<Job> waitingJobs = context.Jobs.Local.Where(j =>
-                j.Enabled && (j.state == JobState.FirstExecution || j.state == JobState.CompletedWaiting)).ToList();
             List<Job> runningJobs = context.Jobs.Local.Where(j => j.state == JobState.Running).ToList();
-            List<Job> dueJobs = waitingJobs.Where(j => j.NextExecution < DateTime.UtcNow).ToList();
-
+            
             List<MangaConnector> busyConnectors = GetBusyConnectors(runningJobs);
-            List<Job> startJobs = FilterJobPreconditions(context, dueJobs, busyConnectors);
+
+            List<Job> waitingJobs = GetWaitingJobs(context.Jobs.Local.ToList());
+            List<Job> dueJobs = FilterDueJobs(waitingJobs);
+            List<Job> jobsWithoutBusyConnectors = FilterJobWithBusyConnectors(dueJobs, busyConnectors);
+            List<Job> jobsWithoutMissingDependencies = FilterJobDependencies(context, jobsWithoutBusyConnectors);
+
+            List<Job> jobsWithoutDownloading =
+                jobsWithoutMissingDependencies
+                    .Where(j => j.JobType != JobType.DownloadSingleChapterJob)
+                    .ToList();
+            List<Job> firstChapterPerConnector =
+                jobsWithoutMissingDependencies
+                    .Where(j => j.JobType == JobType.DownloadSingleChapterJob)
+                    .OrderBy(j =>
+                    {
+                        DownloadSingleChapterJob dscj = (DownloadSingleChapterJob)j;
+                        return dscj.Chapter;
+                    })
+                    .DistinctBy(j =>
+                    {
+                        DownloadSingleChapterJob dscj = (DownloadSingleChapterJob)j;
+                        return dscj.Chapter.ParentManga.MangaConnector;
+                    })
+                    .ToList();
+
+            List<Job> startJobs = jobsWithoutDownloading.Concat(firstChapterPerConnector).ToList();
             
             //Start Jobs that are allowed to run (preconditions match)
             foreach (Job job in startJobs)
@@ -155,7 +177,7 @@ public static class Tranga
             Log.Debug($"Jobs Completed: {completedJobs.Count} Failed: {failedJobs.Count} Running: {runningJobs.Count}\n" +
                       $"Waiting: {waitingJobs.Count}\n" +
                       $"\tof which Due: {dueJobs.Count}\n" +
-                      $"\t\tof which Started: {startJobs.Count}");
+                      $"\t\tof which Started: {jobsWithoutMissingDependencies.Count}");
 
             (Thread, Job)[] removeFromThreadsList = RunningJobs.Where(t => !t.Key.IsAlive)
                 .Select(t => (t.Key, t.Value)).ToArray();
@@ -187,9 +209,21 @@ public static class Tranga
         }
         return busyConnectors.ToList();
     }
+    
+    private static List<Job> GetWaitingJobs(List<Job> jobs) =>
+        jobs
+            .Where(j =>
+                j.Enabled &&
+                (j.state == JobState.FirstExecution || j.state == JobState.CompletedWaiting))
+            .ToList();
 
-    private static List<Job> FilterJobPreconditions(PgsqlContext context, List<Job> dueJobs, List<MangaConnector> busyConnectors) =>
-        dueJobs
+    private static List<Job> FilterDueJobs(List<Job> jobs) =>
+        jobs
+            .Where(j => j.NextExecution < DateTime.UtcNow)
+            .ToList();
+
+    private static List<Job> FilterJobDependencies(PgsqlContext context, List<Job> jobs) =>
+        jobs
             .Where(j =>
             {
                 Log.Debug($"Loading Job Preconditions {j}...");
@@ -197,6 +231,10 @@ public static class Tranga
                 Log.Debug($"Loaded Job Preconditions {j}!");
                 return j.DependenciesFulfilled;
             })
+            .ToList();
+
+    private static List<Job> FilterJobWithBusyConnectors(List<Job> jobs, List<MangaConnector> busyConnectors) =>
+        jobs
             .Where(j =>
             {
                 //Filter jobs with busy connectors
@@ -204,7 +242,6 @@ public static class Tranga
                     return busyConnectors.Contains(mangaConnector) == false;
                 return true;
             })
-            .DistinctBy(j => j.JobType)
             .ToList();
 
     private static MangaConnector? GetJobConnector(Job job)

From 0f0a49f74fcb27098c084ff08b0b8554c35b13f5 Mon Sep 17 00:00:00 2001
From: Glax <johanna@bernloehr.eu>
Date: Fri, 16 May 2025 19:27:22 +0200
Subject: [PATCH 23/50] Change Search with name to GET Request

---
 API/Controllers/SearchController.cs | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/API/Controllers/SearchController.cs b/API/Controllers/SearchController.cs
index d4b8103..2894762 100644
--- a/API/Controllers/SearchController.cs
+++ b/API/Controllers/SearchController.cs
@@ -25,7 +25,7 @@ public class SearchController(PgsqlContext context, ILog Log) : Controller
     /// <response code="404">MangaConnector with ID not found</response>
     /// <response code="406">MangaConnector with ID is disabled</response>
     /// <response code="500">Error during Database Operation</response>
-    [HttpPost("{MangaConnectorName}/{Query}")]
+    [HttpGet("{MangaConnectorName}/{Query}")]
     [ProducesResponseType<Manga[]>(Status200OK, "application/json")]
     [ProducesResponseType(Status404NotFound)]
     [ProducesResponseType(Status406NotAcceptable)]

From 590ccdd09acae4df05ffe7d7de20737b1b04c410 Mon Sep 17 00:00:00 2001
From: Glax <johanna@bernloehr.eu>
Date: Fri, 16 May 2025 19:27:34 +0200
Subject: [PATCH 24/50] Use GlobalConnector for Url Search requests

---
 API/Controllers/SearchController.cs | 15 +++++----------
 1 file changed, 5 insertions(+), 10 deletions(-)

diff --git a/API/Controllers/SearchController.cs b/API/Controllers/SearchController.cs
index 2894762..fbef959 100644
--- a/API/Controllers/SearchController.cs
+++ b/API/Controllers/SearchController.cs
@@ -67,30 +67,25 @@ public class SearchController(PgsqlContext context, ILog Log) : Controller
     /// <response code="500">Error during Database Operation</response>
     [HttpPost("Url")]
     [ProducesResponseType<Manga>(Status200OK, "application/json")]
-    [ProducesResponseType(Status300MultipleChoices)]
     [ProducesResponseType(Status400BadRequest)]
-    [ProducesResponseType(Status404NotFound)]
     [ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
     public IActionResult GetMangaFromUrl([FromBody]string url)
     {
-        List<MangaConnector> connectors = context.MangaConnectors.AsEnumerable().Where(c => c.UrlMatchesConnector(url)).ToList();
-        if (connectors.Count == 0)
-            return NotFound();
-        else if (connectors.Count > 1)
-            return StatusCode(Status300MultipleChoices);
+        if (context.MangaConnectors.Find("Global") is not { } connector)
+            return StatusCode(Status500InternalServerError, "Could not find Global Connector.");
 
-        if(connectors.First().GetMangaFromUrl(url) is not { } manga)
+        if(connector.GetMangaFromUrl(url) is not { } manga)
             return BadRequest();
         try
         {
             if(AddMangaToContext(manga) is { } add)
                 return Ok(add);
-            return StatusCode(500);
+            return StatusCode(Status500InternalServerError);
         }
         catch (DbUpdateException e)
         {
             Log.Error(e);
-            return StatusCode(500, e.Message);
+            return StatusCode(Status500InternalServerError, e.Message);
         }
     }
     

From a764f381c92770face3fffb4f9d14422b6096d48 Mon Sep 17 00:00:00 2001
From: Glax <johanna@bernloehr.eu>
Date: Fri, 16 May 2025 19:46:14 +0200
Subject: [PATCH 25/50] Logging for DBContexts

---
 API/Schema/Contexts/LibraryContext.cs       | 14 ++++++++++++++
 API/Schema/Contexts/NotificationsContext.cs | 13 +++++++++++++
 API/Schema/Contexts/PgsqlContext.cs         | 13 +++++++++++++
 3 files changed, 40 insertions(+)

diff --git a/API/Schema/Contexts/LibraryContext.cs b/API/Schema/Contexts/LibraryContext.cs
index 8d13ef6..bbe9e3c 100644
--- a/API/Schema/Contexts/LibraryContext.cs
+++ b/API/Schema/Contexts/LibraryContext.cs
@@ -1,12 +1,26 @@
 using API.Schema.LibraryConnectors;
+using log4net;
 using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Diagnostics;
 
 namespace API.Schema.Contexts;
 
 public class LibraryContext(DbContextOptions<LibraryContext> options) : DbContext(options)
 {
     public DbSet<LibraryConnector> LibraryConnectors { get; set; }
+    
+    private ILog Log => LogManager.GetLogger(GetType());
 
+    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
+    {
+        base.OnConfiguring(optionsBuilder);
+        optionsBuilder.EnableSensitiveDataLogging();
+        optionsBuilder.LogTo(s =>
+        {
+            Log.Debug(s);
+        }, [DbLoggerCategory.Query.Name], LogLevel.Trace, DbContextLoggerOptions.Level | DbContextLoggerOptions.Category);
+    }
+    
     protected override void OnModelCreating(ModelBuilder modelBuilder)
     {
         //LibraryConnector Types
diff --git a/API/Schema/Contexts/NotificationsContext.cs b/API/Schema/Contexts/NotificationsContext.cs
index 26f5699..8fe9454 100644
--- a/API/Schema/Contexts/NotificationsContext.cs
+++ b/API/Schema/Contexts/NotificationsContext.cs
@@ -1,5 +1,7 @@
 using API.Schema.NotificationConnectors;
+using log4net;
 using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Diagnostics;
 
 namespace API.Schema.Contexts;
 
@@ -7,4 +9,15 @@ public class NotificationsContext(DbContextOptions<NotificationsContext> options
 {
     public DbSet<NotificationConnector> NotificationConnectors { get; set; }
     public DbSet<Notification> Notifications { get; set; }
+    
+    private ILog Log => LogManager.GetLogger(GetType());
+    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
+    {
+        base.OnConfiguring(optionsBuilder);
+        optionsBuilder.EnableSensitiveDataLogging();
+        optionsBuilder.LogTo(s =>
+        {
+            Log.Debug(s);
+        }, [DbLoggerCategory.Query.Name], LogLevel.Trace, DbContextLoggerOptions.Level | DbContextLoggerOptions.Category);
+    }
 }
\ No newline at end of file
diff --git a/API/Schema/Contexts/PgsqlContext.cs b/API/Schema/Contexts/PgsqlContext.cs
index 7308472..aa1b25b 100644
--- a/API/Schema/Contexts/PgsqlContext.cs
+++ b/API/Schema/Contexts/PgsqlContext.cs
@@ -1,6 +1,8 @@
 using API.Schema.Jobs;
 using API.Schema.MangaConnectors;
+using log4net;
 using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Diagnostics;
 
 namespace API.Schema.Contexts;
 
@@ -13,7 +15,18 @@ public class PgsqlContext(DbContextOptions<PgsqlContext> options) : DbContext(op
     public DbSet<Chapter> Chapters { get; set; }
     public DbSet<Author> Authors { get; set; }
     public DbSet<MangaTag> Tags { get; set; }
+    private ILog Log => LogManager.GetLogger(GetType());
     
+    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
+    {
+        base.OnConfiguring(optionsBuilder);
+        optionsBuilder.EnableSensitiveDataLogging();
+        optionsBuilder.LogTo(s =>
+        {
+            Log.Debug(s);
+        }, [DbLoggerCategory.Query.Name], LogLevel.Trace, DbContextLoggerOptions.Level | DbContextLoggerOptions.Category);
+    }
+
     protected override void OnModelCreating(ModelBuilder modelBuilder)
     {
         //Job Types

From adc7ee606e354fa0a38397c752aa8e6871cad6ac Mon Sep 17 00:00:00 2001
From: Glax <johanna@bernloehr.eu>
Date: Fri, 16 May 2025 20:00:53 +0200
Subject: [PATCH 26/50] RetrieveChaptersJob.cs do not use context to access
 Chapters

---
 API/Schema/Jobs/RetrieveChaptersJob.cs | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/API/Schema/Jobs/RetrieveChaptersJob.cs b/API/Schema/Jobs/RetrieveChaptersJob.cs
index a89f7c6..d304af8 100644
--- a/API/Schema/Jobs/RetrieveChaptersJob.cs
+++ b/API/Schema/Jobs/RetrieveChaptersJob.cs
@@ -32,9 +32,10 @@ public class RetrieveChaptersJob : Job
     protected override IEnumerable<Job> RunInternal(PgsqlContext context)
     {
         context.Attach(Manga);
+        context.Entry(Manga).Collection<Chapter>(m => m.Chapters).Load();
         // This gets all chapters that are not downloaded
         Chapter[] allChapters = Manga.MangaConnector.GetChapters(Manga, Language);
-        Chapter[] newChapters = allChapters.Where(chapter => context.Chapters.Contains(chapter) == false).ToArray();
+        Chapter[] newChapters = allChapters.Where(chapter => Manga.Chapters.Contains(chapter) == false).ToArray();
         Log.Info($"{newChapters.Length} new chapters.");
 
         try

From be2adff57dea2171d5aee402885687b1c73f979a Mon Sep 17 00:00:00 2001
From: Glax <johanna@bernloehr.eu>
Date: Fri, 16 May 2025 20:01:50 +0200
Subject: [PATCH 27/50] DownloadSingleChapterJob.cs load Jobs

---
 API/Schema/Jobs/DownloadSingleChapterJob.cs | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/API/Schema/Jobs/DownloadSingleChapterJob.cs b/API/Schema/Jobs/DownloadSingleChapterJob.cs
index e7a4d1d..f336ac2 100644
--- a/API/Schema/Jobs/DownloadSingleChapterJob.cs
+++ b/API/Schema/Jobs/DownloadSingleChapterJob.cs
@@ -3,6 +3,7 @@ using System.IO.Compression;
 using System.Runtime.InteropServices;
 using API.MangaDownloadClients;
 using API.Schema.Contexts;
+using Microsoft.EntityFrameworkCore;
 using Newtonsoft.Json;
 using SixLabors.ImageSharp;
 using SixLabors.ImageSharp.Formats.Jpeg;
@@ -37,6 +38,7 @@ public class DownloadSingleChapterJob : Job
     protected override IEnumerable<Job> RunInternal(PgsqlContext context)
     {
         context.Attach(Chapter);
+        context.Attach(Chapter.ParentManga);
         string[] imageUrls = Chapter.ParentManga.MangaConnector.GetChapterImageUrls(Chapter);
         if (imageUrls.Length < 1)
         {
@@ -97,6 +99,7 @@ public class DownloadSingleChapterJob : Job
         Chapter.Downloaded = true;
         context.SaveChanges();
 
+        context.Jobs.Load();
         if (context.Jobs.AsEnumerable().Any(j =>
             {
                 if (j.JobType != JobType.UpdateFilesDownloadedJob)

From 563afa1e6fd2d4480efa786eed409bfeb4c0db9e Mon Sep 17 00:00:00 2001
From: Glax <johanna@bernloehr.eu>
Date: Fri, 16 May 2025 20:12:08 +0200
Subject: [PATCH 28/50] Split UpdateFilesDownloadedJob.cs to
 UpdateChaptersDownloadedJob.cs and split into
 UpdateSingleChapterDownloadedJob.cs

---
 API/Controllers/JobController.cs              |   6 +-
 API/Controllers/MangaController.cs            |   2 +-
 .../20250515120724_Initial-1.Designer.cs      |   4 +-
 .../20250516121442_AltTitle-Owned.Designer.cs |   4 +-
 ...0516121725_Manga-Year-Nullable.Designer.cs |   4 +-
 ...16122242_AltTitle-Owned-WithId.Designer.cs |   4 +-
 ...dateSingleChapterDownloadedJob.Designer.cs | 720 ++++++++++++++++++
 ...b-Into-UpdateSingleChapterDownloadedJob.cs |  94 +++
 .../pgsql/PgsqlContextModelSnapshot.cs        |  37 +-
 API/Program.cs                                |   2 +-
 API/Schema/Contexts/PgsqlContext.cs           |   7 +-
 API/Schema/Jobs/DownloadSingleChapterJob.cs   |   6 +-
 API/Schema/Jobs/JobType.cs                    |   5 +-
 .../Jobs/UpdateChaptersDownloadedJob.cs       |  34 +
 API/Schema/Jobs/UpdateFilesDownloadedJob.cs   |  46 --
 .../Jobs/UpdateSingleChapterDownloadedJob.cs  |  45 ++
 API/TokenGen.cs                               |   2 +-
 17 files changed, 951 insertions(+), 71 deletions(-)
 create mode 100644 API/Migrations/pgsql/20250516180953_Split-UpdateChaptersDownloadedJob-Into-UpdateSingleChapterDownloadedJob.Designer.cs
 create mode 100644 API/Migrations/pgsql/20250516180953_Split-UpdateChaptersDownloadedJob-Into-UpdateSingleChapterDownloadedJob.cs
 create mode 100644 API/Schema/Jobs/UpdateChaptersDownloadedJob.cs
 delete mode 100644 API/Schema/Jobs/UpdateFilesDownloadedJob.cs
 create mode 100644 API/Schema/Jobs/UpdateSingleChapterDownloadedJob.cs

diff --git a/API/Controllers/JobController.cs b/API/Controllers/JobController.cs
index a4011ea..5424239 100644
--- a/API/Controllers/JobController.cs
+++ b/API/Controllers/JobController.cs
@@ -158,7 +158,7 @@ public class JobController(PgsqlContext context, ILog Log) : Controller
     }
 
     /// <summary>
-    /// Create a new UpdateFilesDownloadedJob
+    /// Create a new UpdateChaptersDownloadedJob
     /// </summary>
     /// <param name="MangaId">ID of the Manga</param>
     /// <response code="201">Job-IDs</response>
@@ -172,7 +172,7 @@ public class JobController(PgsqlContext context, ILog Log) : Controller
     {
         if(context.Mangas.Find(MangaId) is not { } m)
             return NotFound();
-        Job job = new UpdateFilesDownloadedJob(m, 0);
+        Job job = new UpdateChaptersDownloadedJob(m, 0);
         return AddJobs([job]);
     }
 
@@ -186,7 +186,7 @@ public class JobController(PgsqlContext context, ILog Log) : Controller
     [ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
     public IActionResult CreateUpdateAllFilesDownloadedJob()
     {
-        List<UpdateFilesDownloadedJob> jobs = context.Mangas.Select(m => new UpdateFilesDownloadedJob(m, 0, null, null)).ToList();
+        List<UpdateChaptersDownloadedJob> jobs = context.Mangas.Select(m => new UpdateChaptersDownloadedJob(m, 0, null, null)).ToList();
         try
         {
             context.Jobs.AddRange(jobs);
diff --git a/API/Controllers/MangaController.cs b/API/Controllers/MangaController.cs
index 6098d3a..3ec580c 100644
--- a/API/Controllers/MangaController.cs
+++ b/API/Controllers/MangaController.cs
@@ -343,7 +343,7 @@ public class MangaController(PgsqlContext context, ILog Log) : Controller
             return NotFound();
 
         MoveMangaLibraryJob moveLibrary = new(manga, library);
-        UpdateFilesDownloadedJob updateDownloadedFiles = new(manga, 0, dependsOnJobs: [moveLibrary]);
+        UpdateChaptersDownloadedJob updateDownloadedFiles = new(manga, 0, dependsOnJobs: [moveLibrary]);
         
         try
         {
diff --git a/API/Migrations/pgsql/20250515120724_Initial-1.Designer.cs b/API/Migrations/pgsql/20250515120724_Initial-1.Designer.cs
index 9b0185b..4b1b2cb 100644
--- a/API/Migrations/pgsql/20250515120724_Initial-1.Designer.cs
+++ b/API/Migrations/pgsql/20250515120724_Initial-1.Designer.cs
@@ -416,7 +416,7 @@ namespace API.Migrations.pgsql
                     b.HasDiscriminator().HasValue((byte)5);
                 });
 
-            modelBuilder.Entity("API.Schema.Jobs.UpdateFilesDownloadedJob", b =>
+            modelBuilder.Entity("API.Schema.Jobs.UpdateChaptersDownloadedJob", b =>
                 {
                     b.HasBaseType("API.Schema.Jobs.Job");
 
@@ -661,7 +661,7 @@ namespace API.Migrations.pgsql
                     b.Navigation("Manga");
                 });
 
-            modelBuilder.Entity("API.Schema.Jobs.UpdateFilesDownloadedJob", b =>
+            modelBuilder.Entity("API.Schema.Jobs.UpdateChaptersDownloadedJob", b =>
                 {
                     b.HasOne("API.Schema.Manga", "Manga")
                         .WithMany()
diff --git a/API/Migrations/pgsql/20250516121442_AltTitle-Owned.Designer.cs b/API/Migrations/pgsql/20250516121442_AltTitle-Owned.Designer.cs
index 29f52b0..c97d81d 100644
--- a/API/Migrations/pgsql/20250516121442_AltTitle-Owned.Designer.cs
+++ b/API/Migrations/pgsql/20250516121442_AltTitle-Owned.Designer.cs
@@ -416,7 +416,7 @@ namespace API.Migrations.pgsql
                     b.HasDiscriminator().HasValue((byte)5);
                 });
 
-            modelBuilder.Entity("API.Schema.Jobs.UpdateFilesDownloadedJob", b =>
+            modelBuilder.Entity("API.Schema.Jobs.UpdateChaptersDownloadedJob", b =>
                 {
                     b.HasBaseType("API.Schema.Jobs.Job");
 
@@ -667,7 +667,7 @@ namespace API.Migrations.pgsql
                     b.Navigation("Manga");
                 });
 
-            modelBuilder.Entity("API.Schema.Jobs.UpdateFilesDownloadedJob", b =>
+            modelBuilder.Entity("API.Schema.Jobs.UpdateChaptersDownloadedJob", b =>
                 {
                     b.HasOne("API.Schema.Manga", "Manga")
                         .WithMany()
diff --git a/API/Migrations/pgsql/20250516121725_Manga-Year-Nullable.Designer.cs b/API/Migrations/pgsql/20250516121725_Manga-Year-Nullable.Designer.cs
index afe97c3..2ebe310 100644
--- a/API/Migrations/pgsql/20250516121725_Manga-Year-Nullable.Designer.cs
+++ b/API/Migrations/pgsql/20250516121725_Manga-Year-Nullable.Designer.cs
@@ -416,7 +416,7 @@ namespace API.Migrations.pgsql
                     b.HasDiscriminator().HasValue((byte)5);
                 });
 
-            modelBuilder.Entity("API.Schema.Jobs.UpdateFilesDownloadedJob", b =>
+            modelBuilder.Entity("API.Schema.Jobs.UpdateChaptersDownloadedJob", b =>
                 {
                     b.HasBaseType("API.Schema.Jobs.Job");
 
@@ -667,7 +667,7 @@ namespace API.Migrations.pgsql
                     b.Navigation("Manga");
                 });
 
-            modelBuilder.Entity("API.Schema.Jobs.UpdateFilesDownloadedJob", b =>
+            modelBuilder.Entity("API.Schema.Jobs.UpdateChaptersDownloadedJob", b =>
                 {
                     b.HasOne("API.Schema.Manga", "Manga")
                         .WithMany()
diff --git a/API/Migrations/pgsql/20250516122242_AltTitle-Owned-WithId.Designer.cs b/API/Migrations/pgsql/20250516122242_AltTitle-Owned-WithId.Designer.cs
index 30b595c..7b86671 100644
--- a/API/Migrations/pgsql/20250516122242_AltTitle-Owned-WithId.Designer.cs
+++ b/API/Migrations/pgsql/20250516122242_AltTitle-Owned-WithId.Designer.cs
@@ -416,7 +416,7 @@ namespace API.Migrations.pgsql
                     b.HasDiscriminator().HasValue((byte)5);
                 });
 
-            modelBuilder.Entity("API.Schema.Jobs.UpdateFilesDownloadedJob", b =>
+            modelBuilder.Entity("API.Schema.Jobs.UpdateChaptersDownloadedJob", b =>
                 {
                     b.HasBaseType("API.Schema.Jobs.Job");
 
@@ -668,7 +668,7 @@ namespace API.Migrations.pgsql
                     b.Navigation("Manga");
                 });
 
-            modelBuilder.Entity("API.Schema.Jobs.UpdateFilesDownloadedJob", b =>
+            modelBuilder.Entity("API.Schema.Jobs.UpdateChaptersDownloadedJob", b =>
                 {
                     b.HasOne("API.Schema.Manga", "Manga")
                         .WithMany()
diff --git a/API/Migrations/pgsql/20250516180953_Split-UpdateChaptersDownloadedJob-Into-UpdateSingleChapterDownloadedJob.Designer.cs b/API/Migrations/pgsql/20250516180953_Split-UpdateChaptersDownloadedJob-Into-UpdateSingleChapterDownloadedJob.Designer.cs
new file mode 100644
index 0000000..effec49
--- /dev/null
+++ b/API/Migrations/pgsql/20250516180953_Split-UpdateChaptersDownloadedJob-Into-UpdateSingleChapterDownloadedJob.Designer.cs
@@ -0,0 +1,720 @@
+// <auto-generated />
+using System;
+using API.Schema.Contexts;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+
+#nullable disable
+
+namespace API.Migrations.pgsql
+{
+    [DbContext(typeof(PgsqlContext))]
+    [Migration("20250516180953_Split-UpdateChaptersDownloadedJob-Into-UpdateSingleChapterDownloadedJob")]
+    partial class SplitUpdateChaptersDownloadedJobIntoUpdateSingleChapterDownloadedJob
+    {
+        /// <inheritdoc />
+        protected override void BuildTargetModel(ModelBuilder modelBuilder)
+        {
+#pragma warning disable 612, 618
+            modelBuilder
+                .HasAnnotation("ProductVersion", "9.0.3")
+                .HasAnnotation("Relational:MaxIdentifierLength", 63);
+
+            NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
+
+            modelBuilder.Entity("API.Schema.Author", b =>
+                {
+                    b.Property<string>("AuthorId")
+                        .HasMaxLength(64)
+                        .HasColumnType("character varying(64)");
+
+                    b.Property<string>("AuthorName")
+                        .IsRequired()
+                        .HasMaxLength(128)
+                        .HasColumnType("character varying(128)");
+
+                    b.HasKey("AuthorId");
+
+                    b.ToTable("Authors");
+                });
+
+            modelBuilder.Entity("API.Schema.Chapter", b =>
+                {
+                    b.Property<string>("ChapterId")
+                        .HasMaxLength(64)
+                        .HasColumnType("character varying(64)");
+
+                    b.Property<string>("ChapterNumber")
+                        .IsRequired()
+                        .HasMaxLength(10)
+                        .HasColumnType("character varying(10)");
+
+                    b.Property<bool>("Downloaded")
+                        .HasColumnType("boolean");
+
+                    b.Property<string>("FileName")
+                        .IsRequired()
+                        .HasMaxLength(256)
+                        .HasColumnType("character varying(256)");
+
+                    b.Property<string>("ParentMangaId")
+                        .IsRequired()
+                        .HasColumnType("character varying(64)");
+
+                    b.Property<string>("Title")
+                        .HasMaxLength(256)
+                        .HasColumnType("character varying(256)");
+
+                    b.Property<string>("Url")
+                        .IsRequired()
+                        .HasMaxLength(2048)
+                        .HasColumnType("character varying(2048)");
+
+                    b.Property<int?>("VolumeNumber")
+                        .HasColumnType("integer");
+
+                    b.HasKey("ChapterId");
+
+                    b.HasIndex("ParentMangaId");
+
+                    b.ToTable("Chapters");
+                });
+
+            modelBuilder.Entity("API.Schema.Jobs.Job", b =>
+                {
+                    b.Property<string>("JobId")
+                        .HasMaxLength(64)
+                        .HasColumnType("character varying(64)");
+
+                    b.Property<bool>("Enabled")
+                        .HasColumnType("boolean");
+
+                    b.Property<byte>("JobType")
+                        .HasColumnType("smallint");
+
+                    b.Property<DateTime>("LastExecution")
+                        .HasColumnType("timestamp with time zone");
+
+                    b.Property<string>("ParentJobId")
+                        .HasMaxLength(64)
+                        .HasColumnType("character varying(64)");
+
+                    b.Property<decimal>("RecurrenceMs")
+                        .HasColumnType("numeric(20,0)");
+
+                    b.Property<byte>("state")
+                        .HasColumnType("smallint");
+
+                    b.HasKey("JobId");
+
+                    b.HasIndex("ParentJobId");
+
+                    b.ToTable("Jobs");
+
+                    b.HasDiscriminator<byte>("JobType");
+
+                    b.UseTphMappingStrategy();
+                });
+
+            modelBuilder.Entity("API.Schema.LocalLibrary", b =>
+                {
+                    b.Property<string>("LocalLibraryId")
+                        .HasMaxLength(64)
+                        .HasColumnType("character varying(64)");
+
+                    b.Property<string>("BasePath")
+                        .IsRequired()
+                        .HasMaxLength(256)
+                        .HasColumnType("character varying(256)");
+
+                    b.Property<string>("LibraryName")
+                        .IsRequired()
+                        .HasMaxLength(512)
+                        .HasColumnType("character varying(512)");
+
+                    b.HasKey("LocalLibraryId");
+
+                    b.ToTable("LocalLibraries");
+                });
+
+            modelBuilder.Entity("API.Schema.Manga", b =>
+                {
+                    b.Property<string>("MangaId")
+                        .HasMaxLength(64)
+                        .HasColumnType("character varying(64)");
+
+                    b.Property<string>("CoverFileNameInCache")
+                        .HasMaxLength(512)
+                        .HasColumnType("character varying(512)");
+
+                    b.Property<string>("CoverUrl")
+                        .IsRequired()
+                        .HasMaxLength(512)
+                        .HasColumnType("character varying(512)");
+
+                    b.Property<string>("Description")
+                        .IsRequired()
+                        .HasColumnType("text");
+
+                    b.Property<string>("DirectoryName")
+                        .IsRequired()
+                        .HasMaxLength(1024)
+                        .HasColumnType("character varying(1024)");
+
+                    b.Property<string>("IdOnConnectorSite")
+                        .IsRequired()
+                        .HasMaxLength(256)
+                        .HasColumnType("character varying(256)");
+
+                    b.Property<float>("IgnoreChaptersBefore")
+                        .HasColumnType("real");
+
+                    b.Property<string>("LibraryId")
+                        .HasMaxLength(64)
+                        .HasColumnType("character varying(64)");
+
+                    b.Property<string>("MangaConnectorName")
+                        .IsRequired()
+                        .HasMaxLength(32)
+                        .HasColumnType("character varying(32)");
+
+                    b.Property<string>("Name")
+                        .IsRequired()
+                        .HasMaxLength(512)
+                        .HasColumnType("character varying(512)");
+
+                    b.Property<string>("OriginalLanguage")
+                        .HasMaxLength(8)
+                        .HasColumnType("character varying(8)");
+
+                    b.Property<byte>("ReleaseStatus")
+                        .HasColumnType("smallint");
+
+                    b.Property<string>("WebsiteUrl")
+                        .IsRequired()
+                        .HasMaxLength(512)
+                        .HasColumnType("character varying(512)");
+
+                    b.Property<long?>("Year")
+                        .HasColumnType("bigint");
+
+                    b.HasKey("MangaId");
+
+                    b.HasIndex("LibraryId");
+
+                    b.HasIndex("MangaConnectorName");
+
+                    b.ToTable("Mangas");
+                });
+
+            modelBuilder.Entity("API.Schema.MangaConnectors.MangaConnector", b =>
+                {
+                    b.Property<string>("Name")
+                        .HasMaxLength(32)
+                        .HasColumnType("character varying(32)");
+
+                    b.PrimitiveCollection<string[]>("BaseUris")
+                        .IsRequired()
+                        .HasMaxLength(256)
+                        .HasColumnType("text[]");
+
+                    b.Property<bool>("Enabled")
+                        .HasColumnType("boolean");
+
+                    b.Property<string>("IconUrl")
+                        .IsRequired()
+                        .HasMaxLength(2048)
+                        .HasColumnType("character varying(2048)");
+
+                    b.PrimitiveCollection<string[]>("SupportedLanguages")
+                        .IsRequired()
+                        .HasMaxLength(8)
+                        .HasColumnType("text[]");
+
+                    b.HasKey("Name");
+
+                    b.ToTable("MangaConnectors");
+
+                    b.HasDiscriminator<string>("Name").HasValue("MangaConnector");
+
+                    b.UseTphMappingStrategy();
+                });
+
+            modelBuilder.Entity("API.Schema.MangaTag", b =>
+                {
+                    b.Property<string>("Tag")
+                        .HasMaxLength(64)
+                        .HasColumnType("character varying(64)");
+
+                    b.HasKey("Tag");
+
+                    b.ToTable("Tags");
+                });
+
+            modelBuilder.Entity("AuthorToManga", b =>
+                {
+                    b.Property<string>("AuthorIds")
+                        .HasColumnType("character varying(64)");
+
+                    b.Property<string>("MangaIds")
+                        .HasColumnType("character varying(64)");
+
+                    b.HasKey("AuthorIds", "MangaIds");
+
+                    b.HasIndex("MangaIds");
+
+                    b.ToTable("AuthorToManga");
+                });
+
+            modelBuilder.Entity("JobJob", b =>
+                {
+                    b.Property<string>("DependsOnJobsJobId")
+                        .HasColumnType("character varying(64)");
+
+                    b.Property<string>("JobId")
+                        .HasColumnType("character varying(64)");
+
+                    b.HasKey("DependsOnJobsJobId", "JobId");
+
+                    b.HasIndex("JobId");
+
+                    b.ToTable("JobJob");
+                });
+
+            modelBuilder.Entity("MangaTagToManga", b =>
+                {
+                    b.Property<string>("MangaTagIds")
+                        .HasColumnType("character varying(64)");
+
+                    b.Property<string>("MangaIds")
+                        .HasColumnType("character varying(64)");
+
+                    b.HasKey("MangaTagIds", "MangaIds");
+
+                    b.HasIndex("MangaIds");
+
+                    b.ToTable("MangaTagToManga");
+                });
+
+            modelBuilder.Entity("API.Schema.Jobs.DownloadAvailableChaptersJob", b =>
+                {
+                    b.HasBaseType("API.Schema.Jobs.Job");
+
+                    b.Property<string>("MangaId")
+                        .IsRequired()
+                        .HasMaxLength(64)
+                        .HasColumnType("character varying(64)");
+
+                    b.HasIndex("MangaId");
+
+                    b.ToTable("Jobs", t =>
+                        {
+                            t.Property("MangaId")
+                                .HasColumnName("DownloadAvailableChaptersJob_MangaId");
+                        });
+
+                    b.HasDiscriminator().HasValue((byte)1);
+                });
+
+            modelBuilder.Entity("API.Schema.Jobs.DownloadMangaCoverJob", b =>
+                {
+                    b.HasBaseType("API.Schema.Jobs.Job");
+
+                    b.Property<string>("MangaId")
+                        .IsRequired()
+                        .HasMaxLength(64)
+                        .HasColumnType("character varying(64)");
+
+                    b.HasIndex("MangaId");
+
+                    b.HasDiscriminator().HasValue((byte)4);
+                });
+
+            modelBuilder.Entity("API.Schema.Jobs.DownloadSingleChapterJob", b =>
+                {
+                    b.HasBaseType("API.Schema.Jobs.Job");
+
+                    b.Property<string>("ChapterId")
+                        .IsRequired()
+                        .HasMaxLength(64)
+                        .HasColumnType("character varying(64)");
+
+                    b.HasIndex("ChapterId");
+
+                    b.HasDiscriminator().HasValue((byte)0);
+                });
+
+            modelBuilder.Entity("API.Schema.Jobs.MoveFileOrFolderJob", b =>
+                {
+                    b.HasBaseType("API.Schema.Jobs.Job");
+
+                    b.Property<string>("FromLocation")
+                        .IsRequired()
+                        .HasMaxLength(256)
+                        .HasColumnType("character varying(256)");
+
+                    b.Property<string>("ToLocation")
+                        .IsRequired()
+                        .HasMaxLength(256)
+                        .HasColumnType("character varying(256)");
+
+                    b.HasDiscriminator().HasValue((byte)3);
+                });
+
+            modelBuilder.Entity("API.Schema.Jobs.MoveMangaLibraryJob", b =>
+                {
+                    b.HasBaseType("API.Schema.Jobs.Job");
+
+                    b.Property<string>("MangaId")
+                        .IsRequired()
+                        .HasMaxLength(64)
+                        .HasColumnType("character varying(64)");
+
+                    b.Property<string>("ToLibraryId")
+                        .IsRequired()
+                        .HasMaxLength(64)
+                        .HasColumnType("character varying(64)");
+
+                    b.HasIndex("MangaId");
+
+                    b.HasIndex("ToLibraryId");
+
+                    b.ToTable("Jobs", t =>
+                        {
+                            t.Property("MangaId")
+                                .HasColumnName("MoveMangaLibraryJob_MangaId");
+                        });
+
+                    b.HasDiscriminator().HasValue((byte)7);
+                });
+
+            modelBuilder.Entity("API.Schema.Jobs.RetrieveChaptersJob", b =>
+                {
+                    b.HasBaseType("API.Schema.Jobs.Job");
+
+                    b.Property<string>("Language")
+                        .IsRequired()
+                        .HasMaxLength(8)
+                        .HasColumnType("character varying(8)");
+
+                    b.Property<string>("MangaId")
+                        .IsRequired()
+                        .HasMaxLength(64)
+                        .HasColumnType("character varying(64)");
+
+                    b.HasIndex("MangaId");
+
+                    b.ToTable("Jobs", t =>
+                        {
+                            t.Property("MangaId")
+                                .HasColumnName("RetrieveChaptersJob_MangaId");
+                        });
+
+                    b.HasDiscriminator().HasValue((byte)5);
+                });
+
+            modelBuilder.Entity("API.Schema.Jobs.UpdateChaptersDownloadedJob", b =>
+                {
+                    b.HasBaseType("API.Schema.Jobs.Job");
+
+                    b.Property<string>("MangaId")
+                        .IsRequired()
+                        .HasMaxLength(64)
+                        .HasColumnType("character varying(64)");
+
+                    b.HasIndex("MangaId");
+
+                    b.ToTable("Jobs", t =>
+                        {
+                            t.Property("MangaId")
+                                .HasColumnName("UpdateChaptersDownloadedJob_MangaId");
+                        });
+
+                    b.HasDiscriminator().HasValue((byte)6);
+                });
+
+            modelBuilder.Entity("API.Schema.Jobs.UpdateSingleChapterDownloadedJob", b =>
+                {
+                    b.HasBaseType("API.Schema.Jobs.Job");
+
+                    b.Property<string>("ChapterId")
+                        .IsRequired()
+                        .HasMaxLength(64)
+                        .HasColumnType("character varying(64)");
+
+                    b.HasIndex("ChapterId");
+
+                    b.ToTable("Jobs", t =>
+                        {
+                            t.Property("ChapterId")
+                                .HasColumnName("UpdateSingleChapterDownloadedJob_ChapterId");
+                        });
+
+                    b.HasDiscriminator().HasValue((byte)8);
+                });
+
+            modelBuilder.Entity("API.Schema.MangaConnectors.ComickIo", b =>
+                {
+                    b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
+
+                    b.HasDiscriminator().HasValue("ComickIo");
+                });
+
+            modelBuilder.Entity("API.Schema.MangaConnectors.Global", b =>
+                {
+                    b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
+
+                    b.HasDiscriminator().HasValue("Global");
+                });
+
+            modelBuilder.Entity("API.Schema.MangaConnectors.MangaDex", b =>
+                {
+                    b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
+
+                    b.HasDiscriminator().HasValue("MangaDex");
+                });
+
+            modelBuilder.Entity("API.Schema.Chapter", b =>
+                {
+                    b.HasOne("API.Schema.Manga", "ParentManga")
+                        .WithMany("Chapters")
+                        .HasForeignKey("ParentMangaId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("ParentManga");
+                });
+
+            modelBuilder.Entity("API.Schema.Jobs.Job", b =>
+                {
+                    b.HasOne("API.Schema.Jobs.Job", "ParentJob")
+                        .WithMany()
+                        .HasForeignKey("ParentJobId")
+                        .OnDelete(DeleteBehavior.Cascade);
+
+                    b.Navigation("ParentJob");
+                });
+
+            modelBuilder.Entity("API.Schema.Manga", b =>
+                {
+                    b.HasOne("API.Schema.LocalLibrary", "Library")
+                        .WithMany()
+                        .HasForeignKey("LibraryId")
+                        .OnDelete(DeleteBehavior.SetNull);
+
+                    b.HasOne("API.Schema.MangaConnectors.MangaConnector", "MangaConnector")
+                        .WithMany()
+                        .HasForeignKey("MangaConnectorName")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.OwnsMany("API.Schema.Link", "Links", b1 =>
+                        {
+                            b1.Property<string>("LinkId")
+                                .HasMaxLength(64)
+                                .HasColumnType("character varying(64)");
+
+                            b1.Property<string>("LinkProvider")
+                                .IsRequired()
+                                .HasMaxLength(64)
+                                .HasColumnType("character varying(64)");
+
+                            b1.Property<string>("LinkUrl")
+                                .IsRequired()
+                                .HasMaxLength(2048)
+                                .HasColumnType("character varying(2048)");
+
+                            b1.Property<string>("MangaId")
+                                .IsRequired()
+                                .HasColumnType("character varying(64)");
+
+                            b1.HasKey("LinkId");
+
+                            b1.HasIndex("MangaId");
+
+                            b1.ToTable("Link");
+
+                            b1.WithOwner()
+                                .HasForeignKey("MangaId");
+                        });
+
+                    b.OwnsMany("API.Schema.MangaAltTitle", "AltTitles", b1 =>
+                        {
+                            b1.Property<string>("AltTitleId")
+                                .HasMaxLength(64)
+                                .HasColumnType("character varying(64)");
+
+                            b1.Property<string>("Language")
+                                .IsRequired()
+                                .HasMaxLength(8)
+                                .HasColumnType("character varying(8)");
+
+                            b1.Property<string>("MangaId")
+                                .IsRequired()
+                                .HasColumnType("character varying(64)");
+
+                            b1.Property<string>("Title")
+                                .IsRequired()
+                                .HasMaxLength(256)
+                                .HasColumnType("character varying(256)");
+
+                            b1.HasKey("AltTitleId");
+
+                            b1.HasIndex("MangaId");
+
+                            b1.ToTable("MangaAltTitle");
+
+                            b1.WithOwner()
+                                .HasForeignKey("MangaId");
+                        });
+
+                    b.Navigation("AltTitles");
+
+                    b.Navigation("Library");
+
+                    b.Navigation("Links");
+
+                    b.Navigation("MangaConnector");
+                });
+
+            modelBuilder.Entity("AuthorToManga", b =>
+                {
+                    b.HasOne("API.Schema.Author", null)
+                        .WithMany()
+                        .HasForeignKey("AuthorIds")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.HasOne("API.Schema.Manga", null)
+                        .WithMany()
+                        .HasForeignKey("MangaIds")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+                });
+
+            modelBuilder.Entity("JobJob", b =>
+                {
+                    b.HasOne("API.Schema.Jobs.Job", null)
+                        .WithMany()
+                        .HasForeignKey("DependsOnJobsJobId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.HasOne("API.Schema.Jobs.Job", null)
+                        .WithMany()
+                        .HasForeignKey("JobId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+                });
+
+            modelBuilder.Entity("MangaTagToManga", b =>
+                {
+                    b.HasOne("API.Schema.Manga", null)
+                        .WithMany()
+                        .HasForeignKey("MangaIds")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.HasOne("API.Schema.MangaTag", null)
+                        .WithMany()
+                        .HasForeignKey("MangaTagIds")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+                });
+
+            modelBuilder.Entity("API.Schema.Jobs.DownloadAvailableChaptersJob", b =>
+                {
+                    b.HasOne("API.Schema.Manga", "Manga")
+                        .WithMany()
+                        .HasForeignKey("MangaId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("Manga");
+                });
+
+            modelBuilder.Entity("API.Schema.Jobs.DownloadMangaCoverJob", b =>
+                {
+                    b.HasOne("API.Schema.Manga", "Manga")
+                        .WithMany()
+                        .HasForeignKey("MangaId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("Manga");
+                });
+
+            modelBuilder.Entity("API.Schema.Jobs.DownloadSingleChapterJob", b =>
+                {
+                    b.HasOne("API.Schema.Chapter", "Chapter")
+                        .WithMany()
+                        .HasForeignKey("ChapterId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("Chapter");
+                });
+
+            modelBuilder.Entity("API.Schema.Jobs.MoveMangaLibraryJob", b =>
+                {
+                    b.HasOne("API.Schema.Manga", "Manga")
+                        .WithMany()
+                        .HasForeignKey("MangaId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.HasOne("API.Schema.LocalLibrary", "ToLibrary")
+                        .WithMany()
+                        .HasForeignKey("ToLibraryId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("Manga");
+
+                    b.Navigation("ToLibrary");
+                });
+
+            modelBuilder.Entity("API.Schema.Jobs.RetrieveChaptersJob", b =>
+                {
+                    b.HasOne("API.Schema.Manga", "Manga")
+                        .WithMany()
+                        .HasForeignKey("MangaId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("Manga");
+                });
+
+            modelBuilder.Entity("API.Schema.Jobs.UpdateChaptersDownloadedJob", b =>
+                {
+                    b.HasOne("API.Schema.Manga", "Manga")
+                        .WithMany()
+                        .HasForeignKey("MangaId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("Manga");
+                });
+
+            modelBuilder.Entity("API.Schema.Jobs.UpdateSingleChapterDownloadedJob", b =>
+                {
+                    b.HasOne("API.Schema.Chapter", "Chapter")
+                        .WithMany()
+                        .HasForeignKey("ChapterId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("Chapter");
+                });
+
+            modelBuilder.Entity("API.Schema.Manga", b =>
+                {
+                    b.Navigation("Chapters");
+                });
+#pragma warning restore 612, 618
+        }
+    }
+}
diff --git a/API/Migrations/pgsql/20250516180953_Split-UpdateChaptersDownloadedJob-Into-UpdateSingleChapterDownloadedJob.cs b/API/Migrations/pgsql/20250516180953_Split-UpdateChaptersDownloadedJob-Into-UpdateSingleChapterDownloadedJob.cs
new file mode 100644
index 0000000..eff3b29
--- /dev/null
+++ b/API/Migrations/pgsql/20250516180953_Split-UpdateChaptersDownloadedJob-Into-UpdateSingleChapterDownloadedJob.cs
@@ -0,0 +1,94 @@
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace API.Migrations.pgsql
+{
+    /// <inheritdoc />
+    public partial class SplitUpdateChaptersDownloadedJobIntoUpdateSingleChapterDownloadedJob : Migration
+    {
+        /// <inheritdoc />
+        protected override void Up(MigrationBuilder migrationBuilder)
+        {
+            migrationBuilder.DropForeignKey(
+                name: "FK_Jobs_Mangas_UpdateFilesDownloadedJob_MangaId",
+                table: "Jobs");
+
+            migrationBuilder.RenameColumn(
+                name: "UpdateFilesDownloadedJob_MangaId",
+                table: "Jobs",
+                newName: "UpdateChaptersDownloadedJob_MangaId");
+
+            migrationBuilder.RenameIndex(
+                name: "IX_Jobs_UpdateFilesDownloadedJob_MangaId",
+                table: "Jobs",
+                newName: "IX_Jobs_UpdateChaptersDownloadedJob_MangaId");
+
+            migrationBuilder.AddColumn<string>(
+                name: "UpdateSingleChapterDownloadedJob_ChapterId",
+                table: "Jobs",
+                type: "character varying(64)",
+                maxLength: 64,
+                nullable: true);
+
+            migrationBuilder.CreateIndex(
+                name: "IX_Jobs_UpdateSingleChapterDownloadedJob_ChapterId",
+                table: "Jobs",
+                column: "UpdateSingleChapterDownloadedJob_ChapterId");
+
+            migrationBuilder.AddForeignKey(
+                name: "FK_Jobs_Chapters_UpdateSingleChapterDownloadedJob_ChapterId",
+                table: "Jobs",
+                column: "UpdateSingleChapterDownloadedJob_ChapterId",
+                principalTable: "Chapters",
+                principalColumn: "ChapterId",
+                onDelete: ReferentialAction.Cascade);
+
+            migrationBuilder.AddForeignKey(
+                name: "FK_Jobs_Mangas_UpdateChaptersDownloadedJob_MangaId",
+                table: "Jobs",
+                column: "UpdateChaptersDownloadedJob_MangaId",
+                principalTable: "Mangas",
+                principalColumn: "MangaId",
+                onDelete: ReferentialAction.Cascade);
+        }
+
+        /// <inheritdoc />
+        protected override void Down(MigrationBuilder migrationBuilder)
+        {
+            migrationBuilder.DropForeignKey(
+                name: "FK_Jobs_Chapters_UpdateSingleChapterDownloadedJob_ChapterId",
+                table: "Jobs");
+
+            migrationBuilder.DropForeignKey(
+                name: "FK_Jobs_Mangas_UpdateChaptersDownloadedJob_MangaId",
+                table: "Jobs");
+
+            migrationBuilder.DropIndex(
+                name: "IX_Jobs_UpdateSingleChapterDownloadedJob_ChapterId",
+                table: "Jobs");
+
+            migrationBuilder.DropColumn(
+                name: "UpdateSingleChapterDownloadedJob_ChapterId",
+                table: "Jobs");
+
+            migrationBuilder.RenameColumn(
+                name: "UpdateChaptersDownloadedJob_MangaId",
+                table: "Jobs",
+                newName: "UpdateFilesDownloadedJob_MangaId");
+
+            migrationBuilder.RenameIndex(
+                name: "IX_Jobs_UpdateChaptersDownloadedJob_MangaId",
+                table: "Jobs",
+                newName: "IX_Jobs_UpdateFilesDownloadedJob_MangaId");
+
+            migrationBuilder.AddForeignKey(
+                name: "FK_Jobs_Mangas_UpdateFilesDownloadedJob_MangaId",
+                table: "Jobs",
+                column: "UpdateFilesDownloadedJob_MangaId",
+                principalTable: "Mangas",
+                principalColumn: "MangaId",
+                onDelete: ReferentialAction.Cascade);
+        }
+    }
+}
diff --git a/API/Migrations/pgsql/PgsqlContextModelSnapshot.cs b/API/Migrations/pgsql/PgsqlContextModelSnapshot.cs
index 623bec6..9153829 100644
--- a/API/Migrations/pgsql/PgsqlContextModelSnapshot.cs
+++ b/API/Migrations/pgsql/PgsqlContextModelSnapshot.cs
@@ -413,7 +413,7 @@ namespace API.Migrations.pgsql
                     b.HasDiscriminator().HasValue((byte)5);
                 });
 
-            modelBuilder.Entity("API.Schema.Jobs.UpdateFilesDownloadedJob", b =>
+            modelBuilder.Entity("API.Schema.Jobs.UpdateChaptersDownloadedJob", b =>
                 {
                     b.HasBaseType("API.Schema.Jobs.Job");
 
@@ -427,12 +427,32 @@ namespace API.Migrations.pgsql
                     b.ToTable("Jobs", t =>
                         {
                             t.Property("MangaId")
-                                .HasColumnName("UpdateFilesDownloadedJob_MangaId");
+                                .HasColumnName("UpdateChaptersDownloadedJob_MangaId");
                         });
 
                     b.HasDiscriminator().HasValue((byte)6);
                 });
 
+            modelBuilder.Entity("API.Schema.Jobs.UpdateSingleChapterDownloadedJob", b =>
+                {
+                    b.HasBaseType("API.Schema.Jobs.Job");
+
+                    b.Property<string>("ChapterId")
+                        .IsRequired()
+                        .HasMaxLength(64)
+                        .HasColumnType("character varying(64)");
+
+                    b.HasIndex("ChapterId");
+
+                    b.ToTable("Jobs", t =>
+                        {
+                            t.Property("ChapterId")
+                                .HasColumnName("UpdateSingleChapterDownloadedJob_ChapterId");
+                        });
+
+                    b.HasDiscriminator().HasValue((byte)8);
+                });
+
             modelBuilder.Entity("API.Schema.MangaConnectors.ComickIo", b =>
                 {
                     b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
@@ -665,7 +685,7 @@ namespace API.Migrations.pgsql
                     b.Navigation("Manga");
                 });
 
-            modelBuilder.Entity("API.Schema.Jobs.UpdateFilesDownloadedJob", b =>
+            modelBuilder.Entity("API.Schema.Jobs.UpdateChaptersDownloadedJob", b =>
                 {
                     b.HasOne("API.Schema.Manga", "Manga")
                         .WithMany()
@@ -676,6 +696,17 @@ namespace API.Migrations.pgsql
                     b.Navigation("Manga");
                 });
 
+            modelBuilder.Entity("API.Schema.Jobs.UpdateSingleChapterDownloadedJob", b =>
+                {
+                    b.HasOne("API.Schema.Chapter", "Chapter")
+                        .WithMany()
+                        .HasForeignKey("ChapterId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("Chapter");
+                });
+
             modelBuilder.Entity("API.Schema.Manga", b =>
                 {
                     b.Navigation("Chapters");
diff --git a/API/Program.cs b/API/Program.cs
index 1953c47..f4b4b24 100644
--- a/API/Program.cs
+++ b/API/Program.cs
@@ -126,7 +126,7 @@ using (IServiceScope scope = app.Services.CreateScope())
         .Select(dacj =>
         {
             DownloadAvailableChaptersJob? j = dacj as DownloadAvailableChaptersJob;
-            return new UpdateFilesDownloadedJob(j!.Manga, 0);
+            return new UpdateChaptersDownloadedJob(j!.Manga, 0);
         }));
     context.Jobs.RemoveRange(context.Jobs.Where(j => j.state == JobState.Completed && j.RecurrenceMs < 1));
     foreach (Job job in context.Jobs.Where(j => j.state == JobState.Running))
diff --git a/API/Schema/Contexts/PgsqlContext.cs b/API/Schema/Contexts/PgsqlContext.cs
index aa1b25b..1ca0fed 100644
--- a/API/Schema/Contexts/PgsqlContext.cs
+++ b/API/Schema/Contexts/PgsqlContext.cs
@@ -38,7 +38,8 @@ public class PgsqlContext(DbContextOptions<PgsqlContext> options) : DbContext(op
             .HasValue<DownloadSingleChapterJob>(JobType.DownloadSingleChapterJob)
             .HasValue<DownloadMangaCoverJob>(JobType.DownloadMangaCoverJob)
             .HasValue<RetrieveChaptersJob>(JobType.RetrieveChaptersJob)
-            .HasValue<UpdateFilesDownloadedJob>(JobType.UpdateFilesDownloadedJob);
+            .HasValue<UpdateChaptersDownloadedJob>(JobType.UpdateChaptersDownloadedJob)
+            .HasValue<UpdateSingleChapterDownloadedJob>(JobType.UpdateSingleChapterDownloadedJob);
         
         //Job specification
         modelBuilder.Entity<DownloadAvailableChaptersJob>()
@@ -95,13 +96,13 @@ public class PgsqlContext(DbContextOptions<PgsqlContext> options) : DbContext(op
         modelBuilder.Entity<RetrieveChaptersJob>()
             .Navigation(j => j.Manga)
             .AutoInclude();
-        modelBuilder.Entity<UpdateFilesDownloadedJob>()
+        modelBuilder.Entity<UpdateChaptersDownloadedJob>()
             .HasOne<Manga>(j => j.Manga)
             .WithMany()
             .HasForeignKey(j => j.MangaId)
             .IsRequired()
             .OnDelete(DeleteBehavior.Cascade);
-        modelBuilder.Entity<UpdateFilesDownloadedJob>()
+        modelBuilder.Entity<UpdateChaptersDownloadedJob>()
             .Navigation(j => j.Manga)
             .AutoInclude();
         
diff --git a/API/Schema/Jobs/DownloadSingleChapterJob.cs b/API/Schema/Jobs/DownloadSingleChapterJob.cs
index f336ac2..5a84e7e 100644
--- a/API/Schema/Jobs/DownloadSingleChapterJob.cs
+++ b/API/Schema/Jobs/DownloadSingleChapterJob.cs
@@ -102,14 +102,14 @@ public class DownloadSingleChapterJob : Job
         context.Jobs.Load();
         if (context.Jobs.AsEnumerable().Any(j =>
             {
-                if (j.JobType != JobType.UpdateFilesDownloadedJob)
+                if (j.JobType != JobType.UpdateChaptersDownloadedJob)
                     return false;
-                UpdateFilesDownloadedJob job = (UpdateFilesDownloadedJob)j;
+                UpdateChaptersDownloadedJob job = (UpdateChaptersDownloadedJob)j;
                 return job.MangaId == this.Chapter.ParentMangaId;
             }))
             return [];
 
-        return [new UpdateFilesDownloadedJob(Chapter.ParentManga, 0, this.ParentJob)];
+        return [new UpdateChaptersDownloadedJob(Chapter.ParentManga, 0, this.ParentJob)];
     }
     
     private void ProcessImage(string imagePath)
diff --git a/API/Schema/Jobs/JobType.cs b/API/Schema/Jobs/JobType.cs
index 6919313..4ce607e 100644
--- a/API/Schema/Jobs/JobType.cs
+++ b/API/Schema/Jobs/JobType.cs
@@ -9,6 +9,7 @@ public enum JobType : byte
     MoveFileOrFolderJob = 3,
     DownloadMangaCoverJob = 4,
     RetrieveChaptersJob = 5,
-    UpdateFilesDownloadedJob = 6,
-    MoveMangaLibraryJob = 7
+    UpdateChaptersDownloadedJob = 6,
+    MoveMangaLibraryJob = 7,
+    UpdateSingleChapterDownloadedJob = 8,
 }
\ No newline at end of file
diff --git a/API/Schema/Jobs/UpdateChaptersDownloadedJob.cs b/API/Schema/Jobs/UpdateChaptersDownloadedJob.cs
new file mode 100644
index 0000000..a3d3859
--- /dev/null
+++ b/API/Schema/Jobs/UpdateChaptersDownloadedJob.cs
@@ -0,0 +1,34 @@
+using System.ComponentModel.DataAnnotations;
+using API.Schema.Contexts;
+using Microsoft.EntityFrameworkCore;
+using Newtonsoft.Json;
+
+namespace API.Schema.Jobs;
+
+public class UpdateChaptersDownloadedJob : Job
+{
+    [StringLength(64)] [Required] public string MangaId { get; init; }
+    [JsonIgnore] public Manga Manga { get; init; } = null!;
+    
+    public UpdateChaptersDownloadedJob(Manga manga, ulong recurrenceMs, Job? parentJob = null, ICollection<Job>? dependsOnJobs = null)
+        : base(TokenGen.CreateToken(typeof(UpdateChaptersDownloadedJob)), JobType.UpdateChaptersDownloadedJob, recurrenceMs, parentJob, dependsOnJobs)
+    {
+        this.MangaId = manga.MangaId;
+        this.Manga = manga;
+    }
+    
+    /// <summary>
+    /// EF ONLY!!!
+    /// </summary>
+    internal UpdateChaptersDownloadedJob(string mangaId, ulong recurrenceMs, string? parentJobId)
+        : base(TokenGen.CreateToken(typeof(UpdateChaptersDownloadedJob)), JobType.UpdateChaptersDownloadedJob, recurrenceMs, parentJobId)
+    {
+        this.MangaId = mangaId;
+    }
+    
+    protected override IEnumerable<Job> RunInternal(PgsqlContext context)
+    {
+        context.Attach(Manga);
+        return Manga.Chapters.Select(c => new UpdateSingleChapterDownloadedJob(c, this));
+    }
+}
\ No newline at end of file
diff --git a/API/Schema/Jobs/UpdateFilesDownloadedJob.cs b/API/Schema/Jobs/UpdateFilesDownloadedJob.cs
deleted file mode 100644
index 962b0d2..0000000
--- a/API/Schema/Jobs/UpdateFilesDownloadedJob.cs
+++ /dev/null
@@ -1,46 +0,0 @@
-using System.ComponentModel.DataAnnotations;
-using API.Schema.Contexts;
-using Microsoft.EntityFrameworkCore;
-using Newtonsoft.Json;
-
-namespace API.Schema.Jobs;
-
-public class UpdateFilesDownloadedJob : Job
-{
-    [StringLength(64)] [Required] public string MangaId { get; init; }
-    [JsonIgnore] public Manga Manga { get; init; } = null!;
-    
-    public UpdateFilesDownloadedJob(Manga manga, ulong recurrenceMs, Job? parentJob = null, ICollection<Job>? dependsOnJobs = null)
-        : base(TokenGen.CreateToken(typeof(UpdateFilesDownloadedJob)), JobType.UpdateFilesDownloadedJob, recurrenceMs, parentJob, dependsOnJobs)
-    {
-        this.MangaId = manga.MangaId;
-        this.Manga = manga;
-    }
-    
-    /// <summary>
-    /// EF ONLY!!!
-    /// </summary>
-    internal UpdateFilesDownloadedJob(string mangaId, ulong recurrenceMs, string? parentJobId)
-        : base(TokenGen.CreateToken(typeof(UpdateFilesDownloadedJob)), JobType.UpdateFilesDownloadedJob, recurrenceMs, parentJobId)
-    {
-        this.MangaId = mangaId;
-    }
-    
-    protected override IEnumerable<Job> RunInternal(PgsqlContext context)
-    {
-        context.Attach(Manga);
-        context.Entry(Manga).Collection<Chapter>(m => m.Chapters).Load();
-        foreach (Chapter chapter in Manga.Chapters)
-            chapter.Downloaded = chapter.CheckDownloaded();
-
-        try
-        {
-            context.SaveChanges();
-        }
-        catch (DbUpdateException e)
-        {
-            Log.Error(e);
-        }
-        return [];
-    }
-}
\ No newline at end of file
diff --git a/API/Schema/Jobs/UpdateSingleChapterDownloadedJob.cs b/API/Schema/Jobs/UpdateSingleChapterDownloadedJob.cs
new file mode 100644
index 0000000..15638c4
--- /dev/null
+++ b/API/Schema/Jobs/UpdateSingleChapterDownloadedJob.cs
@@ -0,0 +1,45 @@
+using System.ComponentModel.DataAnnotations;
+using API.Schema.Contexts;
+using Microsoft.EntityFrameworkCore;
+using Newtonsoft.Json;
+
+namespace API.Schema.Jobs;
+
+public class UpdateSingleChapterDownloadedJob : Job
+{
+    [StringLength(64)] [Required] public string ChapterId { get; init; }
+    [JsonIgnore] public Chapter Chapter { get; init; } = null!;
+    
+    public UpdateSingleChapterDownloadedJob(Chapter chapter, Job? parentJob = null, ICollection<Job>? dependsOnJobs = null)
+        : base(TokenGen.CreateToken(typeof(UpdateSingleChapterDownloadedJob)), JobType.UpdateSingleChapterDownloadedJob, 0, parentJob, dependsOnJobs)
+    {
+        this.ChapterId = chapter.ChapterId;
+        this.Chapter = chapter;
+    }
+    
+    /// <summary>
+    /// EF ONLY!!!
+    /// </summary>
+    internal UpdateSingleChapterDownloadedJob(string chapterId, string? parentJobId) 
+        : base(TokenGen.CreateToken(typeof(UpdateSingleChapterDownloadedJob)), JobType.UpdateSingleChapterDownloadedJob, 0, parentJobId)
+    {
+        this.ChapterId = chapterId;
+    }
+
+    protected override IEnumerable<Job> RunInternal(PgsqlContext context)
+    {
+        context.Attach(Chapter);
+        Chapter.Downloaded = Chapter.CheckDownloaded();
+        context.SaveChanges();
+
+        try
+        {
+            context.SaveChanges();
+        }
+        catch (DbUpdateException e)
+        {
+            Log.Error(e);
+        }
+        return [];
+    }
+}
\ No newline at end of file
diff --git a/API/TokenGen.cs b/API/TokenGen.cs
index 75e17c4..1745cb3 100644
--- a/API/TokenGen.cs
+++ b/API/TokenGen.cs
@@ -5,7 +5,7 @@ namespace API;
 
 public static class TokenGen
 {
-    private const int MinimumLength = 32;
+    private const int MinimumLength = 16;
     private const int MaximumLength = 64;
     private const string Chars = "abcdefghijklmnopqrstuvwxyz0123456789";
 

From 065cac62afdb96fdfb88e55810bc8bff742b1670 Mon Sep 17 00:00:00 2001
From: Glax <johanna@bernloehr.eu>
Date: Fri, 16 May 2025 20:15:32 +0200
Subject: [PATCH 29/50] Move TRANGA message

---
 API/Tranga.cs | 19 +++++++++++--------
 1 file changed, 11 insertions(+), 8 deletions(-)

diff --git a/API/Tranga.cs b/API/Tranga.cs
index 48773d9..efa59f0 100644
--- a/API/Tranga.cs
+++ b/API/Tranga.cs
@@ -12,6 +12,16 @@ namespace API;
 
 public static class Tranga
 {
+
+    // ReSharper disable once InconsistentNaming
+    private const string TRANGA = 
+        "\n\n" +
+        " _______                                 v2\n" +
+        "|_     _|.----..---.-..-----..-----..---.-.\n" +
+        "  |   |  |   _||  _  ||     ||  _  ||  _  |\n" +
+        "  |___|  |__|  |___._||__|__||___  ||___._|\n" +
+        "                             |_____|       \n\n";
+    
     public static Thread NotificationSenderThread { get; } = new (NotificationSender);
     public static Thread JobStarterThread { get; } = new (JobStarter);
     private static readonly ILog Log = LogManager.GetLogger(typeof(Tranga));
@@ -20,6 +30,7 @@ public static class Tranga
     {
         BasicConfigurator.Configure();
         Log.Info("Logger Configured.");
+        Log.Info(TRANGA);
     }
 
     private static void NotificationSender(object? serviceProviderObj)
@@ -82,14 +93,6 @@ public static class Tranga
             Log.Error("Error sending notifications.", e);
         }
     }
-
-    private const string TRANGA = 
-        "\n\n" +
-        " _______                                   \n" +
-        "|_     _|.----..---.-..-----..-----..---.-.\n" +
-        "  |   |  |   _||  _  ||     ||  _  ||  _  |\n" +
-        "  |___|  |__|  |___._||__|__||___  ||___._|\n" +
-        "                             |_____|       \n\n";
     private static readonly Dictionary<Thread, Job> RunningJobs = new();
     private static void JobStarter(object? serviceProviderObj)
     {

From 110dd36166269ee0709f4757a2a9ba760820c276 Mon Sep 17 00:00:00 2001
From: Glax <johanna@bernloehr.eu>
Date: Fri, 16 May 2025 20:16:11 +0200
Subject: [PATCH 30/50] Do not update context.Jobs on every cycle

---
 API/Tranga.cs | 22 +++++++++++-----------
 1 file changed, 11 insertions(+), 11 deletions(-)

diff --git a/API/Tranga.cs b/API/Tranga.cs
index efa59f0..bc3b638 100644
--- a/API/Tranga.cs
+++ b/API/Tranga.cs
@@ -96,6 +96,7 @@ public static class Tranga
     private static readonly Dictionary<Thread, Job> RunningJobs = new();
     private static void JobStarter(object? serviceProviderObj)
     {
+        Log.Info("JobStarter Thread running.");
         if (serviceProviderObj is null)
         {
             Log.Error("serviceProviderObj is null");
@@ -103,23 +104,22 @@ public static class Tranga
         }
         IServiceProvider serviceProvider = (IServiceProvider)serviceProviderObj;
         using IServiceScope scope = serviceProvider.CreateScope();
-        PgsqlContext? context = scope.ServiceProvider.GetService<PgsqlContext>();
-        if (context is null)
-        {
-            Log.Error("PgsqlContext is null");
-            return;
-        }
+        PgsqlContext context = scope.ServiceProvider.GetRequiredService<PgsqlContext>();
 
-        Log.Info(TRANGA);
-        Log.Info("Loading Jobs");
-        context.Jobs.Load();
-        Log.Info("JobStarter Thread running.");
+        DateTime lastContextUpdate = DateTime.UnixEpoch;
+        
         while (true)
         {
+            if (lastContextUpdate.AddMilliseconds(TrangaSettings.startNewJobTimeoutMs * 10) < DateTime.UtcNow)
+            {
+                Log.Info("Loading Jobs...");
+                context.Jobs.Load();
+                lastContextUpdate = DateTime.UtcNow;
+                Log.Info("Jobs Loaded!");
+            }
             foreach (EntityEntry entityEntry in context.ChangeTracker.Entries().ToArray())
                 entityEntry.Reload();
             //Update finished Jobs to new states
-            context.Jobs.Load();
             List<Job> completedJobs = context.Jobs.Local.Where(j => j.state == JobState.Completed).ToList();
             foreach (Job completedJob in completedJobs)
                 if (completedJob.RecurrenceMs <= 0)

From 3a3306240fd6f20da1b9d0c0de3dfcdd7a846f9f Mon Sep 17 00:00:00 2001
From: Glax <johanna@bernloehr.eu>
Date: Fri, 16 May 2025 21:05:55 +0200
Subject: [PATCH 31/50] Use LazyLoading

---
 API/API.csproj                                |  1 +
 API/Program.cs                                | 11 ++++-------
 API/Schema/Contexts/PgsqlContext.cs           | 19 +++++++++----------
 .../Jobs/DownloadAvailableChaptersJob.cs      | 15 ++++++++++++---
 API/Schema/Jobs/DownloadMangaCoverJob.cs      | 15 ++++++++++++---
 API/Schema/Jobs/DownloadSingleChapterJob.cs   | 14 +++++++++++---
 API/Schema/Jobs/Job.cs                        |  5 ++++-
 API/Schema/Jobs/MoveFileOrFolderJob.cs        |  5 +++--
 API/Schema/Jobs/MoveMangaLibraryJob.cs        | 15 ++++++++++++---
 API/Schema/Jobs/RetrieveChaptersJob.cs        | 15 ++++++++++++---
 .../Jobs/UpdateChaptersDownloadedJob.cs       | 16 ++++++++++++----
 .../Jobs/UpdateSingleChapterDownloadedJob.cs  | 15 ++++++++++++---
 12 files changed, 104 insertions(+), 42 deletions(-)

diff --git a/API/API.csproj b/API/API.csproj
index a9eb97e..24d1e9c 100644
--- a/API/API.csproj
+++ b/API/API.csproj
@@ -19,6 +19,7 @@
           <PrivateAssets>all</PrivateAssets>
           <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
         </PackageReference>
+        <PackageReference Include="Microsoft.EntityFrameworkCore.Proxies" Version="9.0.5" />
         <PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
         <PackageReference Include="Npgsql" Version="9.0.3" />
         <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4" />
diff --git a/API/Program.cs b/API/Program.cs
index f4b4b24..ef4da11 100644
--- a/API/Program.cs
+++ b/API/Program.cs
@@ -122,12 +122,9 @@ using (IServiceScope scope = app.Services.CreateScope())
         context.LocalLibraries.Add(new LocalLibrary(TrangaSettings.downloadLocation, "Default Library"));
 
     context.Jobs.AddRange(context.Jobs.Where(j => j.JobType == JobType.DownloadAvailableChaptersJob)
-        .AsEnumerable()
-        .Select(dacj =>
-        {
-            DownloadAvailableChaptersJob? j = dacj as DownloadAvailableChaptersJob;
-            return new UpdateChaptersDownloadedJob(j!.Manga, 0);
-        }));
+        .Include(downloadAvailableChaptersJob => ((DownloadAvailableChaptersJob)downloadAvailableChaptersJob).Manga)
+        .ToList()
+        .Select(dacj => new UpdateChaptersDownloadedJob(((DownloadAvailableChaptersJob)dacj).Manga, 0)));
     context.Jobs.RemoveRange(context.Jobs.Where(j => j.state == JobState.Completed && j.RecurrenceMs < 1));
     foreach (Job job in context.Jobs.Where(j => j.state == JobState.Running))
     {
@@ -153,7 +150,7 @@ using (IServiceScope scope = app.Services.CreateScope())
 TrangaSettings.Load();
 Tranga.StartLogger();
 Tranga.JobStarterThread.Start(app.Services);
-Tranga.NotificationSenderThread.Start(app.Services);
+//Tranga.NotificationSenderThread.Start(app.Services); //TODO RE-ENABLE
 
 app.UseCors("AllowAll");
 
diff --git a/API/Schema/Contexts/PgsqlContext.cs b/API/Schema/Contexts/PgsqlContext.cs
index 1ca0fed..594c9c9 100644
--- a/API/Schema/Contexts/PgsqlContext.cs
+++ b/API/Schema/Contexts/PgsqlContext.cs
@@ -50,7 +50,7 @@ public class PgsqlContext(DbContextOptions<PgsqlContext> options) : DbContext(op
             .OnDelete(DeleteBehavior.Cascade);
         modelBuilder.Entity<DownloadAvailableChaptersJob>()
             .Navigation(j => j.Manga)
-            .AutoInclude();
+            .EnableLazyLoading();
         modelBuilder.Entity<DownloadMangaCoverJob>()
             .HasOne<Manga>(j => j.Manga)
             .WithMany()
@@ -59,7 +59,7 @@ public class PgsqlContext(DbContextOptions<PgsqlContext> options) : DbContext(op
             .OnDelete(DeleteBehavior.Cascade);
         modelBuilder.Entity<DownloadMangaCoverJob>()
             .Navigation(j => j.Manga)
-            .AutoInclude();
+            .EnableLazyLoading();
         modelBuilder.Entity<DownloadSingleChapterJob>()
             .HasOne<Chapter>(j => j.Chapter)
             .WithMany()
@@ -68,7 +68,7 @@ public class PgsqlContext(DbContextOptions<PgsqlContext> options) : DbContext(op
             .OnDelete(DeleteBehavior.Cascade);
         modelBuilder.Entity<DownloadSingleChapterJob>()
             .Navigation(j => j.Chapter)
-            .AutoInclude();
+            .EnableLazyLoading();
         modelBuilder.Entity<MoveMangaLibraryJob>()
             .HasOne<Manga>(j => j.Manga)
             .WithMany()
@@ -77,7 +77,7 @@ public class PgsqlContext(DbContextOptions<PgsqlContext> options) : DbContext(op
             .OnDelete(DeleteBehavior.Cascade);
         modelBuilder.Entity<MoveMangaLibraryJob>()
             .Navigation(j => j.Manga)
-            .AutoInclude();
+            .EnableLazyLoading();
         modelBuilder.Entity<MoveMangaLibraryJob>()
             .HasOne<LocalLibrary>(j => j.ToLibrary)
             .WithMany()
@@ -86,7 +86,7 @@ public class PgsqlContext(DbContextOptions<PgsqlContext> options) : DbContext(op
             .OnDelete(DeleteBehavior.Cascade);
         modelBuilder.Entity<MoveMangaLibraryJob>()
             .Navigation(j => j.ToLibrary)
-            .AutoInclude();
+            .EnableLazyLoading();
         modelBuilder.Entity<RetrieveChaptersJob>()
             .HasOne<Manga>(j => j.Manga)
             .WithMany()
@@ -95,7 +95,7 @@ public class PgsqlContext(DbContextOptions<PgsqlContext> options) : DbContext(op
             .OnDelete(DeleteBehavior.Cascade);
         modelBuilder.Entity<RetrieveChaptersJob>()
             .Navigation(j => j.Manga)
-            .AutoInclude();
+            .EnableLazyLoading();
         modelBuilder.Entity<UpdateChaptersDownloadedJob>()
             .HasOne<Manga>(j => j.Manga)
             .WithMany()
@@ -104,14 +104,13 @@ public class PgsqlContext(DbContextOptions<PgsqlContext> options) : DbContext(op
             .OnDelete(DeleteBehavior.Cascade);
         modelBuilder.Entity<UpdateChaptersDownloadedJob>()
             .Navigation(j => j.Manga)
-            .AutoInclude();
+            .EnableLazyLoading();
         
         //Job has possible ParentJob
         modelBuilder.Entity<Job>()
-            .HasMany<Job>()
-            .WithOne(childJob => childJob.ParentJob)
+            .HasOne<Job>(childJob => childJob.ParentJob)
+            .WithMany()
             .HasForeignKey(childjob => childjob.ParentJobId)
-            .IsRequired(false)
             .OnDelete(DeleteBehavior.Cascade);
         //Job might be dependent on other Jobs
         modelBuilder.Entity<Job>()
diff --git a/API/Schema/Jobs/DownloadAvailableChaptersJob.cs b/API/Schema/Jobs/DownloadAvailableChaptersJob.cs
index 1e45cfa..4b1ae44 100644
--- a/API/Schema/Jobs/DownloadAvailableChaptersJob.cs
+++ b/API/Schema/Jobs/DownloadAvailableChaptersJob.cs
@@ -1,5 +1,6 @@
 using System.ComponentModel.DataAnnotations;
 using API.Schema.Contexts;
+using Microsoft.EntityFrameworkCore.Infrastructure;
 using Newtonsoft.Json;
 
 namespace API.Schema.Jobs;
@@ -7,7 +8,15 @@ namespace API.Schema.Jobs;
 public class DownloadAvailableChaptersJob : Job
 {
     [StringLength(64)] [Required] public string MangaId { get; init; }
-    [JsonIgnore] public Manga Manga { get; init; } = null!;
+
+    private Manga _manga = null!;
+    
+    [JsonIgnore]
+    public Manga Manga 
+    {
+        get => LazyLoader.Load(this, ref _manga);
+        init => _manga = value;
+    }
     
     public DownloadAvailableChaptersJob(Manga manga, ulong recurrenceMs, Job? parentJob = null, ICollection<Job>? dependsOnJobs = null)
         : base(TokenGen.CreateToken(typeof(DownloadAvailableChaptersJob)), JobType.DownloadAvailableChaptersJob, recurrenceMs, parentJob, dependsOnJobs)
@@ -19,8 +28,8 @@ public class DownloadAvailableChaptersJob : Job
     /// <summary>
     /// EF ONLY!!!
     /// </summary>
-    internal DownloadAvailableChaptersJob(string mangaId, ulong recurrenceMs, string? parentJobId)
-        : base(TokenGen.CreateToken(typeof(DownloadAvailableChaptersJob)), JobType.DownloadAvailableChaptersJob, recurrenceMs, parentJobId)
+    internal DownloadAvailableChaptersJob(ILazyLoader lazyLoader, string mangaId, ulong recurrenceMs, string? parentJobId)
+        : base(lazyLoader, TokenGen.CreateToken(typeof(DownloadAvailableChaptersJob)), JobType.DownloadAvailableChaptersJob, recurrenceMs, parentJobId)
     {
         this.MangaId = mangaId;
     }
diff --git a/API/Schema/Jobs/DownloadMangaCoverJob.cs b/API/Schema/Jobs/DownloadMangaCoverJob.cs
index d748fa3..1400d65 100644
--- a/API/Schema/Jobs/DownloadMangaCoverJob.cs
+++ b/API/Schema/Jobs/DownloadMangaCoverJob.cs
@@ -1,6 +1,7 @@
 using System.ComponentModel.DataAnnotations;
 using API.Schema.Contexts;
 using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
 using Newtonsoft.Json;
 
 namespace API.Schema.Jobs;
@@ -8,7 +9,15 @@ namespace API.Schema.Jobs;
 public class DownloadMangaCoverJob : Job
 {
     [StringLength(64)] [Required] public string MangaId { get; init; }
-    [JsonIgnore] public Manga Manga { get; init; } = null!;
+
+    private Manga _manga = null!;
+    
+    [JsonIgnore]
+    public Manga Manga 
+    {
+        get => LazyLoader.Load(this, ref _manga);
+        init => _manga = value;
+    }
 
     public DownloadMangaCoverJob(Manga manga, Job? parentJob = null, ICollection<Job>? dependsOnJobs = null)
         : base(TokenGen.CreateToken(typeof(DownloadMangaCoverJob)), JobType.DownloadMangaCoverJob, 0, parentJob, dependsOnJobs)
@@ -20,8 +29,8 @@ public class DownloadMangaCoverJob : Job
     /// <summary>
     /// EF ONLY!!!
     /// </summary>
-    internal DownloadMangaCoverJob(string mangaId, string? parentJobId)
-        : base(TokenGen.CreateToken(typeof(DownloadMangaCoverJob)), JobType.DownloadMangaCoverJob, 0, parentJobId)
+    internal DownloadMangaCoverJob(ILazyLoader lazyLoader, string mangaId, string? parentJobId)
+        : base(lazyLoader, TokenGen.CreateToken(typeof(DownloadMangaCoverJob)), JobType.DownloadMangaCoverJob, 0, parentJobId)
     {
         this.MangaId = mangaId;
     }
diff --git a/API/Schema/Jobs/DownloadSingleChapterJob.cs b/API/Schema/Jobs/DownloadSingleChapterJob.cs
index 5a84e7e..1f1bc70 100644
--- a/API/Schema/Jobs/DownloadSingleChapterJob.cs
+++ b/API/Schema/Jobs/DownloadSingleChapterJob.cs
@@ -4,6 +4,7 @@ using System.Runtime.InteropServices;
 using API.MangaDownloadClients;
 using API.Schema.Contexts;
 using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
 using Newtonsoft.Json;
 using SixLabors.ImageSharp;
 using SixLabors.ImageSharp.Formats.Jpeg;
@@ -17,7 +18,14 @@ public class DownloadSingleChapterJob : Job
 {
     [StringLength(64)] [Required] public string ChapterId { get; init; }
 
-    [JsonIgnore] public Chapter Chapter { get; init; } = null!;
+    private Chapter _chapter = null!;
+    
+    [JsonIgnore]
+    public Chapter Chapter 
+    {
+        get => LazyLoader.Load(this, ref _chapter);
+        init => _chapter = value;
+    }
 
     public DownloadSingleChapterJob(Chapter chapter, Job? parentJob = null, ICollection<Job>? dependsOnJobs = null)
         : base(TokenGen.CreateToken(typeof(DownloadSingleChapterJob)), JobType.DownloadSingleChapterJob, 0, parentJob, dependsOnJobs)
@@ -29,8 +37,8 @@ public class DownloadSingleChapterJob : Job
     /// <summary>
     /// EF ONLY!!!
     /// </summary>
-    internal DownloadSingleChapterJob(string chapterId, string? parentJobId)
-        : base(TokenGen.CreateToken(typeof(DownloadSingleChapterJob)), JobType.DownloadSingleChapterJob, 0, parentJobId)
+    internal DownloadSingleChapterJob(ILazyLoader lazyLoader, string chapterId, string? parentJobId)
+        : base(lazyLoader, TokenGen.CreateToken(typeof(DownloadSingleChapterJob)), JobType.DownloadSingleChapterJob, 0, parentJobId)
     {
         this.ChapterId = chapterId;
     }
diff --git a/API/Schema/Jobs/Job.cs b/API/Schema/Jobs/Job.cs
index fbb36c3..100c536 100644
--- a/API/Schema/Jobs/Job.cs
+++ b/API/Schema/Jobs/Job.cs
@@ -3,6 +3,7 @@ using System.ComponentModel.DataAnnotations.Schema;
 using API.Schema.Contexts;
 using log4net;
 using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
 using Newtonsoft.Json;
 
 namespace API.Schema.Jobs;
@@ -32,6 +33,7 @@ public abstract class Job
     [JsonIgnore] [NotMapped] internal bool DependenciesFulfilled => DependsOnJobs.All(j => j.IsCompleted);
 
     [NotMapped] [JsonIgnore] protected ILog Log { get; init; }
+    [NotMapped] [JsonIgnore] protected ILazyLoader LazyLoader { get; init; }
 
     protected Job(string jobId, JobType jobType, ulong recurrenceMs, Job? parentJob = null, ICollection<Job>? dependsOnJobs = null)
     {
@@ -48,8 +50,9 @@ public abstract class Job
     /// <summary>
     /// EF ONLY!!!
     /// </summary>
-    protected internal Job(string jobId, JobType jobType, ulong recurrenceMs, string? parentJobId)
+    protected internal Job(ILazyLoader lazyLoader, string jobId, JobType jobType, ulong recurrenceMs, string? parentJobId)
     {
+        this.LazyLoader = lazyLoader;
         this.JobId = jobId;
         this.JobType = jobType;
         this.RecurrenceMs = recurrenceMs;
diff --git a/API/Schema/Jobs/MoveFileOrFolderJob.cs b/API/Schema/Jobs/MoveFileOrFolderJob.cs
index ef71104..dbd9c71 100644
--- a/API/Schema/Jobs/MoveFileOrFolderJob.cs
+++ b/API/Schema/Jobs/MoveFileOrFolderJob.cs
@@ -1,5 +1,6 @@
 using System.ComponentModel.DataAnnotations;
 using API.Schema.Contexts;
+using Microsoft.EntityFrameworkCore.Infrastructure;
 
 namespace API.Schema.Jobs;
 
@@ -22,8 +23,8 @@ public class MoveFileOrFolderJob : Job
     /// <summary>
     /// EF ONLY!!!
     /// </summary>
-    internal MoveFileOrFolderJob(string jobId, string fromLocation, string toLocation, string? parentJobId)
-        : base(jobId, JobType.MoveFileOrFolderJob, 0, parentJobId)
+    internal MoveFileOrFolderJob(ILazyLoader lazyLoader, string jobId, string fromLocation, string toLocation, string? parentJobId)
+        : base(lazyLoader, jobId, JobType.MoveFileOrFolderJob, 0, parentJobId)
     {
         this.FromLocation = fromLocation;
         this.ToLocation = toLocation;
diff --git a/API/Schema/Jobs/MoveMangaLibraryJob.cs b/API/Schema/Jobs/MoveMangaLibraryJob.cs
index 76dcafa..2b044c1 100644
--- a/API/Schema/Jobs/MoveMangaLibraryJob.cs
+++ b/API/Schema/Jobs/MoveMangaLibraryJob.cs
@@ -1,6 +1,7 @@
 using System.ComponentModel.DataAnnotations;
 using API.Schema.Contexts;
 using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
 using Newtonsoft.Json;
 
 namespace API.Schema.Jobs;
@@ -8,7 +9,15 @@ namespace API.Schema.Jobs;
 public class MoveMangaLibraryJob : Job
 {
     [StringLength(64)] [Required] public string MangaId { get; init; }
-    [JsonIgnore] public Manga Manga { get; init; } = null!;
+
+    private Manga _manga = null!;
+    
+    [JsonIgnore]
+    public Manga Manga 
+    {
+        get => LazyLoader.Load(this, ref _manga);
+        init => _manga = value;
+    }
     [StringLength(64)] [Required] public string ToLibraryId { get; init; }
     public LocalLibrary ToLibrary { get; init; } = null!;
     
@@ -24,8 +33,8 @@ public class MoveMangaLibraryJob : Job
     /// <summary>
     /// EF ONLY!!!
     /// </summary>
-    internal MoveMangaLibraryJob(string mangaId, string toLibraryId, string? parentJobId)
-        : base(TokenGen.CreateToken(typeof(MoveMangaLibraryJob)), JobType.MoveMangaLibraryJob, 0, parentJobId)
+    internal MoveMangaLibraryJob(ILazyLoader lazyLoader, string mangaId, string toLibraryId, string? parentJobId)
+        : base(lazyLoader, TokenGen.CreateToken(typeof(MoveMangaLibraryJob)), JobType.MoveMangaLibraryJob, 0, parentJobId)
     {
         this.MangaId = mangaId;
         this.ToLibraryId = toLibraryId;
diff --git a/API/Schema/Jobs/RetrieveChaptersJob.cs b/API/Schema/Jobs/RetrieveChaptersJob.cs
index d304af8..d0b37d7 100644
--- a/API/Schema/Jobs/RetrieveChaptersJob.cs
+++ b/API/Schema/Jobs/RetrieveChaptersJob.cs
@@ -1,6 +1,7 @@
 using System.ComponentModel.DataAnnotations;
 using API.Schema.Contexts;
 using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
 using Newtonsoft.Json;
 
 namespace API.Schema.Jobs;
@@ -8,7 +9,15 @@ namespace API.Schema.Jobs;
 public class RetrieveChaptersJob : Job
 {
     [StringLength(64)] [Required] public string MangaId { get; init; }
-    [JsonIgnore] public Manga Manga { get; init; } = null!;
+
+    private Manga _manga = null!;
+    
+    [JsonIgnore]
+    public Manga Manga 
+    {
+        get => LazyLoader.Load(this, ref _manga);
+        init => _manga = value;
+    }
     [StringLength(8)] [Required] public string Language { get; private set; }
     
     public RetrieveChaptersJob(Manga manga, string language, ulong recurrenceMs, Job? parentJob = null, ICollection<Job>? dependsOnJobs = null)
@@ -22,8 +31,8 @@ public class RetrieveChaptersJob : Job
     /// <summary>
     /// EF ONLY!!!
     /// </summary>
-    internal RetrieveChaptersJob(string mangaId, string language, ulong recurrenceMs, string? parentJobId)
-        : base(TokenGen.CreateToken(typeof(RetrieveChaptersJob)), JobType.RetrieveChaptersJob, recurrenceMs, parentJobId)
+    internal RetrieveChaptersJob(ILazyLoader lazyLoader, string mangaId, string language, ulong recurrenceMs, string? parentJobId)
+        : base(lazyLoader, TokenGen.CreateToken(typeof(RetrieveChaptersJob)), JobType.RetrieveChaptersJob, recurrenceMs, parentJobId)
     {
         this.MangaId = mangaId;
         this.Language = language;
diff --git a/API/Schema/Jobs/UpdateChaptersDownloadedJob.cs b/API/Schema/Jobs/UpdateChaptersDownloadedJob.cs
index a3d3859..14dea49 100644
--- a/API/Schema/Jobs/UpdateChaptersDownloadedJob.cs
+++ b/API/Schema/Jobs/UpdateChaptersDownloadedJob.cs
@@ -1,6 +1,6 @@
 using System.ComponentModel.DataAnnotations;
 using API.Schema.Contexts;
-using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
 using Newtonsoft.Json;
 
 namespace API.Schema.Jobs;
@@ -8,7 +8,15 @@ namespace API.Schema.Jobs;
 public class UpdateChaptersDownloadedJob : Job
 {
     [StringLength(64)] [Required] public string MangaId { get; init; }
-    [JsonIgnore] public Manga Manga { get; init; } = null!;
+
+    private Manga _manga = null!;
+    
+    [JsonIgnore]
+    public Manga Manga 
+    {
+        get => LazyLoader.Load(this, ref _manga);
+        init => _manga = value;
+    }
     
     public UpdateChaptersDownloadedJob(Manga manga, ulong recurrenceMs, Job? parentJob = null, ICollection<Job>? dependsOnJobs = null)
         : base(TokenGen.CreateToken(typeof(UpdateChaptersDownloadedJob)), JobType.UpdateChaptersDownloadedJob, recurrenceMs, parentJob, dependsOnJobs)
@@ -20,8 +28,8 @@ public class UpdateChaptersDownloadedJob : Job
     /// <summary>
     /// EF ONLY!!!
     /// </summary>
-    internal UpdateChaptersDownloadedJob(string mangaId, ulong recurrenceMs, string? parentJobId)
-        : base(TokenGen.CreateToken(typeof(UpdateChaptersDownloadedJob)), JobType.UpdateChaptersDownloadedJob, recurrenceMs, parentJobId)
+    internal UpdateChaptersDownloadedJob(ILazyLoader lazyLoader, string mangaId, ulong recurrenceMs, string? parentJobId)
+        : base(lazyLoader, TokenGen.CreateToken(typeof(UpdateChaptersDownloadedJob)), JobType.UpdateChaptersDownloadedJob, recurrenceMs, parentJobId)
     {
         this.MangaId = mangaId;
     }
diff --git a/API/Schema/Jobs/UpdateSingleChapterDownloadedJob.cs b/API/Schema/Jobs/UpdateSingleChapterDownloadedJob.cs
index 15638c4..fa787cc 100644
--- a/API/Schema/Jobs/UpdateSingleChapterDownloadedJob.cs
+++ b/API/Schema/Jobs/UpdateSingleChapterDownloadedJob.cs
@@ -1,6 +1,7 @@
 using System.ComponentModel.DataAnnotations;
 using API.Schema.Contexts;
 using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
 using Newtonsoft.Json;
 
 namespace API.Schema.Jobs;
@@ -8,7 +9,15 @@ namespace API.Schema.Jobs;
 public class UpdateSingleChapterDownloadedJob : Job
 {
     [StringLength(64)] [Required] public string ChapterId { get; init; }
-    [JsonIgnore] public Chapter Chapter { get; init; } = null!;
+
+    private Chapter _chapter = null!;
+    
+    [JsonIgnore]
+    public Chapter Chapter 
+    {
+        get => LazyLoader.Load(this, ref _chapter);
+        init => _chapter = value;
+    }
     
     public UpdateSingleChapterDownloadedJob(Chapter chapter, Job? parentJob = null, ICollection<Job>? dependsOnJobs = null)
         : base(TokenGen.CreateToken(typeof(UpdateSingleChapterDownloadedJob)), JobType.UpdateSingleChapterDownloadedJob, 0, parentJob, dependsOnJobs)
@@ -20,8 +29,8 @@ public class UpdateSingleChapterDownloadedJob : Job
     /// <summary>
     /// EF ONLY!!!
     /// </summary>
-    internal UpdateSingleChapterDownloadedJob(string chapterId, string? parentJobId) 
-        : base(TokenGen.CreateToken(typeof(UpdateSingleChapterDownloadedJob)), JobType.UpdateSingleChapterDownloadedJob, 0, parentJobId)
+    internal UpdateSingleChapterDownloadedJob(ILazyLoader lazyLoader, string chapterId, string? parentJobId) 
+        : base(lazyLoader, TokenGen.CreateToken(typeof(UpdateSingleChapterDownloadedJob)), JobType.UpdateSingleChapterDownloadedJob, 0, parentJobId)
     {
         this.ChapterId = chapterId;
     }

From d6e945741ae00d460cf58c6e53bb914ef51d661c Mon Sep 17 00:00:00 2001
From: Glax <johanna@bernloehr.eu>
Date: Fri, 16 May 2025 21:09:32 +0200
Subject: [PATCH 32/50] Fix Manga-Chapter loading

---
 API/Schema/Jobs/DownloadAvailableChaptersJob.cs     | 1 +
 API/Schema/Jobs/UpdateChaptersDownloadedJob.cs      | 1 +
 API/Schema/Jobs/UpdateSingleChapterDownloadedJob.cs | 1 -
 3 files changed, 2 insertions(+), 1 deletion(-)

diff --git a/API/Schema/Jobs/DownloadAvailableChaptersJob.cs b/API/Schema/Jobs/DownloadAvailableChaptersJob.cs
index 4b1ae44..907e359 100644
--- a/API/Schema/Jobs/DownloadAvailableChaptersJob.cs
+++ b/API/Schema/Jobs/DownloadAvailableChaptersJob.cs
@@ -37,6 +37,7 @@ public class DownloadAvailableChaptersJob : Job
     protected override IEnumerable<Job> RunInternal(PgsqlContext context)
     {
         context.Attach(Manga);
+        context.Entry(Manga).Collection<Chapter>(m => m.Chapters).Load();
         return Manga.Chapters.Select(chapter => new DownloadSingleChapterJob(chapter, this));
     }
 }
\ No newline at end of file
diff --git a/API/Schema/Jobs/UpdateChaptersDownloadedJob.cs b/API/Schema/Jobs/UpdateChaptersDownloadedJob.cs
index 14dea49..f62b8a6 100644
--- a/API/Schema/Jobs/UpdateChaptersDownloadedJob.cs
+++ b/API/Schema/Jobs/UpdateChaptersDownloadedJob.cs
@@ -37,6 +37,7 @@ public class UpdateChaptersDownloadedJob : Job
     protected override IEnumerable<Job> RunInternal(PgsqlContext context)
     {
         context.Attach(Manga);
+        context.Entry(Manga).Collection<Chapter>(m => m.Chapters).Load();
         return Manga.Chapters.Select(c => new UpdateSingleChapterDownloadedJob(c, this));
     }
 }
\ No newline at end of file
diff --git a/API/Schema/Jobs/UpdateSingleChapterDownloadedJob.cs b/API/Schema/Jobs/UpdateSingleChapterDownloadedJob.cs
index fa787cc..0435938 100644
--- a/API/Schema/Jobs/UpdateSingleChapterDownloadedJob.cs
+++ b/API/Schema/Jobs/UpdateSingleChapterDownloadedJob.cs
@@ -39,7 +39,6 @@ public class UpdateSingleChapterDownloadedJob : Job
     {
         context.Attach(Chapter);
         Chapter.Downloaded = Chapter.CheckDownloaded();
-        context.SaveChanges();
 
         try
         {

From 8e0c964883428dff383ad102f4ac718c12a744f1 Mon Sep 17 00:00:00 2001
From: Glax <johanna@bernloehr.eu>
Date: Fri, 16 May 2025 21:09:43 +0200
Subject: [PATCH 33/50] Update Jobs on each cycle (since it is super fast now)

---
 API/Tranga.cs | 14 +++++---------
 1 file changed, 5 insertions(+), 9 deletions(-)

diff --git a/API/Tranga.cs b/API/Tranga.cs
index bc3b638..03b3b6d 100644
--- a/API/Tranga.cs
+++ b/API/Tranga.cs
@@ -105,20 +105,16 @@ public static class Tranga
         IServiceProvider serviceProvider = (IServiceProvider)serviceProviderObj;
         using IServiceScope scope = serviceProvider.CreateScope();
         PgsqlContext context = scope.ServiceProvider.GetRequiredService<PgsqlContext>();
-
-        DateTime lastContextUpdate = DateTime.UnixEpoch;
         
         while (true)
         {
-            if (lastContextUpdate.AddMilliseconds(TrangaSettings.startNewJobTimeoutMs * 10) < DateTime.UtcNow)
-            {
-                Log.Info("Loading Jobs...");
-                context.Jobs.Load();
-                lastContextUpdate = DateTime.UtcNow;
-                Log.Info("Jobs Loaded!");
-            }
+            Log.Info("Loading Jobs...");
+            DateTime loadStart = DateTime.UtcNow;
+            context.Jobs.Load();
+            Log.Info("Updating Entries...");
             foreach (EntityEntry entityEntry in context.ChangeTracker.Entries().ToArray())
                 entityEntry.Reload();
+            Log.Info($"Jobs Loaded! (took {DateTime.UtcNow.Subtract(loadStart).TotalMilliseconds}ms)");
             //Update finished Jobs to new states
             List<Job> completedJobs = context.Jobs.Local.Where(j => j.state == JobState.Completed).ToList();
             foreach (Job completedJob in completedJobs)

From 021ad5e804920b2e5bde034f509022540b4fb74a Mon Sep 17 00:00:00 2001
From: Glax <johanna@bernloehr.eu>
Date: Fri, 16 May 2025 21:18:19 +0200
Subject: [PATCH 34/50] Include FullArchivePath in Chapter-Response

---
 API/Schema/Chapter.cs | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/API/Schema/Chapter.cs b/API/Schema/Chapter.cs
index 054dd7a..75fec12 100644
--- a/API/Schema/Chapter.cs
+++ b/API/Schema/Chapter.cs
@@ -26,7 +26,7 @@ public class Chapter : IComparable<Chapter>
     [StringLength(256)] [Required] public string FileName { get; private set; }
 
     [Required] public bool Downloaded { get; internal set; }
-    [JsonIgnore] [NotMapped] public string FullArchiveFilePath => Path.Join(ParentManga.FullDirectoryPath, FileName);
+    [NotMapped] public string FullArchiveFilePath => Path.Join(ParentManga.FullDirectoryPath, FileName);
 
     public Chapter(Manga parentManga, string url, string chapterNumber, int? volumeNumber = null, string? title = null)
     {

From e45b72dcf9aff5db163270dcc20d78fad05e8802 Mon Sep 17 00:00:00 2001
From: Glax <johanna@bernloehr.eu>
Date: Fri, 16 May 2025 21:18:37 +0200
Subject: [PATCH 35/50] Include Spaces in Directory-Path

---
 API/Schema/Manga.cs | 6 ++++--
 1 file changed, 4 insertions(+), 2 deletions(-)

diff --git a/API/Schema/Manga.cs b/API/Schema/Manga.cs
index 2374da1..2620193 100644
--- a/API/Schema/Manga.cs
+++ b/API/Schema/Manga.cs
@@ -97,7 +97,9 @@ public class Manga
     
     public string CreatePublicationFolder()
     {
-        string publicationFolder = FullDirectoryPath;
+        string? publicationFolder = FullDirectoryPath;
+        if (publicationFolder is null)
+            throw new DirectoryNotFoundException("Publication folder not found");
         if(!Directory.Exists(publicationFolder))
             Directory.CreateDirectory(publicationFolder);
         if(RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
@@ -131,7 +133,7 @@ public class Manga
         StringBuilder sb = new ();
         foreach (char c in name)
         {
-            if (c > 32 && c < 127 && ForbiddenCharsBelow127.Contains(c) == false)
+            if (c >= 32 && c < 127 && ForbiddenCharsBelow127.Contains(c) == false)
                 sb.Append(c);
             else if (c > 127 && c < 152 && IncludeCharsAbove127.Contains(c))
                 sb.Append(c);

From 63fee081e6cc9fc36b22e49ca38f7183caf114df Mon Sep 17 00:00:00 2001
From: Glax <johanna@bernloehr.eu>
Date: Fri, 16 May 2025 21:18:46 +0200
Subject: [PATCH 36/50] Catch all Exceptions in Job

---
 API/Schema/Jobs/Job.cs | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/API/Schema/Jobs/Job.cs b/API/Schema/Jobs/Job.cs
index 100c536..7b601b5 100644
--- a/API/Schema/Jobs/Job.cs
+++ b/API/Schema/Jobs/Job.cs
@@ -81,7 +81,7 @@ public abstract class Job
             Log.Info($"Job {JobId} completed. Generated {newJobs.Length} new jobs.");
             return newJobs;
         }
-        catch (DbUpdateException e)
+        catch (Exception e)
         {
             this.state = JobState.Failed;
             Log.Error($"Failed to run job {JobId}", e);

From 4bc70eca6873f006c54a4c4c17797ef71ec4ec66 Mon Sep 17 00:00:00 2001
From: Glax <johanna@bernloehr.eu>
Date: Fri, 16 May 2025 21:23:47 +0200
Subject: [PATCH 37/50] Distinct Jobs

---
 API/Tranga.cs | 1 +
 1 file changed, 1 insertion(+)

diff --git a/API/Tranga.cs b/API/Tranga.cs
index 03b3b6d..ac83e49 100644
--- a/API/Tranga.cs
+++ b/API/Tranga.cs
@@ -145,6 +145,7 @@ public static class Tranga
             List<Job> jobsWithoutDownloading =
                 jobsWithoutMissingDependencies
                     .Where(j => j.JobType != JobType.DownloadSingleChapterJob)
+                    .DistinctBy(j => j.JobType)
                     .ToList();
             List<Job> firstChapterPerConnector =
                 jobsWithoutMissingDependencies

From 5a6dc5a5b24b2a56cb22f1b4b8bc33d4a85f91f5 Mon Sep 17 00:00:00 2001
From: Glax <johanna@bernloehr.eu>
Date: Fri, 16 May 2025 21:25:08 +0200
Subject: [PATCH 38/50] Logging for Jobs-Filtering

---
 API/Tranga.cs | 11 +++++++----
 1 file changed, 7 insertions(+), 4 deletions(-)

diff --git a/API/Tranga.cs b/API/Tranga.cs
index ac83e49..b1b0fe7 100644
--- a/API/Tranga.cs
+++ b/API/Tranga.cs
@@ -107,14 +107,13 @@ public static class Tranga
         PgsqlContext context = scope.ServiceProvider.GetRequiredService<PgsqlContext>();
         
         while (true)
-        {
-            Log.Info("Loading Jobs...");
+        {            Log.Debug("Loading Jobs...");
             DateTime loadStart = DateTime.UtcNow;
             context.Jobs.Load();
-            Log.Info("Updating Entries...");
+            Log.Debug("Updating Entries...");
             foreach (EntityEntry entityEntry in context.ChangeTracker.Entries().ToArray())
                 entityEntry.Reload();
-            Log.Info($"Jobs Loaded! (took {DateTime.UtcNow.Subtract(loadStart).TotalMilliseconds}ms)");
+            Log.Debug($"Jobs Loaded! (took {DateTime.UtcNow.Subtract(loadStart).TotalMilliseconds}ms)");
             //Update finished Jobs to new states
             List<Job> completedJobs = context.Jobs.Local.Where(j => j.state == JobState.Completed).ToList();
             foreach (Job completedJob in completedJobs)
@@ -135,6 +134,8 @@ public static class Tranga
             //Retrieve waiting and due Jobs
             List<Job> runningJobs = context.Jobs.Local.Where(j => j.state == JobState.Running).ToList();
             
+            DateTime filterStart = DateTime.UtcNow;
+            Log.Debug("Filtering Jobs...");
             List<MangaConnector> busyConnectors = GetBusyConnectors(runningJobs);
 
             List<Job> waitingJobs = GetWaitingJobs(context.Jobs.Local.ToList());
@@ -163,6 +164,8 @@ public static class Tranga
                     .ToList();
 
             List<Job> startJobs = jobsWithoutDownloading.Concat(firstChapterPerConnector).ToList();
+            Log.Debug($"Jobs Filtered! (took {DateTime.UtcNow.Subtract(filterStart).TotalMilliseconds}ms)");
+            
             
             //Start Jobs that are allowed to run (preconditions match)
             foreach (Job job in startJobs)

From 49b382fe1f78bb116ca19581c0a562c39b1765af Mon Sep 17 00:00:00 2001
From: Glax <johanna@bernloehr.eu>
Date: Fri, 16 May 2025 21:27:02 +0200
Subject: [PATCH 39/50] Logging for Job-Cycle

---
 API/Tranga.cs | 6 +++++-
 1 file changed, 5 insertions(+), 1 deletion(-)

diff --git a/API/Tranga.cs b/API/Tranga.cs
index b1b0fe7..f5039f8 100644
--- a/API/Tranga.cs
+++ b/API/Tranga.cs
@@ -107,7 +107,10 @@ public static class Tranga
         PgsqlContext context = scope.ServiceProvider.GetRequiredService<PgsqlContext>();
         
         while (true)
-        {            Log.Debug("Loading Jobs...");
+        {            
+            Log.Debug("Starting Job-Cycle...");
+            DateTime cycleStart = DateTime.UtcNow;
+            Log.Debug("Loading Jobs...");
             DateTime loadStart = DateTime.UtcNow;
             context.Jobs.Load();
             Log.Debug("Updating Entries...");
@@ -198,6 +201,7 @@ public static class Tranga
             {
                 Log.Error("Failed saving Job changes.", e);
             }
+            Log.Debug($"Job-Cycle over! (took {DateTime.UtcNow.Subtract(cycleStart).TotalMilliseconds}ms)");
             Thread.Sleep(TrangaSettings.startNewJobTimeoutMs);
         }
     }

From 622198a09e8ea6f06286cc0340bfd2db79312077 Mon Sep 17 00:00:00 2001
From: Glax <johanna@bernloehr.eu>
Date: Fri, 16 May 2025 21:32:42 +0200
Subject: [PATCH 40/50] Changes to Job.cs: - Nest try-catch for
 DBUpdateException and other Exceptions - More Logging for Jobs

---
 API/Schema/Jobs/Job.cs | 52 +++++++++++++++++++++++++++++-------------
 1 file changed, 36 insertions(+), 16 deletions(-)

diff --git a/API/Schema/Jobs/Job.cs b/API/Schema/Jobs/Job.cs
index 7b601b5..cf2b5d9 100644
--- a/API/Schema/Jobs/Job.cs
+++ b/API/Schema/Jobs/Job.cs
@@ -64,29 +64,49 @@ public abstract class Job
 
     public IEnumerable<Job> Run(IServiceProvider serviceProvider)
     {
-        Log.Debug($"Running job {JobId}");
-        using IServiceScope scope = serviceProvider.CreateScope();
+        Log.Info($"Running job {JobId}");
+        DateTime jobStart = DateTime.UtcNow;
+        Job[]? ret = null;
 
         try
         {
+
+            using IServiceScope scope = serviceProvider.CreateScope();
             PgsqlContext context = scope.ServiceProvider.GetRequiredService<PgsqlContext>();
-            context.Attach(this);
-            this.state = JobState.Running;
-            context.SaveChanges();
-            Job[] newJobs = RunInternal(context).ToArray();
-            this.state = JobState.Completed;
-            context.SaveChanges();
-            context.Jobs.AddRange(newJobs);
-            context.SaveChanges();
-            Log.Info($"Job {JobId} completed. Generated {newJobs.Length} new jobs.");
-            return newJobs;
+            try
+            {
+                context.Attach(this);
+                this.state = JobState.Running;
+                context.SaveChanges();
+                ret = RunInternal(context).ToArray();
+                this.state = JobState.Completed;
+                context.Jobs.AddRange(ret);
+                Log.Info($"Job {JobId} completed. Generated {ret.Length} new jobs.");
+            }
+            catch (Exception e)
+            {
+                if (e is not DbUpdateException dbEx)
+                {
+                    this.state = JobState.Failed;
+                    Log.Error($"Failed to run job {JobId}", e);
+                }
+                else
+                {
+                    throw;
+                }
+            }
+            finally
+            {
+                context.SaveChanges();
+            }
         }
-        catch (Exception e)
+        catch (DbUpdateException e)
         {
-            this.state = JobState.Failed;
-            Log.Error($"Failed to run job {JobId}", e);
-            return [];
+            Log.Error($"Failed to update Database {JobId}", e);
         }
+        
+        Log.Info($"Finished Job {JobId}! (took {DateTime.UtcNow.Subtract(jobStart).TotalMilliseconds}ms)");
+        return ret ?? [];
     }
     
     protected abstract IEnumerable<Job> RunInternal(PgsqlContext context);

From 6258e07f20c745519377ddaf838d51513d8db149 Mon Sep 17 00:00:00 2001
From: Glax <johanna@bernloehr.eu>
Date: Fri, 16 May 2025 21:36:24 +0200
Subject: [PATCH 41/50] Remove unnecessary Attachments

---
 API/Schema/Jobs/DownloadAvailableChaptersJob.cs     | 1 -
 API/Schema/Jobs/DownloadMangaCoverJob.cs            | 1 -
 API/Schema/Jobs/DownloadSingleChapterJob.cs         | 5 +----
 API/Schema/Jobs/RetrieveChaptersJob.cs              | 1 -
 API/Schema/Jobs/UpdateChaptersDownloadedJob.cs      | 1 -
 API/Schema/Jobs/UpdateSingleChapterDownloadedJob.cs | 1 -
 6 files changed, 1 insertion(+), 9 deletions(-)

diff --git a/API/Schema/Jobs/DownloadAvailableChaptersJob.cs b/API/Schema/Jobs/DownloadAvailableChaptersJob.cs
index 907e359..cffd60f 100644
--- a/API/Schema/Jobs/DownloadAvailableChaptersJob.cs
+++ b/API/Schema/Jobs/DownloadAvailableChaptersJob.cs
@@ -36,7 +36,6 @@ public class DownloadAvailableChaptersJob : Job
     
     protected override IEnumerable<Job> RunInternal(PgsqlContext context)
     {
-        context.Attach(Manga);
         context.Entry(Manga).Collection<Chapter>(m => m.Chapters).Load();
         return Manga.Chapters.Select(chapter => new DownloadSingleChapterJob(chapter, this));
     }
diff --git a/API/Schema/Jobs/DownloadMangaCoverJob.cs b/API/Schema/Jobs/DownloadMangaCoverJob.cs
index 1400d65..f41126c 100644
--- a/API/Schema/Jobs/DownloadMangaCoverJob.cs
+++ b/API/Schema/Jobs/DownloadMangaCoverJob.cs
@@ -37,7 +37,6 @@ public class DownloadMangaCoverJob : Job
     
     protected override IEnumerable<Job> RunInternal(PgsqlContext context)
     {
-        context.Attach(Manga);
         try
         {
             Manga.CoverFileNameInCache = Manga.MangaConnector.SaveCoverImageToCache(Manga);
diff --git a/API/Schema/Jobs/DownloadSingleChapterJob.cs b/API/Schema/Jobs/DownloadSingleChapterJob.cs
index 1f1bc70..564d181 100644
--- a/API/Schema/Jobs/DownloadSingleChapterJob.cs
+++ b/API/Schema/Jobs/DownloadSingleChapterJob.cs
@@ -45,8 +45,6 @@ public class DownloadSingleChapterJob : Job
     
     protected override IEnumerable<Job> RunInternal(PgsqlContext context)
     {
-        context.Attach(Chapter);
-        context.Attach(Chapter.ParentManga);
         string[] imageUrls = Chapter.ParentManga.MangaConnector.GetChapterImageUrls(Chapter);
         if (imageUrls.Length < 1)
         {
@@ -107,8 +105,7 @@ public class DownloadSingleChapterJob : Job
         Chapter.Downloaded = true;
         context.SaveChanges();
 
-        context.Jobs.Load();
-        if (context.Jobs.AsEnumerable().Any(j =>
+        if (context.Jobs.ToList().Any(j =>
             {
                 if (j.JobType != JobType.UpdateChaptersDownloadedJob)
                     return false;
diff --git a/API/Schema/Jobs/RetrieveChaptersJob.cs b/API/Schema/Jobs/RetrieveChaptersJob.cs
index d0b37d7..95866fc 100644
--- a/API/Schema/Jobs/RetrieveChaptersJob.cs
+++ b/API/Schema/Jobs/RetrieveChaptersJob.cs
@@ -40,7 +40,6 @@ public class RetrieveChaptersJob : Job
     
     protected override IEnumerable<Job> RunInternal(PgsqlContext context)
     {
-        context.Attach(Manga);
         context.Entry(Manga).Collection<Chapter>(m => m.Chapters).Load();
         // This gets all chapters that are not downloaded
         Chapter[] allChapters = Manga.MangaConnector.GetChapters(Manga, Language);
diff --git a/API/Schema/Jobs/UpdateChaptersDownloadedJob.cs b/API/Schema/Jobs/UpdateChaptersDownloadedJob.cs
index f62b8a6..b05d874 100644
--- a/API/Schema/Jobs/UpdateChaptersDownloadedJob.cs
+++ b/API/Schema/Jobs/UpdateChaptersDownloadedJob.cs
@@ -36,7 +36,6 @@ public class UpdateChaptersDownloadedJob : Job
     
     protected override IEnumerable<Job> RunInternal(PgsqlContext context)
     {
-        context.Attach(Manga);
         context.Entry(Manga).Collection<Chapter>(m => m.Chapters).Load();
         return Manga.Chapters.Select(c => new UpdateSingleChapterDownloadedJob(c, this));
     }
diff --git a/API/Schema/Jobs/UpdateSingleChapterDownloadedJob.cs b/API/Schema/Jobs/UpdateSingleChapterDownloadedJob.cs
index 0435938..8ff69bf 100644
--- a/API/Schema/Jobs/UpdateSingleChapterDownloadedJob.cs
+++ b/API/Schema/Jobs/UpdateSingleChapterDownloadedJob.cs
@@ -37,7 +37,6 @@ public class UpdateSingleChapterDownloadedJob : Job
 
     protected override IEnumerable<Job> RunInternal(PgsqlContext context)
     {
-        context.Attach(Chapter);
         Chapter.Downloaded = Chapter.CheckDownloaded();
 
         try

From 225b7f02ad869972070b3647401fb1cea45a0a28 Mon Sep 17 00:00:00 2001
From: Glax <johanna@bernloehr.eu>
Date: Fri, 16 May 2025 21:53:59 +0200
Subject: [PATCH 42/50] Lazy Load Jobs.DependsOnJobs, Manga.Chapters

---
 API/Schema/Contexts/PgsqlContext.cs           |  6 +-
 .../Jobs/DownloadAvailableChaptersJob.cs      |  1 -
 API/Schema/Jobs/Job.cs                        | 59 +++++++++----------
 API/Schema/Jobs/MoveMangaLibraryJob.cs        |  2 -
 API/Schema/Jobs/RetrieveChaptersJob.cs        |  1 -
 .../Jobs/UpdateChaptersDownloadedJob.cs       |  1 -
 API/Schema/Manga.cs                           | 17 +++++-
 7 files changed, 45 insertions(+), 42 deletions(-)

diff --git a/API/Schema/Contexts/PgsqlContext.cs b/API/Schema/Contexts/PgsqlContext.cs
index 594c9c9..8fc0b8a 100644
--- a/API/Schema/Contexts/PgsqlContext.cs
+++ b/API/Schema/Contexts/PgsqlContext.cs
@@ -118,7 +118,8 @@ public class PgsqlContext(DbContextOptions<PgsqlContext> options) : DbContext(op
             .WithMany();
         modelBuilder.Entity<Job>()
             .Navigation(j => j.DependsOnJobs)
-            .AutoInclude(false);
+            .AutoInclude(false)
+            .EnableLazyLoading();
         
         //MangaConnector Types
         modelBuilder.Entity<MangaConnector>()
@@ -149,7 +150,8 @@ public class PgsqlContext(DbContextOptions<PgsqlContext> options) : DbContext(op
             .AutoInclude();
         modelBuilder.Entity<Manga>()
             .Navigation(m => m.Chapters)
-            .AutoInclude(false);
+            .AutoInclude(false)
+            .EnableLazyLoading();
         //Manga owns MangaAltTitles
         modelBuilder.Entity<Manga>()
             .OwnsMany<MangaAltTitle>(m => m.AltTitles)
diff --git a/API/Schema/Jobs/DownloadAvailableChaptersJob.cs b/API/Schema/Jobs/DownloadAvailableChaptersJob.cs
index cffd60f..961cb3a 100644
--- a/API/Schema/Jobs/DownloadAvailableChaptersJob.cs
+++ b/API/Schema/Jobs/DownloadAvailableChaptersJob.cs
@@ -36,7 +36,6 @@ public class DownloadAvailableChaptersJob : Job
     
     protected override IEnumerable<Job> RunInternal(PgsqlContext context)
     {
-        context.Entry(Manga).Collection<Chapter>(m => m.Chapters).Load();
         return Manga.Chapters.Select(chapter => new DownloadSingleChapterJob(chapter, this));
     }
 }
\ No newline at end of file
diff --git a/API/Schema/Jobs/Job.cs b/API/Schema/Jobs/Job.cs
index cf2b5d9..ffcb227 100644
--- a/API/Schema/Jobs/Job.cs
+++ b/API/Schema/Jobs/Job.cs
@@ -17,7 +17,12 @@ public abstract class Job
 
     [StringLength(64)] public string? ParentJobId { get; init; }
     [JsonIgnore] public Job? ParentJob { get; init; }
-    [JsonIgnore] public ICollection<Job> DependsOnJobs { get; init; }
+    private ICollection<Job> _dependsOnJobs = null!;
+    [JsonIgnore] public ICollection<Job> DependsOnJobs
+    {
+        get => LazyLoader.Load(this, ref _dependsOnJobs);
+        init => _dependsOnJobs = value;
+    }
 
     [Required] public JobType JobType { get; init; }
 
@@ -68,41 +73,31 @@ public abstract class Job
         DateTime jobStart = DateTime.UtcNow;
         Job[]? ret = null;
 
+        using IServiceScope scope = serviceProvider.CreateScope();
+        PgsqlContext context = scope.ServiceProvider.GetRequiredService<PgsqlContext>();
         try
         {
-
-            using IServiceScope scope = serviceProvider.CreateScope();
-            PgsqlContext context = scope.ServiceProvider.GetRequiredService<PgsqlContext>();
-            try
-            {
-                context.Attach(this);
-                this.state = JobState.Running;
-                context.SaveChanges();
-                ret = RunInternal(context).ToArray();
-                this.state = JobState.Completed;
-                context.Jobs.AddRange(ret);
-                Log.Info($"Job {JobId} completed. Generated {ret.Length} new jobs.");
-            }
-            catch (Exception e)
-            {
-                if (e is not DbUpdateException dbEx)
-                {
-                    this.state = JobState.Failed;
-                    Log.Error($"Failed to run job {JobId}", e);
-                }
-                else
-                {
-                    throw;
-                }
-            }
-            finally
-            {
-                context.SaveChanges();
-            }
+            context.Attach(this);
+            this.state = JobState.Running;
+            context.SaveChanges();
+            ret = RunInternal(context).ToArray();
+            this.state = JobState.Completed;
+            context.Jobs.AddRange(ret);
+            Log.Info($"Job {JobId} completed. Generated {ret.Length} new jobs.");
+            context.SaveChanges();
         }
-        catch (DbUpdateException e)
+        catch (Exception e)
         {
-            Log.Error($"Failed to update Database {JobId}", e);
+            if (e is not DbUpdateException)
+            {
+                this.state = JobState.Failed;
+                Log.Error($"Failed to run job {JobId}", e);
+                context.SaveChanges();
+            }
+            else
+            {
+                Log.Error($"Failed to update Database {JobId}", e);
+            }
         }
         
         Log.Info($"Finished Job {JobId}! (took {DateTime.UtcNow.Subtract(jobStart).TotalMilliseconds}ms)");
diff --git a/API/Schema/Jobs/MoveMangaLibraryJob.cs b/API/Schema/Jobs/MoveMangaLibraryJob.cs
index 2b044c1..365ee1f 100644
--- a/API/Schema/Jobs/MoveMangaLibraryJob.cs
+++ b/API/Schema/Jobs/MoveMangaLibraryJob.cs
@@ -42,8 +42,6 @@ public class MoveMangaLibraryJob : Job
     
     protected override IEnumerable<Job> RunInternal(PgsqlContext context)
     {
-        context.Attach(Manga);
-        context.Entry(Manga).Collection<Chapter>(m => m.Chapters).Load();
         Dictionary<Chapter, string> oldPath = Manga.Chapters.ToDictionary(c => c, c => c.FullArchiveFilePath);
         Manga.Library = ToLibrary;
         try
diff --git a/API/Schema/Jobs/RetrieveChaptersJob.cs b/API/Schema/Jobs/RetrieveChaptersJob.cs
index 95866fc..56ee642 100644
--- a/API/Schema/Jobs/RetrieveChaptersJob.cs
+++ b/API/Schema/Jobs/RetrieveChaptersJob.cs
@@ -40,7 +40,6 @@ public class RetrieveChaptersJob : Job
     
     protected override IEnumerable<Job> RunInternal(PgsqlContext context)
     {
-        context.Entry(Manga).Collection<Chapter>(m => m.Chapters).Load();
         // This gets all chapters that are not downloaded
         Chapter[] allChapters = Manga.MangaConnector.GetChapters(Manga, Language);
         Chapter[] newChapters = allChapters.Where(chapter => Manga.Chapters.Contains(chapter) == false).ToArray();
diff --git a/API/Schema/Jobs/UpdateChaptersDownloadedJob.cs b/API/Schema/Jobs/UpdateChaptersDownloadedJob.cs
index b05d874..41228ea 100644
--- a/API/Schema/Jobs/UpdateChaptersDownloadedJob.cs
+++ b/API/Schema/Jobs/UpdateChaptersDownloadedJob.cs
@@ -36,7 +36,6 @@ public class UpdateChaptersDownloadedJob : Job
     
     protected override IEnumerable<Job> RunInternal(PgsqlContext context)
     {
-        context.Entry(Manga).Collection<Chapter>(m => m.Chapters).Load();
         return Manga.Chapters.Select(c => new UpdateSingleChapterDownloadedJob(c, this));
     }
 }
\ No newline at end of file
diff --git a/API/Schema/Manga.cs b/API/Schema/Manga.cs
index 2620193..d2daf14 100644
--- a/API/Schema/Manga.cs
+++ b/API/Schema/Manga.cs
@@ -4,6 +4,7 @@ using System.Runtime.InteropServices;
 using System.Text;
 using API.Schema.MangaConnectors;
 using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
 using Newtonsoft.Json;
 using static System.IO.UnixFileMode;
 
@@ -45,8 +46,16 @@ public class Manga
     [JsonIgnore]
     [NotMapped]
     public string? FullDirectoryPath => Library is not null ? Path.Join(Library.BasePath, DirectoryName) : null;
-    
-    [JsonIgnore] public ICollection<Chapter> Chapters { get; internal set; } = [];
+
+    [NotMapped] public ICollection<string> ChapterIds => Chapters.Select(c => c.ChapterId).ToList();
+    private readonly ILazyLoader _lazyLoader = null!;
+    private ICollection<Chapter> _chapters = null!;
+    [JsonIgnore] 
+    public ICollection<Chapter> Chapters 
+    {
+        get => _lazyLoader.Load(this, ref _chapters);
+        init => _chapters = value;
+    }
 
     public Manga(string idOnConnector, string name, string description, string websiteUrl, string coverUrl, MangaReleaseStatus releaseStatus,
         MangaConnector mangaConnector, ICollection<Author> authors, ICollection<MangaTag> mangaTags, ICollection<Link> links, ICollection<MangaAltTitle> altTitles,
@@ -71,14 +80,16 @@ public class Manga
         this.DirectoryName = CleanDirectoryName(name);
         this.Year = year;
         this.OriginalLanguage = originalLanguage;
+        this.Chapters = [];
     }
 
     /// <summary>
     /// EF ONLY!!!
     /// </summary>
-    public Manga(string mangaId, string idOnConnectorSite, string name, string description, string websiteUrl, string coverUrl, MangaReleaseStatus releaseStatus,
+    public Manga(ILazyLoader lazyLoader, string mangaId, string idOnConnectorSite, string name, string description, string websiteUrl, string coverUrl, MangaReleaseStatus releaseStatus,
         string mangaConnectorName, string directoryName, float ignoreChaptersBefore, string? libraryId, uint? year, string? originalLanguage)
     {
+        this._lazyLoader = lazyLoader;
         this.MangaId = mangaId;
         this.IdOnConnectorSite = idOnConnectorSite;
         this.Name = name;

From 937c5cb7a78669c5ca3ade4ea9416a12275b4164 Mon Sep 17 00:00:00 2001
From: Glax <johanna@bernloehr.eu>
Date: Fri, 16 May 2025 21:59:53 +0200
Subject: [PATCH 43/50] Create a UpdateChaptersDownloadedJob with creation of
 DownloadAvailableChaptersJob

---
 API/Controllers/JobController.cs | 6 ++++--
 1 file changed, 4 insertions(+), 2 deletions(-)

diff --git a/API/Controllers/JobController.cs b/API/Controllers/JobController.cs
index 5424239..4f3ead0 100644
--- a/API/Controllers/JobController.cs
+++ b/API/Controllers/JobController.cs
@@ -134,8 +134,10 @@ public class JobController(PgsqlContext context, ILog Log) : Controller
             }
         }
         Job retrieveChapters = new RetrieveChaptersJob(m, record.language, record.recurrenceTimeMs);
-        Job downloadChapters = new DownloadAvailableChaptersJob(m, record.recurrenceTimeMs, dependsOnJobs: [retrieveChapters]);
-        return AddJobs([retrieveChapters, downloadChapters]);
+        Job updateFilesDownloaded =
+            new UpdateChaptersDownloadedJob(m, record.recurrenceTimeMs, dependsOnJobs: [retrieveChapters]);
+        Job downloadChapters = new DownloadAvailableChaptersJob(m, record.recurrenceTimeMs, dependsOnJobs: [retrieveChapters, updateFilesDownloaded]);
+        return AddJobs([retrieveChapters, downloadChapters, updateFilesDownloaded]);
     }
 
     /// <summary>

From 3283dd72903f4e891bb0662407c0a13837664b64 Mon Sep 17 00:00:00 2001
From: Glax <johanna@bernloehr.eu>
Date: Fri, 16 May 2025 22:00:17 +0200
Subject: [PATCH 44/50] DownloadAvailableChaptersJob.cs only create
 DownloadSingleChapterJobs for Chapters that have not been downloaded

---
 API/Schema/Jobs/DownloadAvailableChaptersJob.cs | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/API/Schema/Jobs/DownloadAvailableChaptersJob.cs b/API/Schema/Jobs/DownloadAvailableChaptersJob.cs
index 961cb3a..e4d2eed 100644
--- a/API/Schema/Jobs/DownloadAvailableChaptersJob.cs
+++ b/API/Schema/Jobs/DownloadAvailableChaptersJob.cs
@@ -36,6 +36,6 @@ public class DownloadAvailableChaptersJob : Job
     
     protected override IEnumerable<Job> RunInternal(PgsqlContext context)
     {
-        return Manga.Chapters.Select(chapter => new DownloadSingleChapterJob(chapter, this));
+        return Manga.Chapters.Where(c => c.Downloaded == false).Select(chapter => new DownloadSingleChapterJob(chapter, this));
     }
 }
\ No newline at end of file

From aacdb72d6a6850d332315b2bd3318388f8b06209 Mon Sep 17 00:00:00 2001
From: Glax <johanna@bernloehr.eu>
Date: Sat, 17 May 2025 17:42:07 +0200
Subject: [PATCH 45/50] Remove LunaseaRecord

---
 API/APIEndpointRecords/LunaseaRecord.cs | 16 ----------------
 1 file changed, 16 deletions(-)
 delete mode 100644 API/APIEndpointRecords/LunaseaRecord.cs

diff --git a/API/APIEndpointRecords/LunaseaRecord.cs b/API/APIEndpointRecords/LunaseaRecord.cs
deleted file mode 100644
index 9bd1a53..0000000
--- a/API/APIEndpointRecords/LunaseaRecord.cs
+++ /dev/null
@@ -1,16 +0,0 @@
-using System.Text.RegularExpressions;
-
-namespace API.APIEndpointRecords;
-
-public record LunaseaRecord(string id)
-{
-    private static Regex validateRex = new(@"(?:device|user)\/[0-9a-zA-Z\-]+");
-    public bool Validate()
-    {
-        if (id == string.Empty)
-            return false;
-        if (!validateRex.IsMatch(id))
-            return false;
-        return true;
-    }
-}
\ No newline at end of file

From 0519ed26deae8155a2e23d3bb4522561d41c3525 Mon Sep 17 00:00:00 2001
From: Glax <johanna@bernloehr.eu>
Date: Sat, 17 May 2025 17:42:07 +0200
Subject: [PATCH 46/50] Remove Lunasea

---
 .../NotificationConnectorController.cs        | 26 -------------------
 1 file changed, 26 deletions(-)

diff --git a/API/Controllers/NotificationConnectorController.cs b/API/Controllers/NotificationConnectorController.cs
index a242e68..c0100e0 100644
--- a/API/Controllers/NotificationConnectorController.cs
+++ b/API/Controllers/NotificationConnectorController.cs
@@ -134,32 +134,6 @@ public class NotificationConnectorController(NotificationsContext context, ILog
         return CreateConnector(ntfyConnector);
     }
     
-    /// <summary>
-    /// Creates a new Lunasea-Notification-Connector
-    /// </summary>
-    /// <remarks>https://docs.lunasea.app/lunasea/notifications/custom-notifications for id. Either device/:device_id or user/:user_id</remarks>
-    /// <response code="201">ID of new connector</response>
-    /// <response code="400"></response>
-    /// <response code="409">A NotificationConnector with name already exists</response>
-    /// <response code="500">Error during Database Operation</response>
-    [HttpPut("Lunasea")]
-    [ProducesResponseType<string>(Status201Created, "application/json")]
-    [ProducesResponseType(Status400BadRequest)]
-    [ProducesResponseType(Status409Conflict)]
-    [ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
-    public IActionResult CreateLunaseaConnector([FromBody]LunaseaRecord lunaseaRecord)
-    {
-        if(!lunaseaRecord.Validate())
-            return BadRequest();
-        
-        NotificationConnector lunaseaConnector = new (TokenGen.CreateToken("Lunasea"),
-            $"https://notify.lunasea.app/v1/custom/{lunaseaRecord.id}", 
-            new Dictionary<string, string>(),
-            "POST", 
-            "{\"title\": \"%title\", \"body\": \"%text\"}");
-        return CreateConnector(lunaseaConnector);
-    }
-    
     /// <summary>
     /// Creates a new Pushover-Notification-Connector
     /// </summary>

From 6cfa29e3dd84b1b0036f74231a5e0ce628e5419c Mon Sep 17 00:00:00 2001
From: Glax <johanna@bernloehr.eu>
Date: Sat, 17 May 2025 18:17:51 +0200
Subject: [PATCH 47/50] Append Headers instead of Adding MangaController.cs

---
 API/Controllers/MangaController.cs | 10 ++++------
 1 file changed, 4 insertions(+), 6 deletions(-)

diff --git a/API/Controllers/MangaController.cs b/API/Controllers/MangaController.cs
index 3ec580c..90e5f23 100644
--- a/API/Controllers/MangaController.cs
+++ b/API/Controllers/MangaController.cs
@@ -109,9 +109,7 @@ public class MangaController(PgsqlContext context, ILog Log) : Controller
     [ProducesResponseType<int>(Status503ServiceUnavailable, "text/plain")]
     public IActionResult GetCover(string MangaId, [FromQuery]int? width, [FromQuery]int? height)
     {
-        DateTime requestStarted = HttpContext.Features.Get<IHttpRequestTimeFeature>()?.RequestTime ?? DateTime.Now;
-        Manga? m = context.Mangas.Find(MangaId);
-        if (m is null)
+        if(context.Mangas.Find(MangaId) is not { } m)
             return NotFound();
         
         if (!System.IO.File.Exists(m.CoverFileNameInCache))
@@ -119,7 +117,7 @@ public class MangaController(PgsqlContext context, ILog Log) : Controller
             List<Job> coverDownloadJobs = context.Jobs.Where(j => j.JobType == JobType.DownloadMangaCoverJob).ToList();
             if (coverDownloadJobs.Any(j => j is DownloadMangaCoverJob dmc && dmc.MangaId == MangaId))
             {
-                Response.Headers.Add("Retry-After", $"{TrangaSettings.startNewJobTimeoutMs * coverDownloadJobs.Count() * 2  / 1000:D}");
+                Response.Headers.Append("Retry-After", $"{TrangaSettings.startNewJobTimeoutMs * coverDownloadJobs.Count() * 2  / 1000:D}");
                 return StatusCode(Status503ServiceUnavailable, TrangaSettings.startNewJobTimeoutMs * coverDownloadJobs.Count() * 2  / 1000);
             }
             else
@@ -238,7 +236,7 @@ public class MangaController(PgsqlContext context, ILog Log) : Controller
             List<Job> retrieveChapterJobs = context.Jobs.Where(j => j.JobType == JobType.RetrieveChaptersJob).ToList();
             if (retrieveChapterJobs.Any(j => j is RetrieveChaptersJob rcj && rcj.MangaId == MangaId))
             {
-                Response.Headers.Add("Retry-After", $"{TrangaSettings.startNewJobTimeoutMs * retrieveChapterJobs.Count() * 2 / 1000:D}");
+                Response.Headers.Append("Retry-After", $"{TrangaSettings.startNewJobTimeoutMs * retrieveChapterJobs.Count() * 2 / 1000:D}");
                 return StatusCode(Status503ServiceUnavailable, TrangaSettings.startNewJobTimeoutMs * retrieveChapterJobs.Count() * 2/ 1000);
             }else
                 return NoContent();
@@ -279,7 +277,7 @@ public class MangaController(PgsqlContext context, ILog Log) : Controller
             List<Job> retrieveChapterJobs = context.Jobs.Where(j => j.JobType == JobType.RetrieveChaptersJob).ToList();
             if (retrieveChapterJobs.Any(j => j is RetrieveChaptersJob rcj && rcj.MangaId == MangaId))
             {
-                Response.Headers.Add("Retry-After", $"{TrangaSettings.startNewJobTimeoutMs * retrieveChapterJobs.Count() * 2  / 1000:D}");
+                Response.Headers.Append("Retry-After", $"{TrangaSettings.startNewJobTimeoutMs * retrieveChapterJobs.Count() * 2  / 1000:D}");
                 return StatusCode(Status503ServiceUnavailable, TrangaSettings.startNewJobTimeoutMs * retrieveChapterJobs.Count() * 2  / 1000);
             }else
                 return NoContent();

From 0903ec606b7ceee180383a9136458a95b21ffe4f Mon Sep 17 00:00:00 2001
From: Glax <johanna@bernloehr.eu>
Date: Sat, 17 May 2025 18:54:24 +0200
Subject: [PATCH 48/50] MangaController.cs use Manga.Chapters for Navigation
 instead of new context-Request

---
 API/Controllers/MangaController.cs | 28 +++++++++++-----------------
 1 file changed, 11 insertions(+), 17 deletions(-)

diff --git a/API/Controllers/MangaController.cs b/API/Controllers/MangaController.cs
index 90e5f23..d0cfa4c 100644
--- a/API/Controllers/MangaController.cs
+++ b/API/Controllers/MangaController.cs
@@ -153,12 +153,11 @@ public class MangaController(PgsqlContext context, ILog Log) : Controller
     [ProducesResponseType(Status404NotFound)]
     public IActionResult GetChapters(string MangaId)
     {
-        Manga? m = context.Mangas.Find(MangaId);
-        if (m is null)
+        if(context.Mangas.Find(MangaId) is not { } m)
             return NotFound();
         
-        Chapter[] ret = context.Chapters.Where(c => c.ParentMangaId == m.MangaId).ToArray();
-        return Ok(ret);
+        Chapter[] chapters = m.Chapters.ToArray();
+        return Ok(chapters);
     }
     
     /// <summary>
@@ -174,11 +173,10 @@ public class MangaController(PgsqlContext context, ILog Log) : Controller
     [ProducesResponseType(Status404NotFound)]
     public IActionResult GetChaptersDownloaded(string MangaId)
     {
-        Manga? m = context.Mangas.Find(MangaId);
-        if (m is null)
+        if(context.Mangas.Find(MangaId) is not { } m)
             return NotFound();
         
-        List<Chapter> chapters = context.Chapters.Where(c => c.ParentMangaId == m.MangaId && c.Downloaded == true).ToList();
+        List<Chapter> chapters = m.Chapters.ToList();
         if (chapters.Count == 0)
             return NoContent();
         
@@ -198,11 +196,10 @@ public class MangaController(PgsqlContext context, ILog Log) : Controller
     [ProducesResponseType(Status404NotFound)]
     public IActionResult GetChaptersNotDownloaded(string MangaId)
     {
-        Manga? m = context.Mangas.Find(MangaId);
-        if (m is null)
+        if(context.Mangas.Find(MangaId) is not { } m)
             return NotFound();
         
-        List<Chapter> chapters = context.Chapters.Where(c => c.ParentMangaId == m.MangaId && c.Downloaded == false).ToList();
+        List<Chapter> chapters = m.Chapters.ToList();
         if (chapters.Count == 0)
             return NoContent();
         
@@ -226,11 +223,10 @@ public class MangaController(PgsqlContext context, ILog Log) : Controller
     [ProducesResponseType<int>(Status503ServiceUnavailable, "text/plain")]
     public IActionResult GetLatestChapter(string MangaId)
     {
-        Manga? m = context.Mangas.Find(MangaId);
-        if (m is null)
+        if(context.Mangas.Find(MangaId) is not { } m)
             return NotFound();
         
-        List<Chapter> chapters = context.Chapters.Where(c => c.ParentMangaId == m.MangaId).ToList();
+        List<Chapter> chapters = m.Chapters.ToList();
         if (chapters.Count == 0)
         {
             List<Job> retrieveChapterJobs = context.Jobs.Where(j => j.JobType == JobType.RetrieveChaptersJob).ToList();
@@ -266,12 +262,10 @@ public class MangaController(PgsqlContext context, ILog Log) : Controller
     [ProducesResponseType<int>(Status503ServiceUnavailable, "text/plain")]
     public IActionResult GetLatestChapterDownloaded(string MangaId)
     {
-        Manga? m = context.Mangas.Find(MangaId);
-        if (m is null)
+        if(context.Mangas.Find(MangaId) is not { } m)
             return NotFound();
         
-        
-        List<Chapter> chapters = context.Chapters.Where(c => c.ParentMangaId == m.MangaId && c.Downloaded == true).ToList();
+        List<Chapter> chapters = m.Chapters.ToList();
         if (chapters.Count == 0)
         {
             List<Job> retrieveChapterJobs = context.Jobs.Where(j => j.JobType == JobType.RetrieveChaptersJob).ToList();

From b3efcf19d9bc3ed9730625686f72dcf3a052d704 Mon Sep 17 00:00:00 2001
From: Glax <johanna@bernloehr.eu>
Date: Sat, 17 May 2025 19:07:01 +0200
Subject: [PATCH 49/50] Manga GetCover, GetLatestDownloaded,
 GetLatestAvailable: Check if Jobs are running to fulfill request

---
 API/Controllers/MangaController.cs | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/API/Controllers/MangaController.cs b/API/Controllers/MangaController.cs
index d0cfa4c..eea2020 100644
--- a/API/Controllers/MangaController.cs
+++ b/API/Controllers/MangaController.cs
@@ -115,7 +115,7 @@ public class MangaController(PgsqlContext context, ILog Log) : Controller
         if (!System.IO.File.Exists(m.CoverFileNameInCache))
         {
             List<Job> coverDownloadJobs = context.Jobs.Where(j => j.JobType == JobType.DownloadMangaCoverJob).ToList();
-            if (coverDownloadJobs.Any(j => j is DownloadMangaCoverJob dmc && dmc.MangaId == MangaId))
+            if (coverDownloadJobs.Any(j => j is DownloadMangaCoverJob dmc && dmc.MangaId == MangaId && dmc.state < JobState.Completed))
             {
                 Response.Headers.Append("Retry-After", $"{TrangaSettings.startNewJobTimeoutMs * coverDownloadJobs.Count() * 2  / 1000:D}");
                 return StatusCode(Status503ServiceUnavailable, TrangaSettings.startNewJobTimeoutMs * coverDownloadJobs.Count() * 2  / 1000);
@@ -230,12 +230,12 @@ public class MangaController(PgsqlContext context, ILog Log) : Controller
         if (chapters.Count == 0)
         {
             List<Job> retrieveChapterJobs = context.Jobs.Where(j => j.JobType == JobType.RetrieveChaptersJob).ToList();
-            if (retrieveChapterJobs.Any(j => j is RetrieveChaptersJob rcj && rcj.MangaId == MangaId))
+            if (retrieveChapterJobs.Any(j => j is RetrieveChaptersJob rcj && rcj.MangaId == MangaId && rcj.state < JobState.Completed))
             {
                 Response.Headers.Append("Retry-After", $"{TrangaSettings.startNewJobTimeoutMs * retrieveChapterJobs.Count() * 2 / 1000:D}");
                 return StatusCode(Status503ServiceUnavailable, TrangaSettings.startNewJobTimeoutMs * retrieveChapterJobs.Count() * 2/ 1000);
             }else
-                return NoContent();
+                return Ok(0);
         }
         
         Chapter? max = chapters.Max();
@@ -269,7 +269,7 @@ public class MangaController(PgsqlContext context, ILog Log) : Controller
         if (chapters.Count == 0)
         {
             List<Job> retrieveChapterJobs = context.Jobs.Where(j => j.JobType == JobType.RetrieveChaptersJob).ToList();
-            if (retrieveChapterJobs.Any(j => j is RetrieveChaptersJob rcj && rcj.MangaId == MangaId))
+            if (retrieveChapterJobs.Any(j => j is RetrieveChaptersJob rcj && rcj.MangaId == MangaId && rcj.state < JobState.Completed))
             {
                 Response.Headers.Append("Retry-After", $"{TrangaSettings.startNewJobTimeoutMs * retrieveChapterJobs.Count() * 2  / 1000:D}");
                 return StatusCode(Status503ServiceUnavailable, TrangaSettings.startNewJobTimeoutMs * retrieveChapterJobs.Count() * 2  / 1000);

From 496b49ccb363342ed4a63fef3f56a59336c57458 Mon Sep 17 00:00:00 2001
From: Glax <johanna@bernloehr.eu>
Date: Sat, 17 May 2025 19:24:24 +0200
Subject: [PATCH 50/50] ParentJob can be updated, DownloadAvailableJobs
 Endpoint automatically sets ParentJob to the DownloadAvailableChaptersJob
 (ondeleteCascade)

---
 API/Controllers/JobController.cs | 2 ++
 API/Schema/Jobs/Job.cs           | 4 ++--
 2 files changed, 4 insertions(+), 2 deletions(-)

diff --git a/API/Controllers/JobController.cs b/API/Controllers/JobController.cs
index 4f3ead0..c68f1c0 100644
--- a/API/Controllers/JobController.cs
+++ b/API/Controllers/JobController.cs
@@ -137,6 +137,8 @@ public class JobController(PgsqlContext context, ILog Log) : Controller
         Job updateFilesDownloaded =
             new UpdateChaptersDownloadedJob(m, record.recurrenceTimeMs, dependsOnJobs: [retrieveChapters]);
         Job downloadChapters = new DownloadAvailableChaptersJob(m, record.recurrenceTimeMs, dependsOnJobs: [retrieveChapters, updateFilesDownloaded]);
+        retrieveChapters.ParentJob = downloadChapters;
+        updateFilesDownloaded.ParentJob = retrieveChapters;
         return AddJobs([retrieveChapters, downloadChapters, updateFilesDownloaded]);
     }
 
diff --git a/API/Schema/Jobs/Job.cs b/API/Schema/Jobs/Job.cs
index ffcb227..ddb2774 100644
--- a/API/Schema/Jobs/Job.cs
+++ b/API/Schema/Jobs/Job.cs
@@ -15,8 +15,8 @@ public abstract class Job
     [Required]
     public string JobId { get; init; }
 
-    [StringLength(64)] public string? ParentJobId { get; init; }
-    [JsonIgnore] public Job? ParentJob { get; init; }
+    [StringLength(64)] public string? ParentJobId { get; private set; }
+    [JsonIgnore] public Job? ParentJob { get; internal set; }
     private ICollection<Job> _dependsOnJobs = null!;
     [JsonIgnore] public ICollection<Job> DependsOnJobs
     {