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("&", "&"); - - 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("&", "&")).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 {