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 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive + 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/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 diff --git a/API/Controllers/JobController.cs b/API/Controllers/JobController.cs index 1f4309a..c68f1c0 100644 --- a/API/Controllers/JobController.cs +++ b/API/Controllers/JobController.cs @@ -1,16 +1,19 @@ using API.APIEndpointRecords; using API.Schema; +using API.Schema.Contexts; 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 { /// /// Returns all Jobs @@ -102,7 +105,7 @@ public class JobController(PgsqlContext context) : Controller /// ID of Manga /// Job-Configuration /// Job-IDs - /// Could not find Library with ID + /// Could not find ToLibrary with ID /// Could not find Manga with ID /// Error during Database Operation [HttpPut("DownloadAvailableChaptersJob/{MangaId}")] @@ -110,7 +113,7 @@ public class JobController(PgsqlContext context) : Controller [ProducesResponseType(Status400BadRequest)] [ProducesResponseType(Status404NotFound)] [ProducesResponseType(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 +129,17 @@ 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 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]); } /// @@ -148,14 +155,14 @@ public class JobController(PgsqlContext context) : Controller [ProducesResponseType(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]); } /// - /// Create a new UpdateFilesDownloadedJob + /// Create a new UpdateChaptersDownloadedJob /// /// ID of the Manga /// Job-IDs @@ -167,9 +174,9 @@ public class JobController(PgsqlContext context) : Controller [ProducesResponseType(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 UpdateChaptersDownloadedJob(m, 0); return AddJobs([job]); } @@ -183,8 +190,7 @@ public class JobController(PgsqlContext context) : Controller [ProducesResponseType(Status500InternalServerError, "text/plain")] public IActionResult CreateUpdateAllFilesDownloadedJob() { - List ids = context.Mangas.Select(m => m.MangaId).ToList(); - List jobs = ids.Select(id => new UpdateFilesDownloadedJob(0, id)).ToList(); + List jobs = context.Mangas.Select(m => new UpdateChaptersDownloadedJob(m, 0, null, null)).ToList(); try { context.Jobs.AddRange(jobs); @@ -193,12 +199,13 @@ public class JobController(PgsqlContext context) : Controller } catch (Exception e) { + Log.Error(e); return StatusCode(500, e.Message); } } /// - /// Create a new UpdateMetadataJob + /// Not Implemented: Create a new UpdateMetadataJob /// /// ID of the Manga /// Job-IDs @@ -210,14 +217,11 @@ public class JobController(PgsqlContext context) : Controller [ProducesResponseType(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); } /// - /// Create a new UpdateMetadataJob for all Manga + /// Not Implemented: Create a new UpdateMetadataJob for all Manga /// /// Job-IDs /// Error during Database Operation @@ -226,18 +230,7 @@ public class JobController(PgsqlContext context) : Controller [ProducesResponseType(Status500InternalServerError, "text/plain")] public IActionResult CreateUpdateAllMetadataJob() { - List ids = context.Mangas.Select(m => m.MangaId).ToList(); - List 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 +243,7 @@ public class JobController(PgsqlContext context) : Controller } catch (Exception e) { + Log.Error(e); return StatusCode(500, e.Message); } } @@ -269,8 +263,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 +272,7 @@ public class JobController(PgsqlContext context) : Controller } catch (Exception e) { + Log.Error(e); return StatusCode(500, e.Message); } } @@ -322,6 +316,7 @@ public class JobController(PgsqlContext context) : Controller } catch (Exception e) { + Log.Error(e); return StatusCode(500, e.Message); } } @@ -354,6 +349,7 @@ public class JobController(PgsqlContext context) : Controller } catch (Exception e) { + Log.Error(e); return StatusCode(500, e.Message); } } @@ -367,6 +363,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..0db5748 100644 --- a/API/Controllers/LibraryConnectorController.cs +++ b/API/Controllers/LibraryConnectorController.cs @@ -1,6 +1,8 @@ using API.Schema; +using API.Schema.Contexts; using API.Schema.LibraryConnectors; using Asp.Versioning; +using log4net; using Microsoft.AspNetCore.Mvc; using static Microsoft.AspNetCore.Http.StatusCodes; @@ -9,10 +11,10 @@ namespace API.Controllers; [ApiVersion(2)] [ApiController] [Route("v{v:apiVersion}/[controller]")] -public class LibraryConnectorController(PgsqlContext context) : Controller +public class LibraryConnectorController(LibraryContext context, ILog Log) : Controller { /// - /// Gets all configured Library-Connectors + /// Gets all configured ToLibrary-Connectors /// /// [HttpGet] @@ -24,9 +26,9 @@ public class LibraryConnectorController(PgsqlContext context) : Controller } /// - /// Returns Library-Connector with requested ID + /// Returns ToLibrary-Connector with requested ID /// - /// Library-Connector-ID + /// ToLibrary-Connector-ID /// /// Connector with ID not found. [HttpGet("{LibraryControllerId}")] @@ -43,9 +45,9 @@ public class LibraryConnectorController(PgsqlContext context) : Controller } /// - /// Creates a new Library-Connector + /// Creates a new ToLibrary-Connector /// - /// Library-Connector + /// ToLibrary-Connector /// /// Error during Database Operation [HttpPut] @@ -61,14 +63,15 @@ public class LibraryConnectorController(PgsqlContext context) : Controller } catch (Exception e) { + Log.Error(e); return StatusCode(500, e.Message); } } /// - /// Deletes the Library-Connector with the requested ID + /// Deletes the ToLibrary-Connector with the requested ID /// - /// Library-Connector-ID + /// ToLibrary-Connector-ID /// /// Connector with ID not found. /// Error during Database Operation @@ -90,6 +93,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..9f09118 100644 --- a/API/Controllers/LocalLibrariesController.cs +++ b/API/Controllers/LocalLibrariesController.cs @@ -1,6 +1,8 @@ using API.APIEndpointRecords; using API.Schema; +using API.Schema.Contexts; using Asp.Versioning; +using log4net; using Microsoft.AspNetCore.Mvc; using static Microsoft.AspNetCore.Http.StatusCodes; @@ -9,7 +11,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(Status200OK, "application/json")] @@ -52,6 +54,7 @@ public class LocalLibrariesController(PgsqlContext context) : Controller } catch (Exception e) { + Log.Error(e); return StatusCode(500, e.Message); } } @@ -79,6 +82,7 @@ public class LocalLibrariesController(PgsqlContext context) : Controller } catch (Exception e) { + Log.Error(e); return StatusCode(500, e.Message); } } @@ -106,6 +110,7 @@ public class LocalLibrariesController(PgsqlContext context) : Controller } catch (Exception e) { + Log.Error(e); return StatusCode(500, e.Message); } } @@ -128,6 +133,7 @@ public class LocalLibrariesController(PgsqlContext context) : Controller } catch (Exception e) { + Log.Error(e); return StatusCode(500, e.Message); } } @@ -151,6 +157,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..095ee59 100644 --- a/API/Controllers/MangaConnectorController.cs +++ b/API/Controllers/MangaConnectorController.cs @@ -1,6 +1,7 @@ -using API.Schema; +using API.Schema.Contexts; 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 { /// /// 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..eea2020 100644 --- a/API/Controllers/MangaController.cs +++ b/API/Controllers/MangaController.cs @@ -1,19 +1,22 @@ using API.Schema; +using API.Schema.Contexts; 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 { /// /// Returns all cached Manga @@ -82,6 +85,7 @@ public class MangaController(PgsqlContext context) : Controller } catch (Exception e) { + Log.Error(e); return StatusCode(500, e.Message); } } @@ -105,17 +109,15 @@ public class MangaController(PgsqlContext context) : Controller [ProducesResponseType(Status503ServiceUnavailable, "text/plain")] public IActionResult GetCover(string MangaId, [FromQuery]int? width, [FromQuery]int? height) { - DateTime requestStarted = HttpContext.Features.Get()?.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)) { List 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.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 @@ -151,12 +153,11 @@ public class MangaController(PgsqlContext context) : 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); } /// @@ -172,11 +173,10 @@ public class MangaController(PgsqlContext context) : 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 chapters = context.Chapters.Where(c => c.ParentMangaId == m.MangaId && c.Downloaded == true).ToList(); + List chapters = m.Chapters.ToList(); if (chapters.Count == 0) return NoContent(); @@ -196,11 +196,10 @@ public class MangaController(PgsqlContext context) : 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 chapters = context.Chapters.Where(c => c.ParentMangaId == m.MangaId && c.Downloaded == false).ToList(); + List chapters = m.Chapters.ToList(); if (chapters.Count == 0) return NoContent(); @@ -224,20 +223,19 @@ public class MangaController(PgsqlContext context) : Controller [ProducesResponseType(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 chapters = context.Chapters.Where(c => c.ParentMangaId == m.MangaId).ToList(); + List chapters = m.Chapters.ToList(); if (chapters.Count == 0) { List 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.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(); + return Ok(0); } Chapter? max = chapters.Max(); @@ -264,18 +262,16 @@ public class MangaController(PgsqlContext context) : Controller [ProducesResponseType(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 chapters = context.Chapters.Where(c => c.ParentMangaId == m.MangaId && c.Downloaded == true).ToList(); + List chapters = m.Chapters.ToList(); if (chapters.Count == 0) { List 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.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(); @@ -287,11 +283,12 @@ public class MangaController(PgsqlContext context) : Controller return Ok(max); } - + /// /// Configure the cut-off for Manga /// /// Manga-ID + /// Threshold (Chapter Number) /// /// Manga with ID not found. /// Error during Database Operation @@ -307,21 +304,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); } } /// - /// Move Manga to different Library + /// Move Manga to different ToLibrary /// /// Manga-ID - /// Library-Id + /// ToLibrary-Id /// Folder is going to be moved /// MangaId or LibraryId not found /// Error during Database Operation @@ -331,24 +329,23 @@ public class MangaController(PgsqlContext context) : Controller [ProducesResponseType(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); + UpdateChaptersDownloadedJob 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..c0100e0 100644 --- a/API/Controllers/NotificationConnectorController.cs +++ b/API/Controllers/NotificationConnectorController.cs @@ -1,8 +1,9 @@ using System.Text; using API.APIEndpointRecords; -using API.Schema; +using API.Schema.Contexts; 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(NotificationsContext context, ILog Log) : Controller { /// /// 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); } } @@ -132,32 +134,6 @@ public class NotificationConnectorController(PgsqlContext context) : Controller return CreateConnector(ntfyConnector); } - /// - /// Creates a new Lunasea-Notification-Connector - /// - /// https://docs.lunasea.app/lunasea/notifications/custom-notifications for id. Either device/:device_id or user/:user_id - /// ID of new connector - /// - /// A NotificationConnector with name already exists - /// Error during Database Operation - [HttpPut("Lunasea")] - [ProducesResponseType(Status201Created, "application/json")] - [ProducesResponseType(Status400BadRequest)] - [ProducesResponseType(Status409Conflict)] - [ProducesResponseType(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(), - "POST", - "{\"title\": \"%title\", \"body\": \"%text\"}"); - return CreateConnector(lunaseaConnector); - } - /// /// Creates a new Pushover-Notification-Connector /// @@ -209,6 +185,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..f4d603f 100644 --- a/API/Controllers/QueryController.cs +++ b/API/Controllers/QueryController.cs @@ -1,14 +1,17 @@ using API.Schema; +using API.Schema.Contexts; 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 { /// /// Returns the Author-Information for Author-ID @@ -32,13 +35,16 @@ public class QueryController(PgsqlContext context) : Controller /// /// Author-ID /// + /// Author not found [HttpGet("Mangas/WithAuthorId/{AuthorId}")] [ProducesResponseType(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))); } - + /* /// /// Returns Link-Information for Link-Id /// @@ -71,18 +77,21 @@ public class QueryController(PgsqlContext context) : Controller if (ret is null) return NotFound(); return Ok(ret); - } + }*/ /// /// Returns all Manga with Tag /// /// /// + /// Tag not found [HttpGet("Mangas/WithTag/{Tag}")] [ProducesResponseType(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))); } /// diff --git a/API/Controllers/SearchController.cs b/API/Controllers/SearchController.cs index cc30c6c..fbef959 100644 --- a/API/Controllers/SearchController.cs +++ b/API/Controllers/SearchController.cs @@ -1,86 +1,55 @@ using API.Schema; +using API.Schema.Contexts; 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 { - - /// - /// Initiate a search for a Manga on all Connectors - /// - /// Name/Title of the Manga - /// - /// Error during Database Operation - [HttpPost("Name")] - [ProducesResponseType(Status200OK, "application/json")] - [ProducesResponseType(Status500InternalServerError, "text/plain")] - public IActionResult SearchMangaGlobal([FromBody]string name) - { - List<(Manga, List?, List?, List?, List?)> allManga = new(); - foreach (MangaConnector contextMangaConnector in context.MangaConnectors.Where(connector => connector.Enabled)) - allManga.AddRange(contextMangaConnector.GetManga(name)); - - List retMangas = new(); - foreach ((Manga? manga, List? authors, List? tags, List? links, List? 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()); - } - /// /// Initiate a search for a Manga on a specific Connector /// - /// Manga-Connector-ID - /// Name/Title of the Manga + /// + /// /// /// MangaConnector with ID not found /// MangaConnector with ID is disabled /// Error during Database Operation - [HttpPost("{MangaConnectorName}")] + [HttpGet("{MangaConnectorName}/{Query}")] [ProducesResponseType(Status200OK, "application/json")] [ProducesResponseType(Status404NotFound)] [ProducesResponseType(Status406NotAcceptable)] [ProducesResponseType(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?, List?, List?, List?)[] mangas = connector.GetManga(name); + Manga[] mangas = connector.SearchManga(Query); List retMangas = new(); - foreach ((Manga? manga, List? authors, List? tags, List? links, List? 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); } } @@ -98,104 +67,66 @@ public class SearchController(PgsqlContext context) : Controller /// Error during Database Operation [HttpPost("Url")] [ProducesResponseType(Status200OK, "application/json")] - [ProducesResponseType(Status300MultipleChoices)] [ProducesResponseType(Status400BadRequest)] - [ProducesResponseType(Status404NotFound)] [ProducesResponseType(Status500InternalServerError, "text/plain")] public IActionResult GetMangaFromUrl([FromBody]string url) { - List connectors = context.MangaConnectors.AsEnumerable().Where(c => c.ValidateUrl(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."); - (Manga manga, List? authors, List? tags, List? links, List? altTitles)? x = connectors.First().GetMangaFromUrl(url); - if (x is null) + if(connector.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); + return StatusCode(Status500InternalServerError); } catch (DbUpdateException e) { - return StatusCode(500, e.Message); + Log.Error(e); + return StatusCode(Status500InternalServerError, e.Message); } } - private Manga? AddMangaToContext(Manga? manga, List? authors, List? tags, List? links, - List? altTitles) + private Manga? AddMangaToContext(Manga manga) { - if (manga is null) - return null; - - Manga? existing = context.Mangas.Find(manga.MangaId); + context.Mangas.Load(); + context.Authors.Load(); + context.Tags.Load(); + context.MangaConnectors.Load(); - if (tags is not null) + IEnumerable mergedTags = manga.MangaTags.Select(mt => { - IEnumerable mergedTags = tags.Select(mt => - { - MangaTag? inDb = context.Tags.Find(mt.Tag); - return inDb ?? mt; - }); - manga.MangaTags = mergedTags.ToList(); - IEnumerable 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 mergedAuthors = manga.Authors.Select(ma => { - IEnumerable mergedAuthors = authors.Select(ma => - { - Author? inDb = context.Authors.Find(ma.AuthorId); - return inDb ?? ma; - }); - manga.Authors = mergedAuthors.ToList(); - IEnumerable 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) + try { - IEnumerable mergedLinks = links.Select(ml => - { - Link? inDb = context.Links.Find(ml.LinkId); - return inDb ?? ml; - }); - manga.Links = mergedLinks.ToList(); - IEnumerable newLinks = manga.Links - .Where(ml => !context.Links.Select(l => l.LinkId).Contains(ml.LinkId)); - context.Links.AddRange(newLinks); - } - if (altTitles is not null) - { - IEnumerable mergedAltTitles = altTitles.Select(mat => + if (context.Mangas.Find(manga.MangaId) is { } r) { - MangaAltTitle? inDb = context.AltTitles.Find(mat.AltTitleId); - return inDb ?? mat; - }); - manga.AltTitles = mergedAltTitles.ToList(); - IEnumerable 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..f6cbd05 100644 --- a/API/Controllers/SettingsController.cs +++ b/API/Controllers/SettingsController.cs @@ -1,7 +1,9 @@ using API.MangaDownloadClients; using API.Schema; +using API.Schema.Contexts; 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 +13,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 { /// /// Get all Settings @@ -252,14 +254,16 @@ public class SettingsController(PgsqlContext context) : Controller { try { + Dictionary 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/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; } 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 diff --git a/API/Migrations/20250316143014_dev-160325-Initial.Designer.cs b/API/Migrations/20250316143014_dev-160325-Initial.Designer.cs deleted file mode 100644 index 24a7d03..0000000 --- a/API/Migrations/20250316143014_dev-160325-Initial.Designer.cs +++ /dev/null @@ -1,821 +0,0 @@ -// -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("20250316143014_dev-160325-Initial")] - partial class dev160325Initial - { - /// - 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("AuthorId") - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("AuthorName") - .IsRequired() - .HasMaxLength(128) - .HasColumnType("character varying(128)"); - - b.HasKey("AuthorId"); - - b.ToTable("Authors"); - }); - - modelBuilder.Entity("API.Schema.Chapter", b => - { - b.Property("ChapterId") - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("ChapterNumber") - .IsRequired() - .HasMaxLength(10) - .HasColumnType("character varying(10)"); - - b.Property("Downloaded") - .HasColumnType("boolean"); - - b.Property("FileName") - .IsRequired() - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.Property("ParentMangaId") - .IsRequired() - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("Title") - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.Property("Url") - .IsRequired() - .HasMaxLength(2048) - .HasColumnType("character varying(2048)"); - - b.Property("VolumeNumber") - .HasColumnType("integer"); - - b.HasKey("ChapterId"); - - b.HasIndex("ParentMangaId"); - - b.ToTable("Chapters"); - }); - - modelBuilder.Entity("API.Schema.Jobs.Job", b => - { - b.Property("JobId") - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.PrimitiveCollection("DependsOnJobsIds") - .HasMaxLength(64) - .HasColumnType("text[]"); - - b.Property("Enabled") - .HasColumnType("boolean"); - - b.Property("JobType") - .HasColumnType("smallint"); - - b.Property("LastExecution") - .HasColumnType("timestamp with time zone"); - - b.Property("ParentJobId") - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("RecurrenceMs") - .HasColumnType("numeric(20,0)"); - - b.Property("state") - .HasColumnType("smallint"); - - b.HasKey("JobId"); - - b.HasIndex("ParentJobId"); - - b.ToTable("Jobs"); - - b.HasDiscriminator("JobType"); - - b.UseTphMappingStrategy(); - }); - - modelBuilder.Entity("API.Schema.LibraryConnectors.LibraryConnector", b => - { - b.Property("LibraryConnectorId") - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("Auth") - .IsRequired() - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.Property("BaseUrl") - .IsRequired() - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.Property("LibraryType") - .HasColumnType("smallint"); - - b.HasKey("LibraryConnectorId"); - - b.ToTable("LibraryConnectors"); - - b.HasDiscriminator("LibraryType"); - - b.UseTphMappingStrategy(); - }); - - modelBuilder.Entity("API.Schema.Link", b => - { - b.Property("LinkId") - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("LinkProvider") - .IsRequired() - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("LinkUrl") - .IsRequired() - .HasMaxLength(2048) - .HasColumnType("character varying(2048)"); - - b.Property("MangaId") - .HasColumnType("character varying(64)"); - - b.HasKey("LinkId"); - - b.HasIndex("MangaId"); - - b.ToTable("Links"); - }); - - modelBuilder.Entity("API.Schema.LocalLibrary", b => - { - b.Property("LocalLibraryId") - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("BasePath") - .IsRequired() - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.Property("LibraryName") - .IsRequired() - .HasMaxLength(512) - .HasColumnType("character varying(512)"); - - b.HasKey("LocalLibraryId"); - - b.ToTable("LocalLibraries"); - }); - - modelBuilder.Entity("API.Schema.Manga", b => - { - b.Property("MangaId") - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("CoverFileNameInCache") - .HasColumnType("text"); - - b.Property("CoverUrl") - .IsRequired() - .HasColumnType("text"); - - b.Property("Description") - .IsRequired() - .HasColumnType("text"); - - b.Property("DirectoryName") - .IsRequired() - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.Property("IdOnConnectorSite") - .IsRequired() - .HasMaxLength(128) - .HasColumnType("character varying(128)"); - - b.Property("IgnoreChapterBefore") - .HasColumnType("real"); - - b.Property("LibraryLocalLibraryId") - .HasColumnType("character varying(64)"); - - b.Property("MangaConnectorId") - .IsRequired() - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.Property("OriginalLanguage") - .IsRequired() - .HasMaxLength(8) - .HasColumnType("character varying(8)"); - - b.Property("ReleaseStatus") - .HasColumnType("smallint"); - - b.Property("WebsiteUrl") - .IsRequired() - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.Property("Year") - .HasColumnType("bigint"); - - b.HasKey("MangaId"); - - b.HasIndex("LibraryLocalLibraryId"); - - b.HasIndex("MangaConnectorId"); - - b.ToTable("Mangas"); - }); - - modelBuilder.Entity("API.Schema.MangaAltTitle", b => - { - b.Property("AltTitleId") - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("Language") - .IsRequired() - .HasMaxLength(8) - .HasColumnType("character varying(8)"); - - b.Property("MangaId") - .HasColumnType("character varying(64)"); - - b.Property("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("Name") - .HasMaxLength(32) - .HasColumnType("character varying(32)"); - - b.PrimitiveCollection("BaseUris") - .IsRequired() - .HasMaxLength(256) - .HasColumnType("text[]"); - - b.Property("Enabled") - .HasColumnType("boolean"); - - b.Property("IconUrl") - .IsRequired() - .HasMaxLength(2048) - .HasColumnType("character varying(2048)"); - - b.PrimitiveCollection("SupportedLanguages") - .IsRequired() - .HasMaxLength(8) - .HasColumnType("text[]"); - - b.HasKey("Name"); - - b.ToTable("MangaConnectors"); - - b.HasDiscriminator("Name").HasValue("MangaConnector"); - - b.UseTphMappingStrategy(); - }); - - modelBuilder.Entity("API.Schema.MangaTag", b => - { - b.Property("Tag") - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.HasKey("Tag"); - - b.ToTable("Tags"); - }); - - modelBuilder.Entity("API.Schema.Notification", b => - { - b.Property("NotificationId") - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("Date") - .HasColumnType("timestamp with time zone"); - - b.Property("Message") - .IsRequired() - .HasMaxLength(512) - .HasColumnType("character varying(512)"); - - b.Property("Title") - .IsRequired() - .HasMaxLength(128) - .HasColumnType("character varying(128)"); - - b.Property("Urgency") - .HasColumnType("smallint"); - - b.HasKey("NotificationId"); - - b.ToTable("Notifications"); - }); - - modelBuilder.Entity("API.Schema.NotificationConnectors.NotificationConnector", b => - { - b.Property("Name") - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("Body") - .IsRequired() - .HasMaxLength(4096) - .HasColumnType("character varying(4096)"); - - b.Property>("Headers") - .IsRequired() - .HasColumnType("hstore"); - - b.Property("HttpMethod") - .IsRequired() - .HasMaxLength(8) - .HasColumnType("character varying(8)"); - - b.Property("Url") - .IsRequired() - .HasMaxLength(2048) - .HasColumnType("character varying(2048)"); - - b.HasKey("Name"); - - b.ToTable("NotificationConnectors"); - }); - - modelBuilder.Entity("AuthorManga", b => - { - b.Property("AuthorsAuthorId") - .HasColumnType("character varying(64)"); - - b.Property("MangaId") - .HasColumnType("character varying(64)"); - - b.HasKey("AuthorsAuthorId", "MangaId"); - - b.HasIndex("MangaId"); - - b.ToTable("AuthorManga"); - }); - - modelBuilder.Entity("JobJob", b => - { - b.Property("DependsOnJobsJobId") - .HasColumnType("character varying(64)"); - - b.Property("JobId") - .HasColumnType("character varying(64)"); - - b.HasKey("DependsOnJobsJobId", "JobId"); - - b.HasIndex("JobId"); - - b.ToTable("JobJob"); - }); - - modelBuilder.Entity("MangaMangaTag", b => - { - b.Property("MangaId") - .HasColumnType("character varying(64)"); - - b.Property("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("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("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("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("FromLocation") - .IsRequired() - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.Property("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("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("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("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.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.Cascade); - - 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/20250316150158_dev-160325-2.Designer.cs b/API/Migrations/20250316150158_dev-160325-2.Designer.cs deleted file mode 100644 index e2bd7a1..0000000 --- a/API/Migrations/20250316150158_dev-160325-2.Designer.cs +++ /dev/null @@ -1,821 +0,0 @@ -// -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("20250316150158_dev-160325-2")] - partial class dev1603252 - { - /// - 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("AuthorId") - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("AuthorName") - .IsRequired() - .HasMaxLength(128) - .HasColumnType("character varying(128)"); - - b.HasKey("AuthorId"); - - b.ToTable("Authors"); - }); - - modelBuilder.Entity("API.Schema.Chapter", b => - { - b.Property("ChapterId") - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("ChapterNumber") - .IsRequired() - .HasMaxLength(10) - .HasColumnType("character varying(10)"); - - b.Property("Downloaded") - .HasColumnType("boolean"); - - b.Property("FileName") - .IsRequired() - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.Property("ParentMangaId") - .IsRequired() - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("Title") - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.Property("Url") - .IsRequired() - .HasMaxLength(2048) - .HasColumnType("character varying(2048)"); - - b.Property("VolumeNumber") - .HasColumnType("integer"); - - b.HasKey("ChapterId"); - - b.HasIndex("ParentMangaId"); - - b.ToTable("Chapters"); - }); - - modelBuilder.Entity("API.Schema.Jobs.Job", b => - { - b.Property("JobId") - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.PrimitiveCollection("DependsOnJobsIds") - .HasMaxLength(64) - .HasColumnType("text[]"); - - b.Property("Enabled") - .HasColumnType("boolean"); - - b.Property("JobType") - .HasColumnType("smallint"); - - b.Property("LastExecution") - .HasColumnType("timestamp with time zone"); - - b.Property("ParentJobId") - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("RecurrenceMs") - .HasColumnType("numeric(20,0)"); - - b.Property("state") - .HasColumnType("smallint"); - - b.HasKey("JobId"); - - b.HasIndex("ParentJobId"); - - b.ToTable("Jobs"); - - b.HasDiscriminator("JobType"); - - b.UseTphMappingStrategy(); - }); - - modelBuilder.Entity("API.Schema.LibraryConnectors.LibraryConnector", b => - { - b.Property("LibraryConnectorId") - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("Auth") - .IsRequired() - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.Property("BaseUrl") - .IsRequired() - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.Property("LibraryType") - .HasColumnType("smallint"); - - b.HasKey("LibraryConnectorId"); - - b.ToTable("LibraryConnectors"); - - b.HasDiscriminator("LibraryType"); - - b.UseTphMappingStrategy(); - }); - - modelBuilder.Entity("API.Schema.Link", b => - { - b.Property("LinkId") - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("LinkProvider") - .IsRequired() - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("LinkUrl") - .IsRequired() - .HasMaxLength(2048) - .HasColumnType("character varying(2048)"); - - b.Property("MangaId") - .HasColumnType("character varying(64)"); - - b.HasKey("LinkId"); - - b.HasIndex("MangaId"); - - b.ToTable("Links"); - }); - - modelBuilder.Entity("API.Schema.LocalLibrary", b => - { - b.Property("LocalLibraryId") - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("BasePath") - .IsRequired() - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.Property("LibraryName") - .IsRequired() - .HasMaxLength(512) - .HasColumnType("character varying(512)"); - - b.HasKey("LocalLibraryId"); - - b.ToTable("LocalLibraries"); - }); - - modelBuilder.Entity("API.Schema.Manga", b => - { - b.Property("MangaId") - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("CoverFileNameInCache") - .HasColumnType("text"); - - b.Property("CoverUrl") - .IsRequired() - .HasColumnType("text"); - - b.Property("Description") - .IsRequired() - .HasColumnType("text"); - - b.Property("DirectoryName") - .IsRequired() - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.Property("IdOnConnectorSite") - .IsRequired() - .HasMaxLength(128) - .HasColumnType("character varying(128)"); - - b.Property("IgnoreChapterBefore") - .HasColumnType("real"); - - b.Property("LibraryLocalLibraryId") - .HasColumnType("character varying(64)"); - - b.Property("MangaConnectorId") - .IsRequired() - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.Property("OriginalLanguage") - .IsRequired() - .HasMaxLength(8) - .HasColumnType("character varying(8)"); - - b.Property("ReleaseStatus") - .HasColumnType("smallint"); - - b.Property("WebsiteUrl") - .IsRequired() - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.Property("Year") - .HasColumnType("bigint"); - - b.HasKey("MangaId"); - - b.HasIndex("LibraryLocalLibraryId"); - - b.HasIndex("MangaConnectorId"); - - b.ToTable("Mangas"); - }); - - modelBuilder.Entity("API.Schema.MangaAltTitle", b => - { - b.Property("AltTitleId") - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("Language") - .IsRequired() - .HasMaxLength(8) - .HasColumnType("character varying(8)"); - - b.Property("MangaId") - .HasColumnType("character varying(64)"); - - b.Property("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("Name") - .HasMaxLength(32) - .HasColumnType("character varying(32)"); - - b.PrimitiveCollection("BaseUris") - .IsRequired() - .HasMaxLength(256) - .HasColumnType("text[]"); - - b.Property("Enabled") - .HasColumnType("boolean"); - - b.Property("IconUrl") - .IsRequired() - .HasMaxLength(2048) - .HasColumnType("character varying(2048)"); - - b.PrimitiveCollection("SupportedLanguages") - .IsRequired() - .HasMaxLength(8) - .HasColumnType("text[]"); - - b.HasKey("Name"); - - b.ToTable("MangaConnectors"); - - b.HasDiscriminator("Name").HasValue("MangaConnector"); - - b.UseTphMappingStrategy(); - }); - - modelBuilder.Entity("API.Schema.MangaTag", b => - { - b.Property("Tag") - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.HasKey("Tag"); - - b.ToTable("Tags"); - }); - - modelBuilder.Entity("API.Schema.Notification", b => - { - b.Property("NotificationId") - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("Date") - .HasColumnType("timestamp with time zone"); - - b.Property("Message") - .IsRequired() - .HasMaxLength(512) - .HasColumnType("character varying(512)"); - - b.Property("Title") - .IsRequired() - .HasMaxLength(128) - .HasColumnType("character varying(128)"); - - b.Property("Urgency") - .HasColumnType("smallint"); - - b.HasKey("NotificationId"); - - b.ToTable("Notifications"); - }); - - modelBuilder.Entity("API.Schema.NotificationConnectors.NotificationConnector", b => - { - b.Property("Name") - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("Body") - .IsRequired() - .HasMaxLength(4096) - .HasColumnType("character varying(4096)"); - - b.Property>("Headers") - .IsRequired() - .HasColumnType("hstore"); - - b.Property("HttpMethod") - .IsRequired() - .HasMaxLength(8) - .HasColumnType("character varying(8)"); - - b.Property("Url") - .IsRequired() - .HasMaxLength(2048) - .HasColumnType("character varying(2048)"); - - b.HasKey("Name"); - - b.ToTable("NotificationConnectors"); - }); - - modelBuilder.Entity("AuthorManga", b => - { - b.Property("AuthorsAuthorId") - .HasColumnType("character varying(64)"); - - b.Property("MangaId") - .HasColumnType("character varying(64)"); - - b.HasKey("AuthorsAuthorId", "MangaId"); - - b.HasIndex("MangaId"); - - b.ToTable("AuthorManga"); - }); - - modelBuilder.Entity("JobJob", b => - { - b.Property("DependsOnJobsJobId") - .HasColumnType("character varying(64)"); - - b.Property("JobId") - .HasColumnType("character varying(64)"); - - b.HasKey("DependsOnJobsJobId", "JobId"); - - b.HasIndex("JobId"); - - b.ToTable("JobJob"); - }); - - modelBuilder.Entity("MangaMangaTag", b => - { - b.Property("MangaId") - .HasColumnType("character varying(64)"); - - b.Property("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("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("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("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("FromLocation") - .IsRequired() - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.Property("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("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("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("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.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/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 -{ - /// - public partial class dev1603252 : Migration - { - /// - 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); - } - - /// - 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 @@ -// -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 - { - /// - 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("AuthorId") - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("AuthorName") - .IsRequired() - .HasMaxLength(128) - .HasColumnType("character varying(128)"); - - b.HasKey("AuthorId"); - - b.ToTable("Authors"); - }); - - modelBuilder.Entity("API.Schema.Chapter", b => - { - b.Property("ChapterId") - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("ChapterNumber") - .IsRequired() - .HasMaxLength(10) - .HasColumnType("character varying(10)"); - - b.Property("Downloaded") - .HasColumnType("boolean"); - - b.Property("FileName") - .IsRequired() - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.Property("ParentMangaId") - .IsRequired() - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("Title") - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.Property("Url") - .IsRequired() - .HasMaxLength(2048) - .HasColumnType("character varying(2048)"); - - b.Property("VolumeNumber") - .HasColumnType("integer"); - - b.HasKey("ChapterId"); - - b.HasIndex("ParentMangaId"); - - b.ToTable("Chapters"); - }); - - modelBuilder.Entity("API.Schema.Jobs.Job", b => - { - b.Property("JobId") - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.PrimitiveCollection("DependsOnJobsIds") - .HasMaxLength(64) - .HasColumnType("text[]"); - - b.Property("Enabled") - .HasColumnType("boolean"); - - b.Property("JobType") - .HasColumnType("smallint"); - - b.Property("LastExecution") - .HasColumnType("timestamp with time zone"); - - b.Property("ParentJobId") - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("RecurrenceMs") - .HasColumnType("numeric(20,0)"); - - b.Property("state") - .HasColumnType("smallint"); - - b.HasKey("JobId"); - - b.HasIndex("ParentJobId"); - - b.ToTable("Jobs"); - - b.HasDiscriminator("JobType"); - - b.UseTphMappingStrategy(); - }); - - modelBuilder.Entity("API.Schema.LibraryConnectors.LibraryConnector", b => - { - b.Property("LibraryConnectorId") - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("Auth") - .IsRequired() - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.Property("BaseUrl") - .IsRequired() - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.Property("LibraryType") - .HasColumnType("smallint"); - - b.HasKey("LibraryConnectorId"); - - b.ToTable("LibraryConnectors"); - - b.HasDiscriminator("LibraryType"); - - b.UseTphMappingStrategy(); - }); - - modelBuilder.Entity("API.Schema.Link", b => - { - b.Property("LinkId") - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("LinkProvider") - .IsRequired() - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("LinkUrl") - .IsRequired() - .HasMaxLength(2048) - .HasColumnType("character varying(2048)"); - - b.Property("MangaId") - .HasColumnType("character varying(64)"); - - b.HasKey("LinkId"); - - b.HasIndex("MangaId"); - - b.ToTable("Links"); - }); - - modelBuilder.Entity("API.Schema.LocalLibrary", b => - { - b.Property("LocalLibraryId") - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("BasePath") - .IsRequired() - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.Property("LibraryName") - .IsRequired() - .HasMaxLength(512) - .HasColumnType("character varying(512)"); - - b.HasKey("LocalLibraryId"); - - b.ToTable("LocalLibraries"); - }); - - modelBuilder.Entity("API.Schema.Manga", b => - { - b.Property("MangaId") - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("CoverFileNameInCache") - .HasColumnType("text"); - - b.Property("CoverUrl") - .IsRequired() - .HasColumnType("text"); - - b.Property("Description") - .IsRequired() - .HasColumnType("text"); - - b.Property("DirectoryName") - .IsRequired() - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.Property("IdOnConnectorSite") - .IsRequired() - .HasMaxLength(128) - .HasColumnType("character varying(128)"); - - b.Property("IgnoreChapterBefore") - .HasColumnType("real"); - - b.Property("LibraryLocalLibraryId") - .HasColumnType("character varying(64)"); - - b.Property("MangaConnectorId") - .IsRequired() - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.Property("OriginalLanguage") - .HasMaxLength(8) - .HasColumnType("character varying(8)"); - - b.Property("ReleaseStatus") - .HasColumnType("smallint"); - - b.Property("WebsiteUrl") - .IsRequired() - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.Property("Year") - .HasColumnType("bigint"); - - b.HasKey("MangaId"); - - b.HasIndex("LibraryLocalLibraryId"); - - b.HasIndex("MangaConnectorId"); - - b.ToTable("Mangas"); - }); - - modelBuilder.Entity("API.Schema.MangaAltTitle", b => - { - b.Property("AltTitleId") - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("Language") - .IsRequired() - .HasMaxLength(8) - .HasColumnType("character varying(8)"); - - b.Property("MangaId") - .HasColumnType("character varying(64)"); - - b.Property("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("Name") - .HasMaxLength(32) - .HasColumnType("character varying(32)"); - - b.PrimitiveCollection("BaseUris") - .IsRequired() - .HasMaxLength(256) - .HasColumnType("text[]"); - - b.Property("Enabled") - .HasColumnType("boolean"); - - b.Property("IconUrl") - .IsRequired() - .HasMaxLength(2048) - .HasColumnType("character varying(2048)"); - - b.PrimitiveCollection("SupportedLanguages") - .IsRequired() - .HasMaxLength(8) - .HasColumnType("text[]"); - - b.HasKey("Name"); - - b.ToTable("MangaConnectors"); - - b.HasDiscriminator("Name").HasValue("MangaConnector"); - - b.UseTphMappingStrategy(); - }); - - modelBuilder.Entity("API.Schema.MangaTag", b => - { - b.Property("Tag") - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.HasKey("Tag"); - - b.ToTable("Tags"); - }); - - modelBuilder.Entity("API.Schema.Notification", b => - { - b.Property("NotificationId") - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("Date") - .HasColumnType("timestamp with time zone"); - - b.Property("Message") - .IsRequired() - .HasMaxLength(512) - .HasColumnType("character varying(512)"); - - b.Property("Title") - .IsRequired() - .HasMaxLength(128) - .HasColumnType("character varying(128)"); - - b.Property("Urgency") - .HasColumnType("smallint"); - - b.HasKey("NotificationId"); - - b.ToTable("Notifications"); - }); - - modelBuilder.Entity("API.Schema.NotificationConnectors.NotificationConnector", b => - { - b.Property("Name") - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("Body") - .IsRequired() - .HasMaxLength(4096) - .HasColumnType("character varying(4096)"); - - b.Property>("Headers") - .IsRequired() - .HasColumnType("hstore"); - - b.Property("HttpMethod") - .IsRequired() - .HasMaxLength(8) - .HasColumnType("character varying(8)"); - - b.Property("Url") - .IsRequired() - .HasMaxLength(2048) - .HasColumnType("character varying(2048)"); - - b.HasKey("Name"); - - b.ToTable("NotificationConnectors"); - }); - - modelBuilder.Entity("AuthorManga", b => - { - b.Property("AuthorsAuthorId") - .HasColumnType("character varying(64)"); - - b.Property("MangaId") - .HasColumnType("character varying(64)"); - - b.HasKey("AuthorsAuthorId", "MangaId"); - - b.HasIndex("MangaId"); - - b.ToTable("AuthorManga"); - }); - - modelBuilder.Entity("JobJob", b => - { - b.Property("DependsOnJobsJobId") - .HasColumnType("character varying(64)"); - - b.Property("JobId") - .HasColumnType("character varying(64)"); - - b.HasKey("DependsOnJobsJobId", "JobId"); - - b.HasIndex("JobId"); - - b.ToTable("JobJob"); - }); - - modelBuilder.Entity("MangaMangaTag", b => - { - b.Property("MangaId") - .HasColumnType("character varying(64)"); - - b.Property("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("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("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("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("FromLocation") - .IsRequired() - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.Property("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("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("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("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/20250401001439_dev-010425-1.cs b/API/Migrations/20250401001439_dev-010425-1.cs deleted file mode 100644 index c5d27f5..0000000 --- a/API/Migrations/20250401001439_dev-010425-1.cs +++ /dev/null @@ -1,40 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace API.Migrations -{ - /// - public partial class dev0104251 : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AlterColumn( - name: "OriginalLanguage", - table: "Mangas", - type: "character varying(8)", - maxLength: 8, - nullable: true, - oldClrType: typeof(string), - oldType: "character varying(8)", - oldMaxLength: 8); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.AlterColumn( - name: "OriginalLanguage", - table: "Mangas", - type: "character varying(8)", - maxLength: 8, - nullable: false, - defaultValue: "", - oldClrType: typeof(string), - oldType: "character varying(8)", - oldMaxLength: 8, - oldNullable: true); - } - } -} 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 -{ - /// - public partial class dev0104252Longer_Var_Chars : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AlterColumn( - name: "WebsiteUrl", - table: "Mangas", - type: "character varying(512)", - maxLength: 512, - nullable: false, - oldClrType: typeof(string), - oldType: "character varying(256)", - oldMaxLength: 256); - - migrationBuilder.AlterColumn( - name: "Name", - table: "Mangas", - type: "character varying(512)", - maxLength: 512, - nullable: false, - oldClrType: typeof(string), - oldType: "character varying(256)", - oldMaxLength: 256); - - migrationBuilder.AlterColumn( - name: "IdOnConnectorSite", - table: "Mangas", - type: "character varying(256)", - maxLength: 256, - nullable: false, - oldClrType: typeof(string), - oldType: "character varying(128)", - oldMaxLength: 128); - - migrationBuilder.AlterColumn( - name: "DirectoryName", - table: "Mangas", - type: "character varying(1024)", - maxLength: 1024, - nullable: false, - oldClrType: typeof(string), - oldType: "character varying(256)", - oldMaxLength: 256); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.AlterColumn( - name: "WebsiteUrl", - table: "Mangas", - type: "character varying(256)", - maxLength: 256, - nullable: false, - oldClrType: typeof(string), - oldType: "character varying(512)", - oldMaxLength: 512); - - migrationBuilder.AlterColumn( - name: "Name", - table: "Mangas", - type: "character varying(256)", - maxLength: 256, - nullable: false, - oldClrType: typeof(string), - oldType: "character varying(512)", - oldMaxLength: 512); - - migrationBuilder.AlterColumn( - name: "IdOnConnectorSite", - table: "Mangas", - type: "character varying(128)", - maxLength: 128, - nullable: false, - oldClrType: typeof(string), - oldType: "character varying(256)", - oldMaxLength: 256); - - migrationBuilder.AlterColumn( - 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/20250401234456_dev-010425-3-ParentJobOwnership.cs b/API/Migrations/20250401234456_dev-010425-3-ParentJobOwnership.cs deleted file mode 100644 index e6c528e..0000000 --- a/API/Migrations/20250401234456_dev-010425-3-ParentJobOwnership.cs +++ /dev/null @@ -1,22 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace API.Migrations -{ - /// - public partial class dev0104253ParentJobOwnership : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - - } - } -} 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 -{ - /// - public partial class dev0104254 : Migration - { - /// - 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"); - } - - /// - 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/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 @@ +// +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 + { + /// + 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("LibraryConnectorId") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Auth") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("BaseUrl") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("LibraryType") + .HasColumnType("smallint"); + + b.HasKey("LibraryConnectorId"); + + b.ToTable("LibraryConnectors"); + + b.HasDiscriminator("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 +{ + /// + public partial class Initial : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "LibraryConnectors", + columns: table => new + { + LibraryConnectorId = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), + LibraryType = table.Column(type: "smallint", nullable: false), + BaseUrl = table.Column(type: "character varying(256)", maxLength: 256, nullable: false), + Auth = table.Column(type: "character varying(256)", maxLength: 256, nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_LibraryConnectors", x => x.LibraryConnectorId); + }); + } + + /// + 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 @@ +// +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("LibraryConnectorId") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Auth") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("BaseUrl") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("LibraryType") + .HasColumnType("smallint"); + + b.HasKey("LibraryConnectorId"); + + b.ToTable("LibraryConnectors"); + + b.HasDiscriminator("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 @@ +// +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 + { + /// + 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("NotificationId") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Date") + .HasColumnType("timestamp with time zone"); + + b.Property("Message") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("Urgency") + .HasColumnType("smallint"); + + b.HasKey("NotificationId"); + + b.ToTable("Notifications"); + }); + + modelBuilder.Entity("API.Schema.NotificationConnectors.NotificationConnector", b => + { + b.Property("Name") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Body") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("character varying(4096)"); + + b.Property>("Headers") + .IsRequired() + .HasColumnType("hstore"); + + b.Property("HttpMethod") + .IsRequired() + .HasMaxLength(8) + .HasColumnType("character varying(8)"); + + b.Property("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 +{ + /// + public partial class Initial : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterDatabase() + .Annotation("Npgsql:PostgresExtension:hstore", ",,"); + + migrationBuilder.CreateTable( + name: "NotificationConnectors", + columns: table => new + { + Name = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), + Url = table.Column(type: "character varying(2048)", maxLength: 2048, nullable: false), + Headers = table.Column>(type: "hstore", nullable: false), + HttpMethod = table.Column(type: "character varying(8)", maxLength: 8, nullable: false), + Body = table.Column(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(type: "character varying(64)", maxLength: 64, nullable: false), + Urgency = table.Column(type: "smallint", nullable: false), + Title = table.Column(type: "character varying(128)", maxLength: 128, nullable: false), + Message = table.Column(type: "character varying(512)", maxLength: 512, nullable: false), + Date = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Notifications", x => x.NotificationId); + }); + } + + /// + 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 @@ +// +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("NotificationId") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Date") + .HasColumnType("timestamp with time zone"); + + b.Property("Message") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("Urgency") + .HasColumnType("smallint"); + + b.HasKey("NotificationId"); + + b.ToTable("Notifications"); + }); + + modelBuilder.Entity("API.Schema.NotificationConnectors.NotificationConnector", b => + { + b.Property("Name") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Body") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("character varying(4096)"); + + b.Property>("Headers") + .IsRequired() + .HasColumnType("hstore"); + + b.Property("HttpMethod") + .IsRequired() + .HasMaxLength(8) + .HasColumnType("character varying(8)"); + + b.Property("Url") + .IsRequired() + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.HasKey("Name"); + + b.ToTable("NotificationConnectors"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/API/Migrations/pgsql/20250515120724_Initial-1.Designer.cs b/API/Migrations/pgsql/20250515120724_Initial-1.Designer.cs new file mode 100644 index 0000000..4b1b2cb --- /dev/null +++ b/API/Migrations/pgsql/20250515120724_Initial-1.Designer.cs @@ -0,0 +1,682 @@ +// +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("20250515120724_Initial-1")] + partial class Initial1 + { + /// + 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("AuthorId") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("AuthorName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.HasKey("AuthorId"); + + b.ToTable("Authors"); + }); + + modelBuilder.Entity("API.Schema.Chapter", b => + { + b.Property("ChapterId") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("ChapterNumber") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.Property("Downloaded") + .HasColumnType("boolean"); + + b.Property("FileName") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("ParentMangaId") + .IsRequired() + .HasColumnType("character varying(64)"); + + b.Property("Title") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Url") + .IsRequired() + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("VolumeNumber") + .HasColumnType("integer"); + + b.HasKey("ChapterId"); + + b.HasIndex("ParentMangaId"); + + b.ToTable("Chapters"); + }); + + modelBuilder.Entity("API.Schema.Jobs.Job", b => + { + b.Property("JobId") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("JobType") + .HasColumnType("smallint"); + + b.Property("LastExecution") + .HasColumnType("timestamp with time zone"); + + b.Property("ParentJobId") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("RecurrenceMs") + .HasColumnType("numeric(20,0)"); + + b.Property("state") + .HasColumnType("smallint"); + + b.HasKey("JobId"); + + b.HasIndex("ParentJobId"); + + b.ToTable("Jobs"); + + b.HasDiscriminator("JobType"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("API.Schema.LocalLibrary", b => + { + b.Property("LocalLibraryId") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("BasePath") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("LibraryName") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.HasKey("LocalLibraryId"); + + b.ToTable("LocalLibraries"); + }); + + modelBuilder.Entity("API.Schema.Manga", b => + { + b.Property("MangaId") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("CoverFileNameInCache") + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("CoverUrl") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("DirectoryName") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("IdOnConnectorSite") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IgnoreChaptersBefore") + .HasColumnType("real"); + + b.Property("LibraryId") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("MangaConnectorName") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("OriginalLanguage") + .HasMaxLength(8) + .HasColumnType("character varying(8)"); + + b.Property("ReleaseStatus") + .HasColumnType("smallint"); + + b.Property("WebsiteUrl") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("Year") + .HasColumnType("bigint"); + + b.HasKey("MangaId"); + + b.HasIndex("LibraryId"); + + b.HasIndex("MangaConnectorName"); + + b.ToTable("Mangas"); + }); + + modelBuilder.Entity("API.Schema.MangaConnectors.MangaConnector", b => + { + b.Property("Name") + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.PrimitiveCollection("BaseUris") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("text[]"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("IconUrl") + .IsRequired() + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.PrimitiveCollection("SupportedLanguages") + .IsRequired() + .HasMaxLength(8) + .HasColumnType("text[]"); + + b.HasKey("Name"); + + b.ToTable("MangaConnectors"); + + b.HasDiscriminator("Name").HasValue("MangaConnector"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("API.Schema.MangaTag", b => + { + b.Property("Tag") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasKey("Tag"); + + b.ToTable("Tags"); + }); + + modelBuilder.Entity("AuthorToManga", b => + { + b.Property("AuthorIds") + .HasColumnType("character varying(64)"); + + b.Property("MangaIds") + .HasColumnType("character varying(64)"); + + b.HasKey("AuthorIds", "MangaIds"); + + b.HasIndex("MangaIds"); + + b.ToTable("AuthorToManga"); + }); + + modelBuilder.Entity("JobJob", b => + { + b.Property("DependsOnJobsJobId") + .HasColumnType("character varying(64)"); + + b.Property("JobId") + .HasColumnType("character varying(64)"); + + b.HasKey("DependsOnJobsJobId", "JobId"); + + b.HasIndex("JobId"); + + b.ToTable("JobJob"); + }); + + modelBuilder.Entity("MangaTagToManga", b => + { + b.Property("MangaTagIds") + .HasColumnType("character varying(64)"); + + b.Property("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("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("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("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("FromLocation") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("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("MangaId") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("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("Language") + .IsRequired() + .HasMaxLength(8) + .HasColumnType("character varying(8)"); + + b.Property("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("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.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("LinkId") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b1.Property("LinkProvider") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b1.Property("LinkUrl") + .IsRequired() + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b1.Property("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("AltTitleId") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b1.Property("Language") + .IsRequired() + .HasMaxLength(8) + .HasColumnType("character varying(8)"); + + b1.Property("MangaId") + .IsRequired() + .HasColumnType("character varying(64)"); + + b1.Property("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.Manga", b => + { + b.Navigation("Chapters"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/API/Migrations/20250316143014_dev-160325-Initial.cs b/API/Migrations/pgsql/20250515120724_Initial-1.cs similarity index 70% rename from API/Migrations/20250316143014_dev-160325-Initial.cs rename to API/Migrations/pgsql/20250515120724_Initial-1.cs index c447504..97295f0 100644 --- a/API/Migrations/20250316143014_dev-160325-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 { /// - public partial class dev160325Initial : Migration + public partial class Initial1 : Migration { /// 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(type: "character varying(64)", maxLength: 64, nullable: false), - LibraryType = table.Column(type: "smallint", nullable: false), - BaseUrl = table.Column(type: "character varying(256)", maxLength: 256, nullable: false), - Auth = table.Column(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(type: "character varying(64)", maxLength: 64, nullable: false), - Url = table.Column(type: "character varying(2048)", maxLength: 2048, nullable: false), - Headers = table.Column>(type: "hstore", nullable: false), - HttpMethod = table.Column(type: "character varying(8)", maxLength: 8, nullable: false), - Body = table.Column(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(type: "character varying(64)", maxLength: 64, nullable: false), - Urgency = table.Column(type: "smallint", nullable: false), - Title = table.Column(type: "character varying(128)", maxLength: 128, nullable: false), - Message = table.Column(type: "character varying(512)", maxLength: 512, nullable: false), - Date = table.Column(type: "timestamp with time zone", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_Notifications", x => x.NotificationId); - }); - migrationBuilder.CreateTable( name: "Tags", columns: table => new @@ -115,76 +67,56 @@ namespace API.Migrations columns: table => new { MangaId = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), - IdOnConnectorSite = table.Column(type: "character varying(128)", maxLength: 128, nullable: false), - Name = table.Column(type: "character varying(256)", maxLength: 256, nullable: false), + IdOnConnectorSite = table.Column(type: "character varying(256)", maxLength: 256, nullable: false), + Name = table.Column(type: "character varying(512)", maxLength: 512, nullable: false), Description = table.Column(type: "text", nullable: false), - WebsiteUrl = table.Column(type: "character varying(256)", maxLength: 256, nullable: false), - CoverUrl = table.Column(type: "text", nullable: false), - CoverFileNameInCache = table.Column(type: "text", nullable: true), - Year = table.Column(type: "bigint", nullable: false), - OriginalLanguage = table.Column(type: "character varying(8)", maxLength: 8, nullable: false), + WebsiteUrl = table.Column(type: "character varying(512)", maxLength: 512, nullable: false), + CoverUrl = table.Column(type: "character varying(512)", maxLength: 512, nullable: false), ReleaseStatus = table.Column(type: "smallint", nullable: false), - DirectoryName = table.Column(type: "character varying(256)", maxLength: 256, nullable: false), - LibraryLocalLibraryId = table.Column(type: "character varying(64)", nullable: true), - IgnoreChapterBefore = table.Column(type: "real", nullable: false), - MangaConnectorId = table.Column(type: "character varying(64)", maxLength: 64, nullable: false) + LibraryId = table.Column(type: "character varying(64)", maxLength: 64, nullable: true), + MangaConnectorName = table.Column(type: "character varying(32)", maxLength: 32, nullable: false), + IgnoreChaptersBefore = table.Column(type: "real", nullable: false), + DirectoryName = table.Column(type: "character varying(1024)", maxLength: 1024, nullable: false), + CoverFileNameInCache = table.Column(type: "character varying(512)", maxLength: 512, nullable: true), + Year = table.Column(type: "bigint", nullable: false), + OriginalLanguage = table.Column(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); }); migrationBuilder.CreateTable( - name: "AltTitles", + name: "AuthorToManga", columns: table => new { - AltTitleId = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), - Language = table.Column(type: "character varying(8)", maxLength: 8, nullable: false), - Title = table.Column(type: "character varying(256)", maxLength: 256, nullable: false), - MangaId = table.Column(type: "character varying(64)", nullable: true) + AuthorIds = table.Column(type: "character varying(64)", nullable: false), + MangaIds = table.Column(type: "character varying(64)", nullable: false) }, constraints: table => { - table.PrimaryKey("PK_AltTitles", x => x.AltTitleId); + table.PrimaryKey("PK_AuthorToManga", x => new { x.AuthorIds, x.MangaIds }); table.ForeignKey( - name: "FK_AltTitles_Mangas_MangaId", - column: x => x.MangaId, - principalTable: "Mangas", - principalColumn: "MangaId", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "AuthorManga", - columns: table => new - { - AuthorsAuthorId = table.Column(type: "character varying(64)", nullable: false), - MangaId = table.Column(type: "character varying(64)", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_AuthorManga", x => new { x.AuthorsAuthorId, x.MangaId }); - 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 +127,13 @@ namespace API.Migrations columns: table => new { ChapterId = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), + ParentMangaId = table.Column(type: "character varying(64)", nullable: false), VolumeNumber = table.Column(type: "integer", nullable: true), ChapterNumber = table.Column(type: "character varying(10)", maxLength: 10, nullable: false), Url = table.Column(type: "character varying(2048)", maxLength: 2048, nullable: false), Title = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), FileName = table.Column(type: "character varying(256)", maxLength: 256, nullable: false), - Downloaded = table.Column(type: "boolean", nullable: false), - ParentMangaId = table.Column(type: "character varying(64)", maxLength: 64, nullable: false) + Downloaded = table.Column(type: "boolean", nullable: false) }, constraints: table => { @@ -215,19 +147,19 @@ namespace API.Migrations }); migrationBuilder.CreateTable( - name: "Links", + name: "Link", columns: table => new { LinkId = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), LinkProvider = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), LinkUrl = table.Column(type: "character varying(2048)", maxLength: 2048, nullable: false), - MangaId = table.Column(type: "character varying(64)", nullable: true) + MangaId = table.Column(type: "character varying(64)", nullable: false) }, 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", @@ -235,24 +167,44 @@ namespace API.Migrations }); migrationBuilder.CreateTable( - name: "MangaMangaTag", + name: "MangaAltTitle", columns: table => new { - MangaId = table.Column(type: "character varying(64)", nullable: false), - MangaTagsTag = table.Column(type: "character varying(64)", nullable: false) + AltTitleId = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), + Language = table.Column(type: "character varying(8)", maxLength: 8, nullable: false), + Title = table.Column(type: "character varying(256)", maxLength: 256, nullable: false), + MangaId = table.Column(type: "character varying(64)", nullable: false) }, constraints: table => { - table.PrimaryKey("PK_MangaMangaTag", x => new { x.MangaId, x.MangaTagsTag }); + table.PrimaryKey("PK_MangaAltTitle", x => x.AltTitleId); table.ForeignKey( - name: "FK_MangaMangaTag_Mangas_MangaId", + name: "FK_MangaAltTitle_Mangas_MangaId", column: x => x.MangaId, principalTable: "Mangas", principalColumn: "MangaId", onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "MangaTagToManga", + columns: table => new + { + MangaTagIds = table.Column(type: "character varying(64)", nullable: false), + MangaIds = table.Column(type: "character varying(64)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_MangaTagToManga", x => new { x.MangaTagIds, x.MangaIds }); table.ForeignKey( - name: "FK_MangaMangaTag_Tags_MangaTagsTag", - column: x => x.MangaTagsTag, + name: "FK_MangaTagToManga_Mangas_MangaIds", + column: x => x.MangaIds, + principalTable: "Mangas", + principalColumn: "MangaId", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_MangaTagToManga_Tags_MangaTagIds", + column: x => x.MangaTagIds, principalTable: "Tags", principalColumn: "Tag", onDelete: ReferentialAction.Cascade); @@ -264,7 +216,6 @@ namespace API.Migrations { JobId = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), ParentJobId = table.Column(type: "character varying(64)", maxLength: 64, nullable: true), - DependsOnJobsIds = table.Column(type: "text[]", maxLength: 64, nullable: true), JobType = table.Column(type: "smallint", nullable: false), RecurrenceMs = table.Column(type: "numeric(20,0)", nullable: false), LastExecution = table.Column(type: "timestamp with time zone", nullable: false), @@ -275,9 +226,11 @@ namespace API.Migrations ChapterId = table.Column(type: "character varying(64)", maxLength: 64, nullable: true), FromLocation = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), ToLocation = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + MoveMangaLibraryJob_MangaId = table.Column(type: "character varying(64)", maxLength: 64, nullable: true), + ToLibraryId = table.Column(type: "character varying(64)", maxLength: 64, nullable: true), RetrieveChaptersJob_MangaId = table.Column(type: "character varying(64)", maxLength: 64, nullable: true), - UpdateFilesDownloadedJob_MangaId = table.Column(type: "character varying(64)", maxLength: 64, nullable: true), - UpdateMetadataJob_MangaId = table.Column(type: "character varying(64)", maxLength: 64, nullable: true) + Language = table.Column(type: "character varying(8)", maxLength: 8, nullable: true), + UpdateFilesDownloadedJob_MangaId = table.Column(type: "character varying(64)", maxLength: 64, nullable: true) }, constraints: table => { @@ -294,6 +247,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 +265,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 +283,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( @@ -351,14 +310,9 @@ namespace API.Migrations }); migrationBuilder.CreateIndex( - name: "IX_AltTitles_MangaId", - table: "AltTitles", - 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 +339,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,63 +354,59 @@ 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", + name: "IX_Link_MangaId", + table: "Link", column: "MangaId"); migrationBuilder.CreateIndex( - name: "IX_MangaMangaTag_MangaTagsTag", - table: "MangaMangaTag", - column: "MangaTagsTag"); + name: "IX_MangaAltTitle_MangaId", + table: "MangaAltTitle", + column: "MangaId"); migrationBuilder.CreateIndex( - name: "IX_Mangas_LibraryLocalLibraryId", + name: "IX_Mangas_LibraryId", table: "Mangas", - column: "LibraryLocalLibraryId"); + column: "LibraryId"); migrationBuilder.CreateIndex( - name: "IX_Mangas_MangaConnectorId", + name: "IX_Mangas_MangaConnectorName", table: "Mangas", - column: "MangaConnectorId"); + column: "MangaConnectorName"); + + migrationBuilder.CreateIndex( + name: "IX_MangaTagToManga_MangaIds", + table: "MangaTagToManga", + column: "MangaIds"); } /// protected override void Down(MigrationBuilder migrationBuilder) { migrationBuilder.DropTable( - name: "AltTitles"); - - migrationBuilder.DropTable( - name: "AuthorManga"); + name: "AuthorToManga"); migrationBuilder.DropTable( name: "JobJob"); migrationBuilder.DropTable( - name: "LibraryConnectors"); + name: "Link"); migrationBuilder.DropTable( - name: "Links"); + name: "MangaAltTitle"); migrationBuilder.DropTable( - name: "MangaMangaTag"); - - migrationBuilder.DropTable( - name: "NotificationConnectors"); - - migrationBuilder.DropTable( - name: "Notifications"); + name: "MangaTagToManga"); migrationBuilder.DropTable( name: "Authors"); diff --git a/API/Migrations/20250401234456_dev-010425-3-ParentJobOwnership.Designer.cs b/API/Migrations/pgsql/20250516121442_AltTitle-Owned.Designer.cs similarity index 66% rename from API/Migrations/20250401234456_dev-010425-3-ParentJobOwnership.Designer.cs rename to API/Migrations/pgsql/20250516121442_AltTitle-Owned.Designer.cs index 6e2a0bd..c97d81d 100644 --- a/API/Migrations/20250401234456_dev-010425-3-ParentJobOwnership.Designer.cs +++ b/API/Migrations/pgsql/20250516121442_AltTitle-Owned.Designer.cs @@ -1,7 +1,6 @@ // 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("20250401234456_dev-010425-3-ParentJobOwnership")] - partial class dev0104253ParentJobOwnership + [Migration("20250516121442_AltTitle-Owned")] + partial class AltTitleOwned { /// 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 => @@ -64,7 +62,6 @@ namespace API.Migrations b.Property("ParentMangaId") .IsRequired() - .HasMaxLength(64) .HasColumnType("character varying(64)"); b.Property("Title") @@ -92,10 +89,6 @@ namespace API.Migrations .HasMaxLength(64) .HasColumnType("character varying(64)"); - b.PrimitiveCollection("DependsOnJobsIds") - .HasMaxLength(64) - .HasColumnType("text[]"); - b.Property("Enabled") .HasColumnType("boolean"); @@ -126,60 +119,6 @@ namespace API.Migrations b.UseTphMappingStrategy(); }); - modelBuilder.Entity("API.Schema.LibraryConnectors.LibraryConnector", b => - { - b.Property("LibraryConnectorId") - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("Auth") - .IsRequired() - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.Property("BaseUrl") - .IsRequired() - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.Property("LibraryType") - .HasColumnType("smallint"); - - b.HasKey("LibraryConnectorId"); - - b.ToTable("LibraryConnectors"); - - b.HasDiscriminator("LibraryType"); - - b.UseTphMappingStrategy(); - }); - - modelBuilder.Entity("API.Schema.Link", b => - { - b.Property("LinkId") - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("LinkProvider") - .IsRequired() - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("LinkUrl") - .IsRequired() - .HasMaxLength(2048) - .HasColumnType("character varying(2048)"); - - b.Property("MangaId") - .HasColumnType("character varying(64)"); - - b.HasKey("LinkId"); - - b.HasIndex("MangaId"); - - b.ToTable("Links"); - }); - modelBuilder.Entity("API.Schema.LocalLibrary", b => { b.Property("LocalLibraryId") @@ -208,11 +147,13 @@ namespace API.Migrations .HasColumnType("character varying(64)"); b.Property("CoverFileNameInCache") - .HasColumnType("text"); + .HasMaxLength(512) + .HasColumnType("character varying(512)"); b.Property("CoverUrl") .IsRequired() - .HasColumnType("text"); + .HasMaxLength(512) + .HasColumnType("character varying(512)"); b.Property("Description") .IsRequired() @@ -228,17 +169,18 @@ namespace API.Migrations .HasMaxLength(256) .HasColumnType("character varying(256)"); - b.Property("IgnoreChapterBefore") + b.Property("IgnoreChaptersBefore") .HasColumnType("real"); - b.Property("LibraryLocalLibraryId") - .HasColumnType("character varying(64)"); - - b.Property("MangaConnectorId") - .IsRequired() + b.Property("LibraryId") .HasMaxLength(64) .HasColumnType("character varying(64)"); + b.Property("MangaConnectorName") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + b.Property("Name") .IsRequired() .HasMaxLength(512) @@ -261,39 +203,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("AltTitleId") - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("Language") - .IsRequired() - .HasMaxLength(8) - .HasColumnType("character varying(8)"); - - b.Property("MangaId") - .HasColumnType("character varying(64)"); - - b.Property("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("Name") @@ -338,76 +254,19 @@ namespace API.Migrations b.ToTable("Tags"); }); - modelBuilder.Entity("API.Schema.Notification", b => + modelBuilder.Entity("AuthorToManga", b => { - b.Property("NotificationId") - .HasMaxLength(64) + b.Property("AuthorIds") .HasColumnType("character varying(64)"); - b.Property("Date") - .HasColumnType("timestamp with time zone"); - - b.Property("Message") - .IsRequired() - .HasMaxLength(512) - .HasColumnType("character varying(512)"); - - b.Property("Title") - .IsRequired() - .HasMaxLength(128) - .HasColumnType("character varying(128)"); - - b.Property("Urgency") - .HasColumnType("smallint"); - - b.HasKey("NotificationId"); - - b.ToTable("Notifications"); - }); - - modelBuilder.Entity("API.Schema.NotificationConnectors.NotificationConnector", b => - { - b.Property("Name") - .HasMaxLength(64) + b.Property("MangaIds") .HasColumnType("character varying(64)"); - b.Property("Body") - .IsRequired() - .HasMaxLength(4096) - .HasColumnType("character varying(4096)"); + b.HasKey("AuthorIds", "MangaIds"); - b.Property>("Headers") - .IsRequired() - .HasColumnType("hstore"); + b.HasIndex("MangaIds"); - b.Property("HttpMethod") - .IsRequired() - .HasMaxLength(8) - .HasColumnType("character varying(8)"); - - b.Property("Url") - .IsRequired() - .HasMaxLength(2048) - .HasColumnType("character varying(2048)"); - - b.HasKey("Name"); - - b.ToTable("NotificationConnectors"); - }); - - modelBuilder.Entity("AuthorManga", b => - { - b.Property("AuthorsAuthorId") - .HasColumnType("character varying(64)"); - - b.Property("MangaId") - .HasColumnType("character varying(64)"); - - b.HasKey("AuthorsAuthorId", "MangaId"); - - b.HasIndex("MangaId"); - - b.ToTable("AuthorManga"); + b.ToTable("AuthorToManga"); }); modelBuilder.Entity("JobJob", b => @@ -425,19 +284,19 @@ namespace API.Migrations b.ToTable("JobJob"); }); - modelBuilder.Entity("MangaMangaTag", b => + modelBuilder.Entity("MangaTagToManga", b => { - b.Property("MangaId") + b.Property("MangaTagIds") .HasColumnType("character varying(64)"); - b.Property("MangaTagsTag") + b.Property("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 +364,42 @@ namespace API.Migrations b.HasDiscriminator().HasValue((byte)3); }); + modelBuilder.Entity("API.Schema.Jobs.MoveMangaLibraryJob", b => + { + b.HasBaseType("API.Schema.Jobs.Job"); + + b.Property("MangaId") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("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("Language") + .IsRequired() + .HasMaxLength(8) + .HasColumnType("character varying(8)"); + b.Property("MangaId") .IsRequired() .HasMaxLength(64) @@ -525,7 +416,7 @@ namespace API.Migrations 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"); @@ -545,52 +436,11 @@ namespace API.Migrations b.HasDiscriminator().HasValue((byte)6); }); - modelBuilder.Entity("API.Schema.Jobs.UpdateMetadataJob", b => - { - b.HasBaseType("API.Schema.Jobs.Job"); - - b.Property("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 => + modelBuilder.Entity("API.Schema.MangaConnectors.ComickIo", 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("ComickIo"); }); modelBuilder.Entity("API.Schema.MangaConnectors.Global", b => @@ -607,52 +457,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 +478,98 @@ 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("LinkId") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b1.Property("LinkProvider") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b1.Property("LinkUrl") + .IsRequired() + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b1.Property("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("MangaId") + .HasColumnType("character varying(64)"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("Language") + .IsRequired() + .HasMaxLength(8) + .HasColumnType("character varying(8)"); + + b1.Property("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("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 +589,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 +637,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") @@ -793,18 +667,7 @@ namespace API.Migrations 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 => + modelBuilder.Entity("API.Schema.Jobs.UpdateChaptersDownloadedJob", b => { b.HasOne("API.Schema.Manga", "Manga") .WithMany() @@ -817,9 +680,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/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 +{ + /// + public partial class AltTitleOwned : Migration + { + /// + 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( + 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" }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropPrimaryKey( + name: "PK_MangaAltTitle", + table: "MangaAltTitle"); + + migrationBuilder.DropColumn( + name: "Id", + table: "MangaAltTitle"); + + migrationBuilder.AddColumn( + 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/20250402001438_dev-010425-4.Designer.cs b/API/Migrations/pgsql/20250516121725_Manga-Year-Nullable.Designer.cs similarity index 65% rename from API/Migrations/20250402001438_dev-010425-4.Designer.cs rename to API/Migrations/pgsql/20250516121725_Manga-Year-Nullable.Designer.cs index 88957a4..2ebe310 100644 --- a/API/Migrations/20250402001438_dev-010425-4.Designer.cs +++ b/API/Migrations/pgsql/20250516121725_Manga-Year-Nullable.Designer.cs @@ -1,7 +1,6 @@ // 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("20250402001438_dev-010425-4")] - partial class dev0104254 + [Migration("20250516121725_Manga-Year-Nullable")] + partial class MangaYearNullable { /// 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 => @@ -64,7 +62,6 @@ namespace API.Migrations b.Property("ParentMangaId") .IsRequired() - .HasMaxLength(64) .HasColumnType("character varying(64)"); b.Property("Title") @@ -92,10 +89,6 @@ namespace API.Migrations .HasMaxLength(64) .HasColumnType("character varying(64)"); - b.PrimitiveCollection("DependsOnJobsIds") - .HasMaxLength(64) - .HasColumnType("text[]"); - b.Property("Enabled") .HasColumnType("boolean"); @@ -126,60 +119,6 @@ namespace API.Migrations b.UseTphMappingStrategy(); }); - modelBuilder.Entity("API.Schema.LibraryConnectors.LibraryConnector", b => - { - b.Property("LibraryConnectorId") - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("Auth") - .IsRequired() - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.Property("BaseUrl") - .IsRequired() - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.Property("LibraryType") - .HasColumnType("smallint"); - - b.HasKey("LibraryConnectorId"); - - b.ToTable("LibraryConnectors"); - - b.HasDiscriminator("LibraryType"); - - b.UseTphMappingStrategy(); - }); - - modelBuilder.Entity("API.Schema.Link", b => - { - b.Property("LinkId") - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("LinkProvider") - .IsRequired() - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("LinkUrl") - .IsRequired() - .HasMaxLength(2048) - .HasColumnType("character varying(2048)"); - - b.Property("MangaId") - .HasColumnType("character varying(64)"); - - b.HasKey("LinkId"); - - b.HasIndex("MangaId"); - - b.ToTable("Links"); - }); - modelBuilder.Entity("API.Schema.LocalLibrary", b => { b.Property("LocalLibraryId") @@ -208,11 +147,13 @@ namespace API.Migrations .HasColumnType("character varying(64)"); b.Property("CoverFileNameInCache") - .HasColumnType("text"); + .HasMaxLength(512) + .HasColumnType("character varying(512)"); b.Property("CoverUrl") .IsRequired() - .HasColumnType("text"); + .HasMaxLength(512) + .HasColumnType("character varying(512)"); b.Property("Description") .IsRequired() @@ -228,17 +169,18 @@ namespace API.Migrations .HasMaxLength(256) .HasColumnType("character varying(256)"); - b.Property("IgnoreChapterBefore") + b.Property("IgnoreChaptersBefore") .HasColumnType("real"); - b.Property("LibraryLocalLibraryId") - .HasColumnType("character varying(64)"); - - b.Property("MangaConnectorId") - .IsRequired() + b.Property("LibraryId") .HasMaxLength(64) .HasColumnType("character varying(64)"); + b.Property("MangaConnectorName") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + b.Property("Name") .IsRequired() .HasMaxLength(512) @@ -256,44 +198,18 @@ namespace API.Migrations .HasMaxLength(512) .HasColumnType("character varying(512)"); - b.Property("Year") + b.Property("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("AltTitleId") - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("Language") - .IsRequired() - .HasMaxLength(8) - .HasColumnType("character varying(8)"); - - b.Property("MangaId") - .HasColumnType("character varying(64)"); - - b.Property("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("Name") @@ -338,76 +254,19 @@ namespace API.Migrations b.ToTable("Tags"); }); - modelBuilder.Entity("API.Schema.Notification", b => + modelBuilder.Entity("AuthorToManga", b => { - b.Property("NotificationId") - .HasMaxLength(64) + b.Property("AuthorIds") .HasColumnType("character varying(64)"); - b.Property("Date") - .HasColumnType("timestamp with time zone"); - - b.Property("Message") - .IsRequired() - .HasMaxLength(512) - .HasColumnType("character varying(512)"); - - b.Property("Title") - .IsRequired() - .HasMaxLength(128) - .HasColumnType("character varying(128)"); - - b.Property("Urgency") - .HasColumnType("smallint"); - - b.HasKey("NotificationId"); - - b.ToTable("Notifications"); - }); - - modelBuilder.Entity("API.Schema.NotificationConnectors.NotificationConnector", b => - { - b.Property("Name") - .HasMaxLength(64) + b.Property("MangaIds") .HasColumnType("character varying(64)"); - b.Property("Body") - .IsRequired() - .HasMaxLength(4096) - .HasColumnType("character varying(4096)"); + b.HasKey("AuthorIds", "MangaIds"); - b.Property>("Headers") - .IsRequired() - .HasColumnType("hstore"); + b.HasIndex("MangaIds"); - b.Property("HttpMethod") - .IsRequired() - .HasMaxLength(8) - .HasColumnType("character varying(8)"); - - b.Property("Url") - .IsRequired() - .HasMaxLength(2048) - .HasColumnType("character varying(2048)"); - - b.HasKey("Name"); - - b.ToTable("NotificationConnectors"); - }); - - modelBuilder.Entity("AuthorManga", b => - { - b.Property("AuthorsAuthorId") - .HasColumnType("character varying(64)"); - - b.Property("MangaId") - .HasColumnType("character varying(64)"); - - b.HasKey("AuthorsAuthorId", "MangaId"); - - b.HasIndex("MangaId"); - - b.ToTable("AuthorManga"); + b.ToTable("AuthorToManga"); }); modelBuilder.Entity("JobJob", b => @@ -425,19 +284,19 @@ namespace API.Migrations b.ToTable("JobJob"); }); - modelBuilder.Entity("MangaMangaTag", b => + modelBuilder.Entity("MangaTagToManga", b => { - b.Property("MangaId") + b.Property("MangaTagIds") .HasColumnType("character varying(64)"); - b.Property("MangaTagsTag") + b.Property("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 => @@ -449,6 +308,8 @@ namespace API.Migrations .HasMaxLength(64) .HasColumnType("character varying(64)"); + b.HasIndex("MangaId"); + b.ToTable("Jobs", t => { t.Property("MangaId") @@ -467,6 +328,8 @@ namespace API.Migrations .HasMaxLength(64) .HasColumnType("character varying(64)"); + b.HasIndex("MangaId"); + b.HasDiscriminator().HasValue((byte)4); }); @@ -479,6 +342,8 @@ namespace API.Migrations .HasMaxLength(64) .HasColumnType("character varying(64)"); + b.HasIndex("ChapterId"); + b.HasDiscriminator().HasValue((byte)0); }); @@ -499,7 +364,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"); @@ -508,6 +373,40 @@ namespace API.Migrations .HasMaxLength(64) .HasColumnType("character varying(64)"); + b.Property("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("Language") + .IsRequired() + .HasMaxLength(8) + .HasColumnType("character varying(8)"); + + b.Property("MangaId") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasIndex("MangaId"); + b.ToTable("Jobs", t => { t.Property("MangaId") @@ -517,25 +416,7 @@ namespace API.Migrations b.HasDiscriminator().HasValue((byte)5); }); - modelBuilder.Entity("API.Schema.Jobs.UpdateFilesDownloadedJob", b => - { - b.HasBaseType("API.Schema.Jobs.Job"); - - b.Property("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 => + modelBuilder.Entity("API.Schema.Jobs.UpdateChaptersDownloadedJob", b => { b.HasBaseType("API.Schema.Jobs.Job"); @@ -549,38 +430,17 @@ namespace API.Migrations b.ToTable("Jobs", t => { t.Property("MangaId") - .HasColumnName("UpdateMetadataJob_MangaId"); + .HasColumnName("UpdateFilesDownloadedJob_MangaId"); }); - b.HasDiscriminator().HasValue((byte)2); + 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.AsuraToon", b => + modelBuilder.Entity("API.Schema.MangaConnectors.ComickIo", 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("ComickIo"); }); modelBuilder.Entity("API.Schema.MangaConnectors.Global", b => @@ -597,52 +457,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(); @@ -660,51 +478,98 @@ 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("LinkId") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b1.Property("LinkProvider") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b1.Property("LinkUrl") + .IsRequired() + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b1.Property("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("MangaId") + .HasColumnType("character varying(64)"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("Language") + .IsRequired() + .HasMaxLength(8) + .HasColumnType("character varying(8)"); + + b1.Property("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("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(); }); @@ -724,22 +589,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.UpdateChaptersDownloadedJob", b => { b.HasOne("API.Schema.Manga", "Manga") .WithMany() @@ -752,9 +680,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/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 +{ + /// + public partial class MangaYearNullable : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "Year", + table: "Mangas", + type: "bigint", + nullable: true, + oldClrType: typeof(long), + oldType: "bigint"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + 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..7b86671 --- /dev/null +++ b/API/Migrations/pgsql/20250516122242_AltTitle-Owned-WithId.Designer.cs @@ -0,0 +1,689 @@ +// +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 + { + /// + 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("AuthorId") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("AuthorName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.HasKey("AuthorId"); + + b.ToTable("Authors"); + }); + + modelBuilder.Entity("API.Schema.Chapter", b => + { + b.Property("ChapterId") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("ChapterNumber") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.Property("Downloaded") + .HasColumnType("boolean"); + + b.Property("FileName") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("ParentMangaId") + .IsRequired() + .HasColumnType("character varying(64)"); + + b.Property("Title") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Url") + .IsRequired() + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("VolumeNumber") + .HasColumnType("integer"); + + b.HasKey("ChapterId"); + + b.HasIndex("ParentMangaId"); + + b.ToTable("Chapters"); + }); + + modelBuilder.Entity("API.Schema.Jobs.Job", b => + { + b.Property("JobId") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("JobType") + .HasColumnType("smallint"); + + b.Property("LastExecution") + .HasColumnType("timestamp with time zone"); + + b.Property("ParentJobId") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("RecurrenceMs") + .HasColumnType("numeric(20,0)"); + + b.Property("state") + .HasColumnType("smallint"); + + b.HasKey("JobId"); + + b.HasIndex("ParentJobId"); + + b.ToTable("Jobs"); + + b.HasDiscriminator("JobType"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("API.Schema.LocalLibrary", b => + { + b.Property("LocalLibraryId") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("BasePath") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("LibraryName") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.HasKey("LocalLibraryId"); + + b.ToTable("LocalLibraries"); + }); + + modelBuilder.Entity("API.Schema.Manga", b => + { + b.Property("MangaId") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("CoverFileNameInCache") + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("CoverUrl") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("DirectoryName") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("IdOnConnectorSite") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IgnoreChaptersBefore") + .HasColumnType("real"); + + b.Property("LibraryId") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("MangaConnectorName") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("OriginalLanguage") + .HasMaxLength(8) + .HasColumnType("character varying(8)"); + + b.Property("ReleaseStatus") + .HasColumnType("smallint"); + + b.Property("WebsiteUrl") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("Year") + .HasColumnType("bigint"); + + b.HasKey("MangaId"); + + b.HasIndex("LibraryId"); + + b.HasIndex("MangaConnectorName"); + + b.ToTable("Mangas"); + }); + + modelBuilder.Entity("API.Schema.MangaConnectors.MangaConnector", b => + { + b.Property("Name") + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.PrimitiveCollection("BaseUris") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("text[]"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("IconUrl") + .IsRequired() + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.PrimitiveCollection("SupportedLanguages") + .IsRequired() + .HasMaxLength(8) + .HasColumnType("text[]"); + + b.HasKey("Name"); + + b.ToTable("MangaConnectors"); + + b.HasDiscriminator("Name").HasValue("MangaConnector"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("API.Schema.MangaTag", b => + { + b.Property("Tag") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasKey("Tag"); + + b.ToTable("Tags"); + }); + + modelBuilder.Entity("AuthorToManga", b => + { + b.Property("AuthorIds") + .HasColumnType("character varying(64)"); + + b.Property("MangaIds") + .HasColumnType("character varying(64)"); + + b.HasKey("AuthorIds", "MangaIds"); + + b.HasIndex("MangaIds"); + + b.ToTable("AuthorToManga"); + }); + + modelBuilder.Entity("JobJob", b => + { + b.Property("DependsOnJobsJobId") + .HasColumnType("character varying(64)"); + + b.Property("JobId") + .HasColumnType("character varying(64)"); + + b.HasKey("DependsOnJobsJobId", "JobId"); + + b.HasIndex("JobId"); + + b.ToTable("JobJob"); + }); + + modelBuilder.Entity("MangaTagToManga", b => + { + b.Property("MangaTagIds") + .HasColumnType("character varying(64)"); + + b.Property("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("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("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("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("FromLocation") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("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("MangaId") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("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("Language") + .IsRequired() + .HasMaxLength(8) + .HasColumnType("character varying(8)"); + + b.Property("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("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("LinkId") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b1.Property("LinkProvider") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b1.Property("LinkUrl") + .IsRequired() + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b1.Property("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("AltTitleId") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b1.Property("Language") + .IsRequired() + .HasMaxLength(8) + .HasColumnType("character varying(8)"); + + b1.Property("MangaId") + .IsRequired() + .HasColumnType("character varying(64)"); + + b1.Property("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.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 +{ + /// + public partial class AltTitleOwnedWithId : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropPrimaryKey( + name: "PK_MangaAltTitle", + table: "MangaAltTitle"); + + migrationBuilder.DropColumn( + name: "Id", + table: "MangaAltTitle"); + + migrationBuilder.AddColumn( + 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"); + } + + /// + 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( + 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/20250401162026_dev-010425-2-Longer_Var_Chars.Designer.cs b/API/Migrations/pgsql/20250516180953_Split-UpdateChaptersDownloadedJob-Into-UpdateSingleChapterDownloadedJob.Designer.cs similarity index 66% rename from API/Migrations/20250401162026_dev-010425-2-Longer_Var_Chars.Designer.cs rename to API/Migrations/pgsql/20250516180953_Split-UpdateChaptersDownloadedJob-Into-UpdateSingleChapterDownloadedJob.Designer.cs index e71924e..effec49 100644 --- a/API/Migrations/20250401162026_dev-010425-2-Longer_Var_Chars.Designer.cs +++ b/API/Migrations/pgsql/20250516180953_Split-UpdateChaptersDownloadedJob-Into-UpdateSingleChapterDownloadedJob.Designer.cs @@ -1,7 +1,6 @@ // 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("20250401162026_dev-010425-2-Longer_Var_Chars")] - partial class dev0104252Longer_Var_Chars + [Migration("20250516180953_Split-UpdateChaptersDownloadedJob-Into-UpdateSingleChapterDownloadedJob")] + partial class SplitUpdateChaptersDownloadedJobIntoUpdateSingleChapterDownloadedJob { /// 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 => @@ -64,7 +62,6 @@ namespace API.Migrations b.Property("ParentMangaId") .IsRequired() - .HasMaxLength(64) .HasColumnType("character varying(64)"); b.Property("Title") @@ -92,10 +89,6 @@ namespace API.Migrations .HasMaxLength(64) .HasColumnType("character varying(64)"); - b.PrimitiveCollection("DependsOnJobsIds") - .HasMaxLength(64) - .HasColumnType("text[]"); - b.Property("Enabled") .HasColumnType("boolean"); @@ -126,60 +119,6 @@ namespace API.Migrations b.UseTphMappingStrategy(); }); - modelBuilder.Entity("API.Schema.LibraryConnectors.LibraryConnector", b => - { - b.Property("LibraryConnectorId") - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("Auth") - .IsRequired() - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.Property("BaseUrl") - .IsRequired() - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.Property("LibraryType") - .HasColumnType("smallint"); - - b.HasKey("LibraryConnectorId"); - - b.ToTable("LibraryConnectors"); - - b.HasDiscriminator("LibraryType"); - - b.UseTphMappingStrategy(); - }); - - modelBuilder.Entity("API.Schema.Link", b => - { - b.Property("LinkId") - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("LinkProvider") - .IsRequired() - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("LinkUrl") - .IsRequired() - .HasMaxLength(2048) - .HasColumnType("character varying(2048)"); - - b.Property("MangaId") - .HasColumnType("character varying(64)"); - - b.HasKey("LinkId"); - - b.HasIndex("MangaId"); - - b.ToTable("Links"); - }); - modelBuilder.Entity("API.Schema.LocalLibrary", b => { b.Property("LocalLibraryId") @@ -208,11 +147,13 @@ namespace API.Migrations .HasColumnType("character varying(64)"); b.Property("CoverFileNameInCache") - .HasColumnType("text"); + .HasMaxLength(512) + .HasColumnType("character varying(512)"); b.Property("CoverUrl") .IsRequired() - .HasColumnType("text"); + .HasMaxLength(512) + .HasColumnType("character varying(512)"); b.Property("Description") .IsRequired() @@ -228,17 +169,18 @@ namespace API.Migrations .HasMaxLength(256) .HasColumnType("character varying(256)"); - b.Property("IgnoreChapterBefore") + b.Property("IgnoreChaptersBefore") .HasColumnType("real"); - b.Property("LibraryLocalLibraryId") - .HasColumnType("character varying(64)"); - - b.Property("MangaConnectorId") - .IsRequired() + b.Property("LibraryId") .HasMaxLength(64) .HasColumnType("character varying(64)"); + b.Property("MangaConnectorName") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + b.Property("Name") .IsRequired() .HasMaxLength(512) @@ -256,44 +198,18 @@ namespace API.Migrations .HasMaxLength(512) .HasColumnType("character varying(512)"); - b.Property("Year") + b.Property("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("AltTitleId") - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("Language") - .IsRequired() - .HasMaxLength(8) - .HasColumnType("character varying(8)"); - - b.Property("MangaId") - .HasColumnType("character varying(64)"); - - b.Property("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("Name") @@ -338,76 +254,19 @@ namespace API.Migrations b.ToTable("Tags"); }); - modelBuilder.Entity("API.Schema.Notification", b => + modelBuilder.Entity("AuthorToManga", b => { - b.Property("NotificationId") - .HasMaxLength(64) + b.Property("AuthorIds") .HasColumnType("character varying(64)"); - b.Property("Date") - .HasColumnType("timestamp with time zone"); - - b.Property("Message") - .IsRequired() - .HasMaxLength(512) - .HasColumnType("character varying(512)"); - - b.Property("Title") - .IsRequired() - .HasMaxLength(128) - .HasColumnType("character varying(128)"); - - b.Property("Urgency") - .HasColumnType("smallint"); - - b.HasKey("NotificationId"); - - b.ToTable("Notifications"); - }); - - modelBuilder.Entity("API.Schema.NotificationConnectors.NotificationConnector", b => - { - b.Property("Name") - .HasMaxLength(64) + b.Property("MangaIds") .HasColumnType("character varying(64)"); - b.Property("Body") - .IsRequired() - .HasMaxLength(4096) - .HasColumnType("character varying(4096)"); + b.HasKey("AuthorIds", "MangaIds"); - b.Property>("Headers") - .IsRequired() - .HasColumnType("hstore"); + b.HasIndex("MangaIds"); - b.Property("HttpMethod") - .IsRequired() - .HasMaxLength(8) - .HasColumnType("character varying(8)"); - - b.Property("Url") - .IsRequired() - .HasMaxLength(2048) - .HasColumnType("character varying(2048)"); - - b.HasKey("Name"); - - b.ToTable("NotificationConnectors"); - }); - - modelBuilder.Entity("AuthorManga", b => - { - b.Property("AuthorsAuthorId") - .HasColumnType("character varying(64)"); - - b.Property("MangaId") - .HasColumnType("character varying(64)"); - - b.HasKey("AuthorsAuthorId", "MangaId"); - - b.HasIndex("MangaId"); - - b.ToTable("AuthorManga"); + b.ToTable("AuthorToManga"); }); modelBuilder.Entity("JobJob", b => @@ -425,19 +284,19 @@ namespace API.Migrations b.ToTable("JobJob"); }); - modelBuilder.Entity("MangaMangaTag", b => + modelBuilder.Entity("MangaTagToManga", b => { - b.Property("MangaId") + b.Property("MangaTagIds") .HasColumnType("character varying(64)"); - b.Property("MangaTagsTag") + b.Property("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 +364,42 @@ namespace API.Migrations b.HasDiscriminator().HasValue((byte)3); }); + modelBuilder.Entity("API.Schema.Jobs.MoveMangaLibraryJob", b => + { + b.HasBaseType("API.Schema.Jobs.Job"); + + b.Property("MangaId") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("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("Language") + .IsRequired() + .HasMaxLength(8) + .HasColumnType("character varying(8)"); + b.Property("MangaId") .IsRequired() .HasMaxLength(64) @@ -525,7 +416,7 @@ namespace API.Migrations 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"); @@ -539,58 +430,37 @@ namespace API.Migrations b.ToTable("Jobs", t => { t.Property("MangaId") - .HasColumnName("UpdateFilesDownloadedJob_MangaId"); + .HasColumnName("UpdateChaptersDownloadedJob_MangaId"); }); b.HasDiscriminator().HasValue((byte)6); }); - modelBuilder.Entity("API.Schema.Jobs.UpdateMetadataJob", b => + modelBuilder.Entity("API.Schema.Jobs.UpdateSingleChapterDownloadedJob", b => { b.HasBaseType("API.Schema.Jobs.Job"); - b.Property("MangaId") + b.Property("ChapterId") .IsRequired() .HasMaxLength(64) .HasColumnType("character varying(64)"); - b.HasIndex("MangaId"); + b.HasIndex("ChapterId"); b.ToTable("Jobs", t => { - t.Property("MangaId") - .HasColumnName("UpdateMetadataJob_MangaId"); + t.Property("ChapterId") + .HasColumnName("UpdateSingleChapterDownloadedJob_ChapterId"); }); - b.HasDiscriminator().HasValue((byte)2); + b.HasDiscriminator().HasValue((byte)8); }); - 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 => + modelBuilder.Entity("API.Schema.MangaConnectors.ComickIo", 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("ComickIo"); }); modelBuilder.Entity("API.Schema.MangaConnectors.Global", b => @@ -607,52 +477,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 +498,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("LinkId") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b1.Property("LinkProvider") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b1.Property("LinkUrl") + .IsRequired() + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b1.Property("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("AltTitleId") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b1.Property("Language") + .IsRequired() + .HasMaxLength(8) + .HasColumnType("character varying(8)"); + + b1.Property("MangaId") + .IsRequired() + .HasColumnType("character varying(64)"); + + b1.Property("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(); }); @@ -734,17 +610,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 +658,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") @@ -793,7 +688,7 @@ namespace API.Migrations b.Navigation("Manga"); }); - modelBuilder.Entity("API.Schema.Jobs.UpdateFilesDownloadedJob", b => + modelBuilder.Entity("API.Schema.Jobs.UpdateChaptersDownloadedJob", b => { b.HasOne("API.Schema.Manga", "Manga") .WithMany() @@ -804,22 +699,20 @@ namespace API.Migrations b.Navigation("Manga"); }); - modelBuilder.Entity("API.Schema.Jobs.UpdateMetadataJob", b => + modelBuilder.Entity("API.Schema.Jobs.UpdateSingleChapterDownloadedJob", b => { - b.HasOne("API.Schema.Manga", "Manga") + b.HasOne("API.Schema.Chapter", "Chapter") .WithMany() - .HasForeignKey("MangaId") + .HasForeignKey("ChapterId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.Navigation("Manga"); + b.Navigation("Chapter"); }); 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/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 +{ + /// + public partial class SplitUpdateChaptersDownloadedJobIntoUpdateSingleChapterDownloadedJob : Migration + { + /// + 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( + 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); + } + + /// + 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/PgsqlContextModelSnapshot.cs b/API/Migrations/pgsql/PgsqlContextModelSnapshot.cs similarity index 66% rename from API/Migrations/PgsqlContextModelSnapshot.cs rename to API/Migrations/pgsql/PgsqlContextModelSnapshot.cs index c290382..9153829 100644 --- a/API/Migrations/PgsqlContextModelSnapshot.cs +++ b/API/Migrations/pgsql/PgsqlContextModelSnapshot.cs @@ -1,7 +1,6 @@ // 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 => @@ -61,7 +59,6 @@ namespace API.Migrations b.Property("ParentMangaId") .IsRequired() - .HasMaxLength(64) .HasColumnType("character varying(64)"); b.Property("Title") @@ -89,10 +86,6 @@ namespace API.Migrations .HasMaxLength(64) .HasColumnType("character varying(64)"); - b.PrimitiveCollection("DependsOnJobsIds") - .HasMaxLength(64) - .HasColumnType("text[]"); - b.Property("Enabled") .HasColumnType("boolean"); @@ -123,60 +116,6 @@ namespace API.Migrations b.UseTphMappingStrategy(); }); - modelBuilder.Entity("API.Schema.LibraryConnectors.LibraryConnector", b => - { - b.Property("LibraryConnectorId") - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("Auth") - .IsRequired() - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.Property("BaseUrl") - .IsRequired() - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.Property("LibraryType") - .HasColumnType("smallint"); - - b.HasKey("LibraryConnectorId"); - - b.ToTable("LibraryConnectors"); - - b.HasDiscriminator("LibraryType"); - - b.UseTphMappingStrategy(); - }); - - modelBuilder.Entity("API.Schema.Link", b => - { - b.Property("LinkId") - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("LinkProvider") - .IsRequired() - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("LinkUrl") - .IsRequired() - .HasMaxLength(2048) - .HasColumnType("character varying(2048)"); - - b.Property("MangaId") - .HasColumnType("character varying(64)"); - - b.HasKey("LinkId"); - - b.HasIndex("MangaId"); - - b.ToTable("Links"); - }); - modelBuilder.Entity("API.Schema.LocalLibrary", b => { b.Property("LocalLibraryId") @@ -205,11 +144,13 @@ namespace API.Migrations .HasColumnType("character varying(64)"); b.Property("CoverFileNameInCache") - .HasColumnType("text"); + .HasMaxLength(512) + .HasColumnType("character varying(512)"); b.Property("CoverUrl") .IsRequired() - .HasColumnType("text"); + .HasMaxLength(512) + .HasColumnType("character varying(512)"); b.Property("Description") .IsRequired() @@ -225,17 +166,18 @@ namespace API.Migrations .HasMaxLength(256) .HasColumnType("character varying(256)"); - b.Property("IgnoreChapterBefore") + b.Property("IgnoreChaptersBefore") .HasColumnType("real"); - b.Property("LibraryLocalLibraryId") - .HasColumnType("character varying(64)"); - - b.Property("MangaConnectorId") - .IsRequired() + b.Property("LibraryId") .HasMaxLength(64) .HasColumnType("character varying(64)"); + b.Property("MangaConnectorName") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + b.Property("Name") .IsRequired() .HasMaxLength(512) @@ -253,44 +195,18 @@ namespace API.Migrations .HasMaxLength(512) .HasColumnType("character varying(512)"); - b.Property("Year") + b.Property("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("AltTitleId") - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("Language") - .IsRequired() - .HasMaxLength(8) - .HasColumnType("character varying(8)"); - - b.Property("MangaId") - .HasColumnType("character varying(64)"); - - b.Property("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("Name") @@ -335,76 +251,19 @@ namespace API.Migrations b.ToTable("Tags"); }); - modelBuilder.Entity("API.Schema.Notification", b => + modelBuilder.Entity("AuthorToManga", b => { - b.Property("NotificationId") - .HasMaxLength(64) + b.Property("AuthorIds") .HasColumnType("character varying(64)"); - b.Property("Date") - .HasColumnType("timestamp with time zone"); - - b.Property("Message") - .IsRequired() - .HasMaxLength(512) - .HasColumnType("character varying(512)"); - - b.Property("Title") - .IsRequired() - .HasMaxLength(128) - .HasColumnType("character varying(128)"); - - b.Property("Urgency") - .HasColumnType("smallint"); - - b.HasKey("NotificationId"); - - b.ToTable("Notifications"); - }); - - modelBuilder.Entity("API.Schema.NotificationConnectors.NotificationConnector", b => - { - b.Property("Name") - .HasMaxLength(64) + b.Property("MangaIds") .HasColumnType("character varying(64)"); - b.Property("Body") - .IsRequired() - .HasMaxLength(4096) - .HasColumnType("character varying(4096)"); + b.HasKey("AuthorIds", "MangaIds"); - b.Property>("Headers") - .IsRequired() - .HasColumnType("hstore"); + b.HasIndex("MangaIds"); - b.Property("HttpMethod") - .IsRequired() - .HasMaxLength(8) - .HasColumnType("character varying(8)"); - - b.Property("Url") - .IsRequired() - .HasMaxLength(2048) - .HasColumnType("character varying(2048)"); - - b.HasKey("Name"); - - b.ToTable("NotificationConnectors"); - }); - - modelBuilder.Entity("AuthorManga", b => - { - b.Property("AuthorsAuthorId") - .HasColumnType("character varying(64)"); - - b.Property("MangaId") - .HasColumnType("character varying(64)"); - - b.HasKey("AuthorsAuthorId", "MangaId"); - - b.HasIndex("MangaId"); - - b.ToTable("AuthorManga"); + b.ToTable("AuthorToManga"); }); modelBuilder.Entity("JobJob", b => @@ -422,19 +281,19 @@ namespace API.Migrations b.ToTable("JobJob"); }); - modelBuilder.Entity("MangaMangaTag", b => + modelBuilder.Entity("MangaTagToManga", b => { - b.Property("MangaId") + b.Property("MangaTagIds") .HasColumnType("character varying(64)"); - b.Property("MangaTagsTag") + b.Property("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 +305,8 @@ namespace API.Migrations .HasMaxLength(64) .HasColumnType("character varying(64)"); + b.HasIndex("MangaId"); + b.ToTable("Jobs", t => { t.Property("MangaId") @@ -464,6 +325,8 @@ namespace API.Migrations .HasMaxLength(64) .HasColumnType("character varying(64)"); + b.HasIndex("MangaId"); + b.HasDiscriminator().HasValue((byte)4); }); @@ -476,6 +339,8 @@ namespace API.Migrations .HasMaxLength(64) .HasColumnType("character varying(64)"); + b.HasIndex("ChapterId"); + b.HasDiscriminator().HasValue((byte)0); }); @@ -496,7 +361,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 +370,40 @@ namespace API.Migrations .HasMaxLength(64) .HasColumnType("character varying(64)"); + b.Property("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("Language") + .IsRequired() + .HasMaxLength(8) + .HasColumnType("character varying(8)"); + + b.Property("MangaId") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasIndex("MangaId"); + b.ToTable("Jobs", t => { t.Property("MangaId") @@ -514,25 +413,7 @@ namespace API.Migrations b.HasDiscriminator().HasValue((byte)5); }); - modelBuilder.Entity("API.Schema.Jobs.UpdateFilesDownloadedJob", b => - { - b.HasBaseType("API.Schema.Jobs.Job"); - - b.Property("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 => + modelBuilder.Entity("API.Schema.Jobs.UpdateChaptersDownloadedJob", b => { b.HasBaseType("API.Schema.Jobs.Job"); @@ -546,38 +427,37 @@ namespace API.Migrations b.ToTable("Jobs", t => { t.Property("MangaId") - .HasColumnName("UpdateMetadataJob_MangaId"); + .HasColumnName("UpdateChaptersDownloadedJob_MangaId"); }); - b.HasDiscriminator().HasValue((byte)2); + b.HasDiscriminator().HasValue((byte)6); }); - modelBuilder.Entity("API.Schema.LibraryConnectors.Kavita", b => + modelBuilder.Entity("API.Schema.Jobs.UpdateSingleChapterDownloadedJob", b => { - b.HasBaseType("API.Schema.LibraryConnectors.LibraryConnector"); + b.HasBaseType("API.Schema.Jobs.Job"); - b.HasDiscriminator().HasValue((byte)1); + b.Property("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.LibraryConnectors.Komga", b => - { - b.HasBaseType("API.Schema.LibraryConnectors.LibraryConnector"); - - b.HasDiscriminator().HasValue((byte)0); - }); - - modelBuilder.Entity("API.Schema.MangaConnectors.AsuraToon", b => + modelBuilder.Entity("API.Schema.MangaConnectors.ComickIo", 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("ComickIo"); }); modelBuilder.Entity("API.Schema.MangaConnectors.Global", b => @@ -594,52 +474,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 +495,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("LinkId") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b1.Property("LinkProvider") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b1.Property("LinkUrl") + .IsRequired() + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b1.Property("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("AltTitleId") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b1.Property("Language") + .IsRequired() + .HasMaxLength(8) + .HasColumnType("character varying(8)"); + + b1.Property("MangaId") + .IsRequired() + .HasColumnType("character varying(64)"); + + b1.Property("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 +607,22 @@ 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() @@ -747,11 +633,83 @@ namespace API.Migrations 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("AltTitles"); - - b.Navigation("Links"); + b.Navigation("Chapters"); }); #pragma warning restore 612, 618 } diff --git a/API/Program.cs b/API/Program.cs index 3e3af0b..ef4da11 100644 --- a/API/Program.cs +++ b/API/Program.cs @@ -1,12 +1,13 @@ 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; using Asp.Versioning.Builder; using Asp.Versioning.Conventions; +using log4net; using Microsoft.EntityFrameworkCore; using Newtonsoft.Json; using Newtonsoft.Json.Converters; @@ -54,11 +55,17 @@ builder.Services.AddSwaggerGen(opt => }); builder.Services.ConfigureOptions(); +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(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(options => + options.UseNpgsql(ConnectionString)); +builder.Services.AddDbContext(options => + options.UseNpgsql(ConnectionString)); builder.Services.AddControllers(options => { @@ -69,6 +76,7 @@ builder.Services.AddControllers().AddNewtonsoftJson(opts => opts.SerializerSettings.Converters.Add(new StringEnumConverter()); opts.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore; }); +builder.Services.AddScoped(opts => LogManager.GetLogger("API")); builder.WebHost.UseUrls("http://*:6531"); @@ -97,38 +105,40 @@ app.UseHttpsRedirection(); app.UseMiddleware(); -using (var scope = app.Services.CreateScope()) +using (IServiceScope scope = app.Services.CreateScope()) { - var db = scope.ServiceProvider.GetRequiredService(); - db.Database.Migrate(); -} - -using (var scope = app.Services.CreateScope()) -{ - PgsqlContext context = scope.ServiceProvider.GetService()!; + PgsqlContext context = scope.ServiceProvider.GetRequiredService(); + context.Database.Migrate(); 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()!) - ]; + [ + new MangaDex(), + new ComickIo(), + new Global(scope.ServiceProvider.GetService()!) + ]; 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.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.Jobs.AddRange(context.Jobs.Where(j => j.JobType == JobType.DownloadAvailableChaptersJob) + .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)) + { + job.state = JobState.FirstExecution; + job.LastExecution = DateTime.UnixEpoch; + } + + context.SaveChanges(); +} + +using (IServiceScope scope = app.Services.CreateScope()) +{ + NotificationsContext context = scope.ServiceProvider.GetRequiredService(); + 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)); @@ -140,7 +150,7 @@ using (var 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/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/Chapter.cs b/API/Schema/Chapter.cs index b844c87..75fec12 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 { - 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; } + [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; + } + + /// + /// EF ONLY!!! + /// + 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 }; } - 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; - } - /// /// Checks the filesystem if an archive at the ArchiveFilePath exists /// /// True if archive exists on disk - 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 /// %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 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 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 }; @@ -215,4 +181,9 @@ public class Chapter : IComparable ); 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/Contexts/LibraryContext.cs b/API/Schema/Contexts/LibraryContext.cs new file mode 100644 index 0000000..bbe9e3c --- /dev/null +++ b/API/Schema/Contexts/LibraryContext.cs @@ -0,0 +1,32 @@ +using API.Schema.LibraryConnectors; +using log4net; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Diagnostics; + +namespace API.Schema.Contexts; + +public class LibraryContext(DbContextOptions options) : DbContext(options) +{ + public DbSet 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 + modelBuilder.Entity() + .HasDiscriminator(l => l.LibraryType) + .HasValue(LibraryType.Komga) + .HasValue(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..8fe9454 --- /dev/null +++ b/API/Schema/Contexts/NotificationsContext.cs @@ -0,0 +1,23 @@ +using API.Schema.NotificationConnectors; +using log4net; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Diagnostics; + +namespace API.Schema.Contexts; + +public class NotificationsContext(DbContextOptions options) : DbContext(options) +{ + public DbSet NotificationConnectors { get; set; } + public DbSet 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 new file mode 100644 index 0000000..8fc0b8a --- /dev/null +++ b/API/Schema/Contexts/PgsqlContext.cs @@ -0,0 +1,204 @@ +using API.Schema.Jobs; +using API.Schema.MangaConnectors; +using log4net; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Diagnostics; + +namespace API.Schema.Contexts; + +public class PgsqlContext(DbContextOptions options) : DbContext(options) +{ + public DbSet Jobs { get; set; } + public DbSet MangaConnectors { get; set; } + public DbSet Mangas { get; set; } + public DbSet LocalLibraries { get; set; } + public DbSet Chapters { get; set; } + public DbSet Authors { get; set; } + public DbSet 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 + modelBuilder.Entity() + .HasDiscriminator(j => j.JobType) + .HasValue(JobType.MoveFileOrFolderJob) + .HasValue(JobType.MoveMangaLibraryJob) + .HasValue(JobType.DownloadAvailableChaptersJob) + .HasValue(JobType.DownloadSingleChapterJob) + .HasValue(JobType.DownloadMangaCoverJob) + .HasValue(JobType.RetrieveChaptersJob) + .HasValue(JobType.UpdateChaptersDownloadedJob) + .HasValue(JobType.UpdateSingleChapterDownloadedJob); + + //Job specification + modelBuilder.Entity() + .HasOne(j => j.Manga) + .WithMany() + .HasForeignKey(j => j.MangaId) + .IsRequired() + .OnDelete(DeleteBehavior.Cascade); + modelBuilder.Entity() + .Navigation(j => j.Manga) + .EnableLazyLoading(); + modelBuilder.Entity() + .HasOne(j => j.Manga) + .WithMany() + .HasForeignKey(j => j.MangaId) + .IsRequired() + .OnDelete(DeleteBehavior.Cascade); + modelBuilder.Entity() + .Navigation(j => j.Manga) + .EnableLazyLoading(); + modelBuilder.Entity() + .HasOne(j => j.Chapter) + .WithMany() + .HasForeignKey(j => j.ChapterId) + .IsRequired() + .OnDelete(DeleteBehavior.Cascade); + modelBuilder.Entity() + .Navigation(j => j.Chapter) + .EnableLazyLoading(); + modelBuilder.Entity() + .HasOne(j => j.Manga) + .WithMany() + .HasForeignKey(j => j.MangaId) + .IsRequired() + .OnDelete(DeleteBehavior.Cascade); + modelBuilder.Entity() + .Navigation(j => j.Manga) + .EnableLazyLoading(); + modelBuilder.Entity() + .HasOne(j => j.ToLibrary) + .WithMany() + .HasForeignKey(j => j.ToLibraryId) + .IsRequired() + .OnDelete(DeleteBehavior.Cascade); + modelBuilder.Entity() + .Navigation(j => j.ToLibrary) + .EnableLazyLoading(); + modelBuilder.Entity() + .HasOne(j => j.Manga) + .WithMany() + .HasForeignKey(j => j.MangaId) + .IsRequired() + .OnDelete(DeleteBehavior.Cascade); + modelBuilder.Entity() + .Navigation(j => j.Manga) + .EnableLazyLoading(); + modelBuilder.Entity() + .HasOne(j => j.Manga) + .WithMany() + .HasForeignKey(j => j.MangaId) + .IsRequired() + .OnDelete(DeleteBehavior.Cascade); + modelBuilder.Entity() + .Navigation(j => j.Manga) + .EnableLazyLoading(); + + //Job has possible ParentJob + modelBuilder.Entity() + .HasOne(childJob => childJob.ParentJob) + .WithMany() + .HasForeignKey(childjob => childjob.ParentJobId) + .OnDelete(DeleteBehavior.Cascade); + //Job might be dependent on other Jobs + modelBuilder.Entity() + .HasMany(root => root.DependsOnJobs) + .WithMany(); + modelBuilder.Entity() + .Navigation(j => j.DependsOnJobs) + .AutoInclude(false) + .EnableLazyLoading(); + + //MangaConnector Types + modelBuilder.Entity() + .HasDiscriminator(c => c.Name) + .HasValue("Global") + .HasValue("MangaDex") + .HasValue("ComickIo"); + //MangaConnector is responsible for many Manga + modelBuilder.Entity() + .HasMany() + .WithOne(m => m.MangaConnector) + .HasForeignKey(m => m.MangaConnectorName) + .IsRequired() + .OnDelete(DeleteBehavior.Cascade); + modelBuilder.Entity() + .Navigation(m => m.MangaConnector) + .AutoInclude(); + + //Manga has many Chapters + modelBuilder.Entity() + .HasMany(m => m.Chapters) + .WithOne(c => c.ParentManga) + .HasForeignKey(c => c.ParentMangaId) + .IsRequired() + .OnDelete(DeleteBehavior.Cascade); + modelBuilder.Entity() + .Navigation(c => c.ParentManga) + .AutoInclude(); + modelBuilder.Entity() + .Navigation(m => m.Chapters) + .AutoInclude(false) + .EnableLazyLoading(); + //Manga owns MangaAltTitles + modelBuilder.Entity() + .OwnsMany(m => m.AltTitles) + .WithOwner(); + modelBuilder.Entity() + .Navigation(m => m.AltTitles) + .AutoInclude(); + //Manga owns Links + modelBuilder.Entity() + .OwnsMany(m => m.Links) + .WithOwner(); + modelBuilder.Entity() + .Navigation(m => m.Links) + .AutoInclude(); + //Manga has many Tags associated with many Manga + modelBuilder.Entity() + .HasMany(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") + ); + modelBuilder.Entity() + .Navigation(m => m.MangaTags) + .AutoInclude(); + //Manga has many Authors associated with many Manga + modelBuilder.Entity() + .HasMany(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") + ); + modelBuilder.Entity() + .Navigation(m => m.Authors) + .AutoInclude(); + + //LocalLibrary has many Mangas + modelBuilder.Entity() + .HasMany() + .WithOne(m => m.Library) + .HasForeignKey(m => m.LibraryId) + .OnDelete(DeleteBehavior.SetNull); + modelBuilder.Entity() + .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 e3ae719..e4d2eed 100644 --- a/API/Schema/Jobs/DownloadAvailableChaptersJob.cs +++ b/API/Schema/Jobs/DownloadAvailableChaptersJob.cs @@ -1,17 +1,41 @@ using System.ComponentModel.DataAnnotations; +using API.Schema.Contexts; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Newtonsoft.Json; namespace API.Schema.Jobs; -public class DownloadAvailableChaptersJob(ulong recurrenceMs, string mangaId, string? parentJobId = null, ICollection? 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; } + + 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? dependsOnJobs = null) + : base(TokenGen.CreateToken(typeof(DownloadAvailableChaptersJob)), JobType.DownloadAvailableChaptersJob, recurrenceMs, parentJob, dependsOnJobs) + { + this.MangaId = manga.MangaId; + this.Manga = manga; + } + + /// + /// EF ONLY!!! + /// + internal DownloadAvailableChaptersJob(ILazyLoader lazyLoader, string mangaId, ulong recurrenceMs, string? parentJobId) + : base(lazyLoader, TokenGen.CreateToken(typeof(DownloadAvailableChaptersJob)), JobType.DownloadAvailableChaptersJob, recurrenceMs, parentJobId) + { + this.MangaId = mangaId; + } protected override IEnumerable RunInternal(PgsqlContext context) { - return context.Chapters.Where(c => c.ParentMangaId == MangaId).AsEnumerable() - .Select(chapter => new DownloadSingleChapterJob(chapter.ChapterId, this.JobId)); + return Manga.Chapters.Where(c => c.Downloaded == false).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..f41126c 100644 --- a/API/Schema/Jobs/DownloadMangaCoverJob.cs +++ b/API/Schema/Jobs/DownloadMangaCoverJob.cs @@ -1,26 +1,51 @@ using System.ComponentModel.DataAnnotations; +using API.Schema.Contexts; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Newtonsoft.Json; namespace API.Schema.Jobs; -public class DownloadMangaCoverJob(string mangaId, string? parentJobId = null, ICollection? 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; } + + 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? dependsOnJobs = null) + : base(TokenGen.CreateToken(typeof(DownloadMangaCoverJob)), JobType.DownloadMangaCoverJob, 0, parentJob, dependsOnJobs) + { + this.MangaId = manga.MangaId; + this.Manga = manga; + } + + /// + /// EF ONLY!!! + /// + internal DownloadMangaCoverJob(ILazyLoader lazyLoader, string mangaId, string? parentJobId) + : base(lazyLoader, TokenGen.CreateToken(typeof(DownloadMangaCoverJob)), JobType.DownloadMangaCoverJob, 0, parentJobId) + { + this.MangaId = mangaId; + } protected override IEnumerable 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..564d181 100644 --- a/API/Schema/Jobs/DownloadSingleChapterJob.cs +++ b/API/Schema/Jobs/DownloadSingleChapterJob.cs @@ -2,7 +2,10 @@ using System.IO.Compression; using System.Runtime.InteropServices; using API.MangaDownloadClients; -using API.Schema.MangaConnectors; +using API.Schema.Contexts; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Newtonsoft.Json; using SixLabors.ImageSharp; using SixLabors.ImageSharp.Formats.Jpeg; using SixLabors.ImageSharp.Processing; @@ -11,45 +14,44 @@ using static System.IO.UnixFileMode; namespace API.Schema.Jobs; -public class DownloadSingleChapterJob(string chapterId, string? parentJobId = null, ICollection? 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; } + + 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? dependsOnJobs = null) + : base(TokenGen.CreateToken(typeof(DownloadSingleChapterJob)), JobType.DownloadSingleChapterJob, 0, parentJob, dependsOnJobs) + { + this.ChapterId = chapter.ChapterId; + this.Chapter = chapter; + } + + /// + /// EF ONLY!!! + /// + internal DownloadSingleChapterJob(ILazyLoader lazyLoader, string chapterId, string? parentJobId) + : base(lazyLoader, TokenGen.CreateToken(typeof(DownloadSingleChapterJob)), JobType.DownloadSingleChapterJob, 0, parentJobId) + { + this.ChapterId = chapterId; + } protected override IEnumerable 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 +90,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 +102,19 @@ 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)]; + if (context.Jobs.ToList().Any(j => + { + if (j.JobType != JobType.UpdateChaptersDownloadedJob) + return false; + UpdateChaptersDownloadedJob job = (UpdateChaptersDownloadedJob)j; + return job.MangaId == this.Chapter.ParentMangaId; + })) + return []; + + return [new UpdateChaptersDownloadedJob(Chapter.ParentManga, 0, this.ParentJob)]; } private void ProcessImage(string imagePath) @@ -138,7 +149,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..ddb2774 100644 --- a/API/Schema/Jobs/Job.cs +++ b/API/Schema/Jobs/Job.cs @@ -1,7 +1,9 @@ using System.ComponentModel.DataAnnotations; 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; @@ -12,75 +14,100 @@ 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? DependsOnJobsIds { get; init; } - [JsonIgnore] - public ICollection? 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? dependsOnJobs = null) - : this(jobId, jobType, recurrenceMs, parentJob?.JobId, dependsOnJobs?.Select(j => j.JobId).ToList()) + [StringLength(64)] public string? ParentJobId { get; private set; } + [JsonIgnore] public Job? ParentJob { get; internal set; } + private ICollection _dependsOnJobs = null!; + [JsonIgnore] public ICollection DependsOnJobs { - this.ParentJob = parentJob; - this.DependsOnJobs = dependsOnJobs; + get => LazyLoader.Load(this, ref _dependsOnJobs); + init => _dependsOnJobs = value; } - public Job(string jobId, JobType jobType, ulong recurrenceMs, string? parentJobId = null, ICollection? dependsOnJobsIds = null) + [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; } + [NotMapped] [JsonIgnore] protected ILazyLoader LazyLoader { get; init; } + + protected Job(string jobId, JobType jobType, ulong recurrenceMs, Job? parentJob = null, ICollection? dependsOnJobs = null) { - 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 = parentJob?.JobId; + this.ParentJob = parentJob; + this.DependsOnJobs = dependsOnJobs ?? []; + + this.Log = LogManager.GetLogger(this.GetType()); + } + + /// + /// EF ONLY!!! + /// + 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; + this.ParentJobId = parentJobId; + this.DependsOnJobs = []; + + this.Log = LogManager.GetLogger(this.GetType()); } public IEnumerable Run(IServiceProvider serviceProvider) { - Log.Debug($"Running job {JobId}"); + Log.Info($"Running job {JobId}"); + DateTime jobStart = DateTime.UtcNow; + Job[]? ret = null; + using IServiceScope scope = serviceProvider.CreateScope(); PgsqlContext context = scope.ServiceProvider.GetRequiredService(); - try { + context.Attach(this); this.state = JobState.Running; context.SaveChanges(); - Job[] newJobs = RunInternal(context).ToArray(); + ret = RunInternal(context).ToArray(); this.state = JobState.Completed; - context.Jobs.AddRange(newJobs); + context.Jobs.AddRange(ret); + Log.Info($"Job {JobId} completed. Generated {ret.Length} new jobs."); context.SaveChanges(); - 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); - return []; + 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)"); + return ret ?? []; } protected abstract IEnumerable RunInternal(PgsqlContext context); + + public override string ToString() + { + return $"{JobId}"; + } } \ No newline at end of file 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/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/MoveFileOrFolderJob.cs b/API/Schema/Jobs/MoveFileOrFolderJob.cs index 0857ea8..dbd9c71 100644 --- a/API/Schema/Jobs/MoveFileOrFolderJob.cs +++ b/API/Schema/Jobs/MoveFileOrFolderJob.cs @@ -1,16 +1,34 @@ using System.ComponentModel.DataAnnotations; +using API.Schema.Contexts; +using Microsoft.EntityFrameworkCore.Infrastructure; namespace API.Schema.Jobs; -public class MoveFileOrFolderJob(string fromLocation, string toLocation, string? parentJobId = null, ICollection? 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? dependsOnJobs = null) + : base(TokenGen.CreateToken(typeof(MoveFileOrFolderJob)), JobType.MoveFileOrFolderJob, 0, parentJob, dependsOnJobs) + { + this.FromLocation = fromLocation; + this.ToLocation = toLocation; + } + + /// + /// EF ONLY!!! + /// + 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; + } protected override IEnumerable RunInternal(PgsqlContext context) { diff --git a/API/Schema/Jobs/MoveMangaLibraryJob.cs b/API/Schema/Jobs/MoveMangaLibraryJob.cs index 210bb28..365ee1f 100644 --- a/API/Schema/Jobs/MoveMangaLibraryJob.cs +++ b/API/Schema/Jobs/MoveMangaLibraryJob.cs @@ -1,35 +1,49 @@ using System.ComponentModel.DataAnnotations; +using API.Schema.Contexts; using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Newtonsoft.Json; namespace API.Schema.Jobs; -public class MoveMangaLibraryJob(string mangaId, string toLibraryId, string? parentJobId = null, ICollection? 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; } + + 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!; + + public MoveMangaLibraryJob(Manga manga, LocalLibrary toLibrary, Job? parentJob = null, ICollection? 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; + } + + /// + /// EF ONLY!!! + /// + 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; + } protected override IEnumerable 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 oldPath = chapters.ToDictionary(c => c, c => c.FullArchiveFilePath!); - manga.Library = library; + Dictionary oldPath = Manga.Chapters.ToDictionary(c => c, c => c.FullArchiveFilePath); + Manga.Library = ToLibrary; try { context.SaveChanges(); @@ -40,6 +54,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..56ee642 100644 --- a/API/Schema/Jobs/RetrieveChaptersJob.cs +++ b/API/Schema/Jobs/RetrieveChaptersJob.cs @@ -1,40 +1,52 @@ using System.ComponentModel.DataAnnotations; -using API.Schema.MangaConnectors; +using API.Schema.Contexts; using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Newtonsoft.Json; namespace API.Schema.Jobs; -public class RetrieveChaptersJob(ulong recurrenceMs, string mangaId, string? parentJobId = null, ICollection? 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; } + + 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? dependsOnJobs = null) + : base(TokenGen.CreateToken(typeof(RetrieveChaptersJob)), JobType.RetrieveChaptersJob, recurrenceMs, parentJob, dependsOnJobs) + { + this.MangaId = manga.MangaId; + this.Manga = manga; + this.Language = language; + } + + /// + /// EF ONLY!!! + /// + 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; + } protected override IEnumerable 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 => Manga.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/UpdateChaptersDownloadedJob.cs b/API/Schema/Jobs/UpdateChaptersDownloadedJob.cs new file mode 100644 index 0000000..41228ea --- /dev/null +++ b/API/Schema/Jobs/UpdateChaptersDownloadedJob.cs @@ -0,0 +1,41 @@ +using System.ComponentModel.DataAnnotations; +using API.Schema.Contexts; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Newtonsoft.Json; + +namespace API.Schema.Jobs; + +public class UpdateChaptersDownloadedJob : Job +{ + [StringLength(64)] [Required] public string MangaId { get; init; } + + 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? dependsOnJobs = null) + : base(TokenGen.CreateToken(typeof(UpdateChaptersDownloadedJob)), JobType.UpdateChaptersDownloadedJob, recurrenceMs, parentJob, dependsOnJobs) + { + this.MangaId = manga.MangaId; + this.Manga = manga; + } + + /// + /// EF ONLY!!! + /// + internal UpdateChaptersDownloadedJob(ILazyLoader lazyLoader, string mangaId, ulong recurrenceMs, string? parentJobId) + : base(lazyLoader, TokenGen.CreateToken(typeof(UpdateChaptersDownloadedJob)), JobType.UpdateChaptersDownloadedJob, recurrenceMs, parentJobId) + { + this.MangaId = mangaId; + } + + protected override IEnumerable RunInternal(PgsqlContext context) + { + 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 a68c3b6..0000000 --- a/API/Schema/Jobs/UpdateFilesDownloadedJob.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using Newtonsoft.Json; - -namespace API.Schema.Jobs; - -public class UpdateFilesDownloadedJob(ulong recurrenceMs, string mangaId, string? parentJobId = null, ICollection? dependsOnJobsIds = null) - : Job(TokenGen.CreateToken(typeof(UpdateFilesDownloadedJob)), JobType.UpdateFilesDownloadedJob, recurrenceMs, parentJobId, dependsOnJobsIds) -{ - [StringLength(64)] - [Required] - public string MangaId { get; init; } = mangaId; - - protected override IEnumerable RunInternal(PgsqlContext context) - { - IQueryable chapters = context.Chapters.Where(c => c.ParentMangaId == MangaId); - foreach (Chapter chapter in chapters) - chapter.Downloaded = chapter.IsDownloaded(); - - context.SaveChanges(); - 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? 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; } - - /// - /// Updates all data related to Manga. - /// Retrieves data from Mangaconnector - /// Updates Chapter-info - /// - /// - protected override IEnumerable RunInternal(PgsqlContext context) - { - Log.Warn("NOT IMPLEMENTED."); - return [];//TODO - } -} \ 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..8ff69bf --- /dev/null +++ b/API/Schema/Jobs/UpdateSingleChapterDownloadedJob.cs @@ -0,0 +1,52 @@ +using System.ComponentModel.DataAnnotations; +using API.Schema.Contexts; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Newtonsoft.Json; + +namespace API.Schema.Jobs; + +public class UpdateSingleChapterDownloadedJob : Job +{ + [StringLength(64)] [Required] public string ChapterId { get; init; } + + 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? dependsOnJobs = null) + : base(TokenGen.CreateToken(typeof(UpdateSingleChapterDownloadedJob)), JobType.UpdateSingleChapterDownloadedJob, 0, parentJob, dependsOnJobs) + { + this.ChapterId = chapter.ChapterId; + this.Chapter = chapter; + } + + /// + /// EF ONLY!!! + /// + internal UpdateSingleChapterDownloadedJob(ILazyLoader lazyLoader, string chapterId, string? parentJobId) + : base(lazyLoader, TokenGen.CreateToken(typeof(UpdateSingleChapterDownloadedJob)), JobType.UpdateSingleChapterDownloadedJob, 0, parentJobId) + { + this.ChapterId = chapterId; + } + + protected override IEnumerable RunInternal(PgsqlContext context) + { + 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/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 /// Array of KavitaLibrary private IEnumerable 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/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/Manga.cs b/API/Schema/Manga.cs index f635dfe..d2daf14 100644 --- a/API/Schema/Manga.cs +++ b/API/Schema/Manga.cs @@ -1,14 +1,10 @@ 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 Microsoft.EntityFrameworkCore.Infrastructure; using Newtonsoft.Json; using static System.IO.UnixFileMode; @@ -20,166 +16,146 @@ 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? Authors { get; internal set; } - [NotMapped] - [StringLength(64)] - [Required] - public IEnumerable AuthorIds => Authors?.Select(a => a.AuthorId) ?? []; + [StringLength(64)] + public string? LibraryId { get; init; } + [JsonIgnore] public LocalLibrary? Library { get; internal set; } - [JsonIgnore] public ICollection? MangaTags { get; internal set; } - [NotMapped] - [StringLength(64)] + [StringLength(32)] [Required] - public IEnumerable Tags => MangaTags?.Select(t => t.Tag) ?? []; - - - [JsonIgnore] public ICollection? Links { get; internal set; } - [NotMapped] - [StringLength(64)] - [Required] - public IEnumerable LinkIds => Links?.Select(l => l.LinkId) ?? []; - - [JsonIgnore] public ICollection? AltTitles { get; internal set; } - [NotMapped] - [StringLength(64)] - [Required] - public IEnumerable 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 authors, - ICollection mangaTags, ICollection links, ICollection altTitles, - LocalLibrary? library = null) - : this(idOnConnectorSite, name, description, websiteUrl, coverUrl, coverFileNameInCache, year, originalLanguage, - releaseStatus, ignoreChapterBefore, mangaConnector.Name) + public ICollection Authors { get; internal set; }= null!; + public ICollection MangaTags { get; internal set; }= null!; + public ICollection Links { get; internal set; }= null!; + public ICollection 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; + 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; + + [NotMapped] public ICollection ChapterIds => Chapters.Select(c => c.ChapterId).ToList(); + private readonly ILazyLoader _lazyLoader = null!; + private ICollection _chapters = null!; + [JsonIgnore] + public ICollection 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 authors, ICollection mangaTags, ICollection links, ICollection 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; + this.Chapters = []; + } + + /// + /// EF ONLY!!! + /// + 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; + 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 (publicationFolder is null) + throw new DirectoryNotFoundException("Publication folder not found"); 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(); + } + + public override string ToString() + { + return $"{MangaId} {Name}"; + } } \ No newline at end of file diff --git a/API/Schema/MangaAltTitle.cs b/API/Schema/MangaAltTitle.cs index 04e41f3..5775a6c 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; @@ -9,11 +8,16 @@ 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; [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/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?, List?, List?, List?)[] 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?, List?, List?, List?)[] publications = ParsePublicationsFromHtml(requestResult.htmlDocument); - return publications; - } - - public override (Manga, List?, List?, List?, List?)? GetMangaFromId(string publicationId) - { - return GetMangaFromUrl($"https://asuracomic.net/series/{publicationId}"); - } - - public override (Manga, List?, List?, List?, List?)? 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?, List?, List?, List?)[] ParsePublicationsFromHtml(HtmlDocument document) - { - HtmlNodeCollection mangaList = document.DocumentNode.SelectNodes("//a[starts-with(@href,'series')]"); - if (mangaList is null || mangaList.Count < 1) - return []; - - IEnumerable urls = mangaList.Select(a => $"https://asuracomic.net/{a.GetAttributeValue("href", "")}"); - - List<(Manga, List?, List?, List?, List?)> ret = new(); - foreach (string url in urls) - { - (Manga, List?, List?, List?, List?)? manga = GetMangaFromUrl(url); - if (manga is { } x) - ret.Add(x); - } - - return ret.ToArray(); - } - - private (Manga, List?, List?, List?, List?) ParseSinglePublicationFromHtml(HtmlDocument document, string publicationId, string websiteUrl) - { - string? originalLanguage = null; - Dictionary altTitles = new(), links = new(); - - HtmlNodeCollection genreNodes = document.DocumentNode.SelectNodes("//h3[text()='Genres']/../div/button"); - string[] tags = genreNodes.Select(b => b.InnerText).ToArray(); - List 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 authorNames = authorNodes is null ? [] : authorNodes.Select(a => a.InnerText); - IEnumerable artistNames = artistNodes is null ? [] : artistNodes.Select(a => a.InnerText); - List authorStrings = authorNames.Concat(artistNames).ToList(); - List 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 chapters = ParseChaptersFromHtml(manga, requestUrl); - return chapters.Order().ToArray(); - } - - private List 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(); - } - - List 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?, List?, List?, List?)[] 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?, List?, List?, List?)[] publications = ParsePublicationsFromHtml(requestResult.htmlDocument); - return publications; - } - - public override (Manga, List?, List?, List?, List?)? GetMangaFromId(string publicationId) - { - return GetMangaFromUrl($"https://bato.to/title/{publicationId}"); - } - - public override (Manga, List?, List?, List?, List?)? 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?, List?, List?, List?)[] ParsePublicationsFromHtml(HtmlDocument document) - { - HtmlNode mangaList = document.DocumentNode.SelectSingleNode("//div[@data-hk='0-0-2']"); - if (!mangaList.ChildNodes.Any(node => node.Name == "div")) - return []; - - List urls = mangaList.ChildNodes - .Select(node => $"https://bato.to{node.Descendants("div").First().FirstChild.GetAttributeValue("href", "")}").ToList(); - - HashSet<(Manga, List?, List?, List?, List?)> ret = new(); - foreach (string url in urls) - { - (Manga, List?, List?, List?, List?)? manga = GetMangaFromUrl(url); - if (manga is { } x) - ret.Add(x); - } - - return ret.ToArray(); - } - - private (Manga, List?, List?, List?, List?) 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 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 genreNodes = document.DocumentNode.SelectSingleNode("//b[text()='Genres:']/..").SelectNodes("span").ToList(); - string[] tags = genreNodes.Select(node => node.FirstChild.InnerText).ToArray(); - List mangaTags = tags.Select(s => new MangaTag(s)).ToList(); - - List authorsNodes = infoNode.ChildNodes[1].ChildNodes[3].Descendants("a").ToList(); - List authorNames = authorsNodes.Select(node => node.InnerText.Replace("amp;", "")).ToList(); - List 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 chapters = ParseChaptersFromHtml(manga, requestUrl); - return chapters.Order().ToArray(); - } - - private List 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(); - } - - List 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/ComickIo.cs b/API/Schema/MangaConnectors/ComickIo.cs new file mode 100644 index 0000000..707e421 --- /dev/null +++ b/API/Schema/MangaConnectors/ComickIo.cs @@ -0,0 +1,247 @@ +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 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("slug")!)); + page++; + } + Log.Debug($"Search {mangaSearchName} yielded {slugs.Count} slugs. Requesting mangas now..."); + + List 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.MangaInfo); + 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 chapterHids = new(); + int page = 1; + while(page < 50) + { + string requestUrl = $"https://api.comick.fun/comic/{manga.IdOnConnectorSite}/chapters?limit=100&page={page}&lang={language}"; + + RequestResult result = downloadClient.MakeRequest(requestUrl, RequestType.MangaInfo); + if ((int)result.statusCode < 200 || (int)result.statusCode >= 300) + { + Log.Error("Request failed"); + return []; + } + + using StreamReader sr = new (result.result); + JToken data = JToken.Parse(sr.ReadToEnd()); + JArray? chaptersArray = data["chapters"] as JArray; + + if (chaptersArray?.Count < 1) + break; + + chapterHids.AddRange(chaptersArray?.Select(token => token.Value("hid")!)!); + + page++; + } + Log.Debug($"Getting chapters for {manga.Name} yielded {chapterHids.Count} hids. Requesting chapters now..."); + + List 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.MangaInfo); + 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("b2key")}"; + return url; + }).ToArray(); + } + + private Manga ParseMangaFromJToken(JToken json) + { + string? hid = json["comic"]?.Value("hid"); + string? slug = json["comic"]?.Value("slug"); + string? name = json["comic"]?.Value("title"); + string? description = json["comic"]?.Value("desc"); + string? originalLanguage = json["comic"]?.Value("country"); + string url = $"https://comick.io/comic/{slug}"; + string? coverName = json["comic"]?["md_covers"]?.First?.Value("b2key"); + string coverUrl = $"https://meo.comick.pictures/{coverName}"; + int? releaseStatusStr = json["comic"]?.Value("status"); + MangaReleaseStatus status = releaseStatusStr switch + { + 1 => MangaReleaseStatus.Continuing, + 2 => MangaReleaseStatus.Completed, + 3 => MangaReleaseStatus.Cancelled, + 4 => MangaReleaseStatus.OnHiatus, + _ => MangaReleaseStatus.Unreleased + }; + uint? year = json["comic"]?.Value("year"); + JArray? altTitlesArray = json["comic"]?["md_titles"] as JArray; + //Cant let language be null, so fill with whatever. + byte whatever = 0; + List altTitles = altTitlesArray? + .Select(token => new MangaAltTitle(token.Value("lang")??whatever++.ToString(), token.Value("title")!)) + .ToList()!; + + JArray? authorsArray = json["authors"] as JArray; + JArray? artistsArray = json["artists"] as JArray; + List authors = authorsArray?.Concat(artistsArray!) + .Select(token => new Author(token.Value("name")!)) + .DistinctBy(a => a.AuthorId) + .ToList()!; + + JArray? genreArray = json["comic"]?["md_comic_md_genres"] as JArray; + List tags = genreArray? + .Select(token => new MangaTag(token["md_genres"]?.Value("name")!)) + .ToList()!; + + JArray? linksArray = json["comic"]?["links"] as JArray; + List links = linksArray? + .ToObject>()? + .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("canonical"); + string? chapterNum = data["chapter"]?.Value("chap"); + string? volumeNumStr = data["chapter"]?.Value("vol"); + int? volumeNum = volumeNumStr is null ? null : int.Parse(volumeNumStr); + string? title = data["chapter"]?.Value("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 diff --git a/API/Schema/MangaConnectors/Global.cs b/API/Schema/MangaConnectors/Global.cs index 52a89f3..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 { @@ -8,15 +10,14 @@ public class Global : MangaConnector this.context = context; } - public override (Manga, List?, List?, List?, List?)[] 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?, List?, List?, List?)[]>[] tasks = - enabledConnectors.Select(c => - new Task<(Manga, List?, List?, List?, List?)[]>(() => c.GetManga(publicationTitle))).ToArray(); + Task[] tasks = + enabledConnectors.Select(c => new Task(() => c.SearchManga(mangaSearchName))).ToArray(); foreach (var task in tasks) task.Start(); @@ -27,29 +28,28 @@ public class Global : MangaConnector }while(tasks.Any(t => t.Status < TaskStatus.RanToCompletion)); //Concatenate all results into one - (Manga, List?, List?, List?, List?)[] 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?, List?, List?, List?)? 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?, List?, List?, List?)? 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?, List?, List?, List?)[] GetManga(string publicationTitle = ""); + public abstract Manga[] SearchManga(string mangaSearchName); - public abstract (Manga, List?, List?, List?, List?)? GetMangaFromUrl(string url); + public abstract Manga? GetMangaFromUrl(string url); - public abstract (Manga, List?, List?, List?, List?)? 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?, List?, List?, List?)[] 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?, List?, List?, List?)> retManga = new(); - List results = new(); + Log.Info($"Searching Manga: {mangaSearchName}"); + List 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(requestResult.result); - - offset += limit; - if (result is null) - { - Log.Info($"result was null: {requestUrl}"); - break; - } - - if(result.ContainsKey("total")) - total = result["total"]!.GetValue(); //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("result") != "ok") + { + JArray? errors = jObject["errors"] as JArray; + Log.Error($"Request failed: {string.Join(',', errors?.Select(e => e.Value("title")) ?? [])}"); + return []; + } + + total = jObject.Value("total"); + + JArray? data = jObject.Value("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?, List?, List?, List?)? 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(requestResult.result); - if(result is not null) - return MangaFromJsonObject(result["data"]!.AsObject()); - Log.Info($"result was null: {url}"); - return null; - } - public override (Manga, List?, List?, List?, List?)? 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?, List?, List?, List?)? 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(); - - 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(), - false => titleNode.AsObject().First().Value!.GetValue() - }; - - Dictionary 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()); - } - } - List 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("result") != "ok") { - Log.Info("description was null"); - return null; - } - string description = descriptionNode!.AsObject().ContainsKey("en") switch - { - true => descriptionNode.AsObject()["en"]!.GetValue(), - false => descriptionNode.AsObject().FirstOrDefault().Value?.GetValue() ?? "" - }; - - Dictionary linksDict = new(); - if (attributes.TryGetPropertyValue("links", out JsonNode? linksNode) && linksNode is not null) - foreach (KeyValuePair linkKv in linksNode!.AsObject()) - linksDict.TryAdd(linkKv.Key, linkKv.Value.GetValue()); - List links = linksDict.Select(x => new Link(x.Key, x.Value)).ToList(); - - string? originalLanguage = - attributes.TryGetPropertyValue("originalLanguage", out JsonNode? originalLanguageNode) switch - { - true => originalLanguageNode?.GetValue(), - false => null - }; - - MangaReleaseStatus releaseStatus = MangaReleaseStatus.Unreleased; - if (attributes.TryGetPropertyValue("status", out JsonNode? statusNode)) - { - releaseStatus = statusNode?.GetValue().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()??0, - false => 0 - }; - - HashSet tags = new(128); - if (attributes.TryGetPropertyValue("tags", out JsonNode? tagsNode)) - foreach (JsonNode? tagNode in tagsNode!.AsArray()) - tags.Add(tagNode!["attributes"]!["name"]!["en"]!.GetValue()); - List 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("title")) ?? [])}"); return null; } - JsonNode? coverNode = relationshipsNode!.AsArray() - .FirstOrDefault(rel => rel!["type"]!.GetValue().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 coverUrl = $"https://uploads.mangadex.org/covers/{publicationId}/{fileName}"; - - List authorNames = new(); - JsonNode?[] authorNodes = relationshipsNode.AsArray() - .Where(rel => rel!["type"]!.GetValue().Equals("author") || rel!["type"]!.GetValue().Equals("artist")).ToArray(); - foreach (JsonNode? authorNode in authorNodes) - { - string authorName = authorNode!["attributes"]!["name"]!.GetValue(); - if(!authorNames.Contains(authorName)) - authorNames.Add(authorName); - } - List 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 chapters = new(); - //As long as we haven't requested all "Pages" - while (offset < total) + Log.Info($"Getting Chapters: {manga.IdOnConnectorSite}"); + List 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(requestResult.result); - - offset += limit; - if (result is null) - { - Log.Info($"result was null: {requestUrl}"); - break; - } - - total = result["total"]!.GetValue(); - 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 url = $"https://mangadex.org/chapter/{chapterId}"; - - string? title = attributes.ContainsKey("title") && attributes["title"] is not null - ? attributes["title"]!.GetValue() - : null; - - int? volume = attributes.ContainsKey("volume") && attributes["volume"] is not null - ? int.Parse(attributes["volume"]!.GetValue()) - : null; - - string? chapterNumStr = attributes.ContainsKey("chapter") && attributes["chapter"] is not null - ? attributes["chapter"]!.GetValue() - : null; - - string chapterNumber = new(chapterNumStr); - - - if (attributes.ContainsKey("pages") && attributes["pages"] is not null && - attributes["pages"]!.GetValue() < 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("result") != "ok") + { + JArray? errors = jObject["errors"] as JArray; + Log.Error($"Request failed: {string.Join(',', errors?.Select(e => e.Value("title")) ?? [])}"); + return []; + } + + total = jObject.Value("total"); + + JArray? data = jObject.Value("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(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 hash = result["chapter"]!["hash"]!.GetValue(); - JsonArray imageFileNames = result["chapter"]!["data"]!.AsArray(); - //Loop through all imageNames and construct urls (imageUrl) - List imageUrls = new(); - foreach (JsonNode? image in imageFileNames) - imageUrls.Add($"{baseUrl}/data/{hash}/{image!.GetValue()}"); - return imageUrls.ToArray(); + + using StreamReader sr = new (result.result); + JObject jObject = JObject.Parse(sr.ReadToEnd()); + + if (jObject.Value("result") != "ok") + { + JArray? errors = jObject["errors"] as JArray; + Log.Error($"Request failed: {string.Join(',', errors?.Select(e => e.Value("title")) ?? [])}"); + return []; + } + + string? baseUrl = jObject.Value("baseUrl"); + JToken? chapterToken = jObject["chapter"]; + string? hash = chapterToken?.Value("hash"); + JArray? data = chapterToken?["data"] as JArray; + + if (baseUrl is null || hash is null || data is null) + { + Log.Error("Data was null"); + return []; + } + + IEnumerable urls = data.Select(t => $"{baseUrl}/data/{hash}/{t.Value()}"); + + return urls.ToArray(); + } + + private Manga ParseMangaFromJToken(JToken jToken) + { + string? id = jToken.Value("id"); + + JObject? attributes = jToken["attributes"] as JObject; + string? name = attributes?["title"]?.Value("en"); + string? description = attributes?["description"]?.Value("en"); + string? status = attributes?["status"]?.Value(); + uint? year = attributes?["year"]?.Value(); + string? originalLanguage = attributes?["originalLanguage"]?.Value(); + 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() == "cover_art")?["attributes"]?.Value("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 links = attributes["links"]? + .ToObject>()? + .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 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 tags = tagsJArray + .Where(t => t.Value("type") == "tag") + .Select(t => t["attributes"]?["name"]?.Value("en")) + .Select(str => str is not null ? new MangaTag(str) : null) + .Where(x => x is not null).ToList()!; + + List authors = relationships + .Where(r => r["type"]?.Value() == "author") + .Select(t => t["attributes"]?.Value("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("id"); + JToken? attributes = jToken["attributes"]; + string? chapter = attributes?.Value("chapter"); + string? volumeStr = attributes?.Value("volume"); + int? volume = null; + string? title = attributes?.Value("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?, List?, List?, List?)[] 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?, List?, List?, List?)[] publications = ParsePublicationsFromHtml(requestResult.htmlDocument); - return publications; - } - - private (Manga, List?, List?, List?, List?)[] 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 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?, List?, List?, List?)> ret = new(); - foreach (string url in urls) - { - (Manga, List?, List?, List?, List?)? manga = GetMangaFromUrl(url); - if (manga is { } x) - ret.Add(x); - } - - return ret.ToArray(); - } - - public override (Manga, List?, List?, List?, List?)? GetMangaFromId(string publicationId) - { - return GetMangaFromUrl($"https://www.mangahere.cc/manga/{publicationId}"); - } - - public override (Manga, List?, List?, List?, List?)? 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?, List?, List?, List?) ParseSinglePublicationFromHtml(HtmlDocument document, string publicationId, string websiteUrl) - { - string originalLanguage = "", status = ""; - Dictionary 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 authorNames = document.DocumentNode - .SelectNodes("//p[contains(concat(' ',normalize-space(@class),' '),' detail-info-right-say ')]/a") - .Select(node => node.InnerText) - .ToList(); - List authors = authorNames.Select(n => new Author(n)).ToList(); - - HashSet tags = document.DocumentNode - .SelectNodes("//p[contains(concat(' ',normalize-space(@class),' '),' detail-info-right-tag-list ')]/a") - .Select(node => node.InnerText) - .ToHashSet(); - List 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(); - - List 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 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 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?, List?, List?, List?)[] 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?, List?, List?, List?)[] publications = ParsePublicationsFromHtml(requestResult.result); - return publications; - } - - public override (Manga, List?, List?, List?, List?)? GetMangaFromId(string publicationId) - { - return GetMangaFromUrl($"https://mangakatana.com/manga/{publicationId}"); - } - - public override (Manga, List?, List?, List?, List?)? 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?, List?, List?, List?)[] ParsePublicationsFromHtml(Stream html) - { - StreamReader reader = new(html); - string htmlString = reader.ReadToEnd(); - HtmlDocument document = new(); - document.LoadHtml(htmlString); - IEnumerable searchResults = document.DocumentNode.SelectNodes("//*[@id='book_list']/div"); - if (searchResults is null || !searchResults.Any()) - return []; - List urls = new(); - foreach (HtmlNode mangaResult in searchResults) - { - urls.Add(mangaResult.Descendants("a").First().GetAttributes() - .First(a => a.Name == "href").Value); - } - - HashSet<(Manga, List?, List?, List?, List?)> ret = new(); - foreach (string url in urls) - { - (Manga, List?, List?, List?, List?)? manga = GetMangaFromUrl(url); - if (manga is { } x) - ret.Add(x); - } - - return ret.ToArray(); - } - - private (Manga, List?, List?, List?, List?) ParseSinglePublicationFromHtml(Stream html, string publicationId, string websiteUrl) - { - StreamReader reader = new(html); - string htmlString = reader.ReadToEnd(); - HtmlDocument document = new(); - document.LoadHtml(htmlString); - Dictionary altTitlesDict = new(); - Dictionary? links = null; - HashSet 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 authors = authorNames.Select(n => new Author(n)).ToList(); - List mangaTags = tags.Select(n => new MangaTag(n)).ToList(); - List 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(); - - //Return Chapters ordered by Chapter-Number - List chapters = ParseChaptersFromHtml(manga, requestUrl); - return chapters.Order().ToArray(); - } - - private List 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 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?, List?, List?, List?)[] 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?, List?, List?, List?)[] publications = - ParsePublicationsFromHtml(requestResult.htmlDocument); - return publications; - } - - private (Manga, List?, List?, List?, List?)[] ParsePublicationsFromHtml( - HtmlDocument document) - { - List searchResults = - document.DocumentNode.Descendants("div").Where(n => n.HasClass("story_item")).ToList(); - List 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?, List?, List?, List?)> ret = new(); - foreach (string url in urls) - { - (Manga, List?, List?, List?, List?)? manga = GetMangaFromUrl(url); - if (manga is { } m) - ret.Add(m); - } - - return ret.ToArray(); - } - - public override (Manga, List?, List?, List?, List?)? GetMangaFromId( - string publicationId) - { - return GetMangaFromUrl($"https://chapmanganato.com/{publicationId}"); - } - - public override (Manga, List?, List?, List?, List?)? - 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?, List?, List?, List?) ParseSinglePublicationFromHtml( - HtmlDocument document, string publicationId, string websiteUrl) - { - Dictionary altTitles = new(); - List tags = new(); - List 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(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(); - - //Return Chapters ordered by Chapter-Number - if (requestResult.htmlDocument is null) - return Array.Empty(); - List 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 ParseChaptersFromHtml(Manga manga, HtmlDocument document) - { - List 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 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?, List?, List?, List?)[] 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?, List?, List?, List?)[] publications = ParsePublicationsFromHtml(requestResult.htmlDocument); - return publications; - } - - private (Manga, List?, List?, List?, List?)[] ParsePublicationsFromHtml(HtmlDocument document) - { - if (!document.DocumentNode.SelectSingleNode("//div[@class='comics-grid']").ChildNodes - .Any(node => node.HasClass("entry"))) - return []; - - List 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?, List?, List?, List?)> ret = new(); - foreach (string url in urls) - { - (Manga, List?, List?, List?, List?)? manga = GetMangaFromUrl(url); - if (manga is { } x) - ret.Add(x); - } - - return ret.ToArray(); - } - - public override (Manga, List?, List?, List?, List?)? GetMangaFromId(string publicationId) - { - return GetMangaFromUrl($"https://www.mangaworld.ac/manga/{publicationId}"); - } - - public override (Manga, List?, List?, List?, List?)? 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?, List?, List?, List?) ParseSinglePublicationFromHtml(HtmlDocument document, string publicationId, string websiteUrl) - { - Dictionary 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 altTitles = altTitlesDict.Select(a => new MangaAltTitle(a.Key, a.Value)).ToList(); - - HtmlNode genresNode = - metadata.SelectSingleNode("//span[text()='Generi: ' or text()='Genero: ']/.."); - HashSet tags = genresNode.SelectNodes("a").Select(node => node.InnerText).ToHashSet(); - List 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 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 chapters = ParseChaptersFromHtml(manga, requestResult.htmlDocument); - return chapters.Order().ToArray(); - } - - private List ParseChaptersFromHtml(Manga manga, HtmlDocument document) - { - List 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 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?, List?, List?, List?)[] 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?, List?, List?, List?)[] publications = ParsePublicationsFromHtml(requestResult.htmlDocument); - return publications; - } - - private (Manga, List?, List?, List?, List?)[] ParsePublicationsFromHtml(HtmlDocument document) - { - if (document.DocumentNode.SelectSingleNode("//h1/../..").ChildNodes//I already want to not. - .Any(node => node.InnerText.Contains("No manga found"))) - return []; - - List 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?, List?, List?, List?)> ret = new(); - foreach (string url in urls) - { - (Manga, List?, List?, List?, List?)? manga = GetMangaFromUrl(url); - if (manga is { } x) - ret.Add(x); - } - - return ret.ToArray(); - } - - public override (Manga, List?, List?, List?, List?)? GetMangaFromId(string publicationId) - { - return GetMangaFromUrl($"https://manhuaplus.org/manga/{publicationId}"); - } - - public override (Manga, List?, List?, List?, List?)? 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?, List?, List?, List?) ParseSinglePublicationFromHtml(HtmlDocument document, string publicationId, string websiteUrl) - { - string originalLanguage = "", status = ""; - Dictionary altTitles = new(), links = new(); - HashSet 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 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 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 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(); - } - - 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 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 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?, List?, List?, List?)[] 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, List, List, List)[] publications = - ParsePublicationsFromHtml(requestResult.htmlDocument); - return publications; - } - - // Done - public override (Manga, List, List, List, List)? 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, List, List, List)? 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/(?[^/]+)/(?[^/]+)/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/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 diff --git a/API/Schema/PgsqlContext.cs b/API/Schema/PgsqlContext.cs deleted file mode 100644 index 3d6aaf0..0000000 --- a/API/Schema/PgsqlContext.cs +++ /dev/null @@ -1,113 +0,0 @@ -using API.Schema.Jobs; -using API.Schema.LibraryConnectors; -using API.Schema.MangaConnectors; -using API.Schema.NotificationConnectors; -using Microsoft.EntityFrameworkCore; - -namespace API.Schema; - -public class PgsqlContext(DbContextOptions<PgsqlContext> options) : DbContext(options) -{ - public DbSet<Job> Jobs { get; set; } - public DbSet<MangaConnector> MangaConnectors { get; set; } - public DbSet<Manga> Mangas { get; set; } - 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); - - modelBuilder.Entity<Job>() - .HasDiscriminator<JobType>(j => j.JobType) - .HasValue<MoveFileOrFolderJob>(JobType.MoveFileOrFolderJob) - .HasValue<DownloadAvailableChaptersJob>(JobType.DownloadAvailableChaptersJob) - .HasValue<DownloadSingleChapterJob>(JobType.DownloadSingleChapterJob) - .HasValue<DownloadMangaCoverJob>(JobType.DownloadMangaCoverJob) - .HasValue<UpdateMetadataJob>(JobType.UpdateMetaDataJob) - .HasValue<RetrieveChaptersJob>(JobType.RetrieveChaptersJob) - .HasValue<UpdateFilesDownloadedJob>(JobType.UpdateFilesDownloadedJob); - modelBuilder.Entity<Job>() - .HasMany<Job>() - .WithOne(j => j.ParentJob) - .OnDelete(DeleteBehavior.Cascade); - modelBuilder.Entity<Job>() - .HasMany<Job>(j => j.DependsOnJobs) - .WithMany(); - modelBuilder.Entity<UpdateMetadataJob>() - .Navigation(umj => umj.Manga) - .AutoInclude(); - - modelBuilder.Entity<Manga>() - .HasOne<MangaConnector>(m => m.MangaConnector) - .WithMany() - .HasForeignKey(m => m.MangaConnectorId) - .OnDelete(DeleteBehavior.Cascade); - modelBuilder.Entity<Manga>() - .Navigation(m => m.MangaConnector) - .AutoInclude(); - 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() - .HasForeignKey(c => c.ParentMangaId) - .OnDelete(DeleteBehavior.Cascade); - modelBuilder.Entity<Chapter>() - .Navigation(c => c.ParentManga) - .AutoInclude(); - } -} \ 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"; diff --git a/API/Tranga.cs b/API/Tranga.cs index f1b7e56..f5039f8 100644 --- a/API/Tranga.cs +++ b/API/Tranga.cs @@ -1,15 +1,27 @@ using API.Schema; +using API.Schema.Contexts; using API.Schema.Jobs; using API.Schema.MangaConnectors; using API.Schema.NotificationConnectors; using log4net; using log4net.Config; using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.ChangeTracking; 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)); @@ -18,6 +30,7 @@ public static class Tranga { BasicConfigurator.Configure(); Log.Info("Logger Configured."); + Log.Info(TRANGA); } private static void NotificationSender(object? serviceProviderObj) @@ -29,12 +42,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 { @@ -61,14 +69,9 @@ 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(); - 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()) @@ -90,17 +93,10 @@ 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) { + Log.Info("JobStarter Thread running."); if (serviceProviderObj is null) { Log.Error("serviceProviderObj is null"); @@ -108,61 +104,75 @@ 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; - } - - Log.Info(TRANGA); - Log.Info("JobStarter Thread running."); + PgsqlContext context = scope.ServiceProvider.GetRequiredService<PgsqlContext>(); + 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); + { + Log.Debug("Starting Job-Cycle..."); + DateTime cycleStart = DateTime.UtcNow; + Log.Debug("Loading Jobs..."); + DateTime loadStart = DateTime.UtcNow; + context.Jobs.Load(); + Log.Debug("Updating Entries..."); + foreach (EntityEntry entityEntry in context.ChangeTracker.Entries().ToArray()) + entityEntry.Reload(); + 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) + 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.Local.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> 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()); + 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) + .DistinctBy(j => j.JobType) + .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(); + 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) + { Thread t = new(() => { job.Run(serviceProvider); @@ -170,6 +180,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: {jobsWithoutMissingDependencies.Count}"); (Thread, Job)[] removeFromThreadsList = RunningJobs.Where(t => !t.Key.IsAlive) .Select(t => (t.Key, t.Value)).ToArray(); @@ -187,88 +201,66 @@ 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); } } - 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(); + } + + private static List<Job> GetWaitingJobs(List<Job> jobs) => + jobs + .Where(j => + j.Enabled && + (j.state == JobState.FirstExecution || j.state == JobState.CompletedWaiting)) + .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> FilterDueJobs(List<Job> jobs) => + jobs + .Where(j => j.NextExecution < DateTime.UtcNow) + .ToList(); - Dictionary<MangaConnector, List<Job>> metadataJobsByConnector = new(); - if (jobsByType.ContainsKey(JobType.DownloadAvailableChaptersJob)) - { - foreach (DownloadAvailableChaptersJob job in jobsByType[JobType.DownloadAvailableChaptersJob]) + private static List<Job> FilterJobDependencies(PgsqlContext context, List<Job> jobs) => + jobs + .Where(j => { - 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))!; + Log.Debug($"Loading Job Preconditions {j}..."); + context.Entry(j).Collection(j => j.DependsOnJobs).Load(); + Log.Debug($"Loaded Job Preconditions {j}!"); + return j.DependenciesFulfilled; + }) + .ToList(); - if (jobsByType.ContainsKey(JobType.DownloadSingleChapterJob)) - { - - Dictionary<MangaConnector, List<DownloadSingleChapterJob>> downloadJobsByConnector = new(); - foreach (DownloadSingleChapterJob job in jobsByType[JobType.DownloadSingleChapterJob]) + private static List<Job> FilterJobWithBusyConnectors(List<Job> jobs, List<MangaConnector> busyConnectors) => + jobs + .Where(j => { - 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; + //Filter jobs with busy connectors + if (GetJobConnector(j) is { } mangaConnector) + return busyConnectors.Contains(mangaConnector) == false; + return true; + }) + .ToList(); + + 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