From 7e9ba7090af9f24def43df55fc3f5fdb2771c4c2 Mon Sep 17 00:00:00 2001 From: glax Date: Mon, 30 Jun 2025 22:01:10 +0200 Subject: [PATCH] Manga and Chapters are shared across Connectors --- API/API.csproj | 4 - API/Controllers/JobController.cs | 35 +- API/Controllers/LibraryConnectorController.cs | 17 +- API/Controllers/LocalLibrariesController.cs | 22 +- API/Controllers/MangaController.cs | 104 +- .../NotificationConnectorController.cs | 2 +- API/Controllers/QueryController.cs | 6 +- API/Controllers/SearchController.cs | 42 +- API/Controllers/SettingsController.cs | 20 +- API/JobQueueSortable.cs | 47 - .../ChromiumDownloadClient.cs | 2 +- .../FlareSolverrDownloadClient.cs | 2 +- .../pgsql/20250630182650_OofV2.1.Designer.cs | 795 +++++++++++++ .../pgsql/20250630182650_OofV2.1.cs | 1055 +++++++++++++++++ .../pgsql/PgsqlContextModelSnapshot.cs | 309 +++-- API/Program.cs | 24 +- API/Schema/AltTitle.cs | 17 + API/Schema/Author.cs | 12 +- API/Schema/Chapter.cs | 87 +- API/Schema/Contexts/PgsqlContext.cs | 104 +- API/Schema/FileLibrary.cs | 19 + API/Schema/Identifiable.cs | 11 + .../Jobs/DownloadAvailableChaptersJob.cs | 38 +- API/Schema/Jobs/DownloadMangaCoverJob.cs | 36 +- API/Schema/Jobs/DownloadSingleChapterJob.cs | 49 +- API/Schema/Jobs/Job.cs | 44 +- API/Schema/Jobs/JobWithDownloading.cs | 31 +- API/Schema/Jobs/MoveFileOrFolderJob.cs | 4 +- API/Schema/Jobs/MoveMangaLibraryJob.cs | 46 +- API/Schema/Jobs/RetrieveChaptersJob.cs | 41 +- .../Jobs/UpdateChaptersDownloadedJob.cs | 22 +- API/Schema/Jobs/UpdateCoverJob.cs | 33 +- API/Schema/LibraryConnectors/Kavita.cs | 11 +- API/Schema/Link.cs | 12 +- API/Schema/LocalLibrary.cs | 22 - API/Schema/Manga.cs | 96 +- API/Schema/MangaAltTitle.cs | 23 - ...ectorMangaEntry.cs => MangaConnectorId.cs} | 34 +- API/Schema/MangaConnectors/ComickIo.cs | 66 +- API/Schema/MangaConnectors/Global.cs | 23 +- API/Schema/MangaConnectors/MangaConnector.cs | 21 +- API/Schema/MangaConnectors/MangaDex.cs | 63 +- API/Schema/MangaReleaseStatus.cs | 10 - API/Tranga.cs | 53 +- API/TrangaSettings.cs | 8 +- DB-Layout.png | Bin 0 -> 37522 bytes DB-Layout.uxf | 460 +++++++ README.md | 2 + Tranga.sln.DotSettings | 3 + 49 files changed, 3192 insertions(+), 795 deletions(-) delete mode 100644 API/JobQueueSortable.cs create mode 100644 API/Migrations/pgsql/20250630182650_OofV2.1.Designer.cs create mode 100644 API/Migrations/pgsql/20250630182650_OofV2.1.cs create mode 100644 API/Schema/AltTitle.cs create mode 100644 API/Schema/FileLibrary.cs create mode 100644 API/Schema/Identifiable.cs delete mode 100644 API/Schema/LocalLibrary.cs delete mode 100644 API/Schema/MangaAltTitle.cs rename API/Schema/{MangaConnectorMangaEntry.cs => MangaConnectorId.cs} (52%) delete mode 100644 API/Schema/MangaReleaseStatus.cs create mode 100644 DB-Layout.png create mode 100644 DB-Layout.uxf diff --git a/API/API.csproj b/API/API.csproj index 24d1e9c..1cb720c 100644 --- a/API/API.csproj +++ b/API/API.csproj @@ -32,8 +32,4 @@ - - - - diff --git a/API/Controllers/JobController.cs b/API/Controllers/JobController.cs index 49609f5..f47317a 100644 --- a/API/Controllers/JobController.cs +++ b/API/Controllers/JobController.cs @@ -37,7 +37,7 @@ public class JobController(PgsqlContext context, ILog Log) : Controller [ProducesResponseType(Status200OK, "application/json")] public IActionResult GetJobs([FromBody]string[] ids) { - Job[] ret = context.Jobs.Where(job => ids.Contains(job.JobId)).ToArray(); + Job[] ret = context.Jobs.Where(job => ids.Contains(job.Key)).ToArray(); return Ok(ret); } @@ -103,11 +103,11 @@ public class JobController(PgsqlContext context, ILog Log) : Controller /// /// Create a new DownloadAvailableChaptersJob /// - /// ID of Manga + /// ID of Obj /// Job-Configuration /// Job-IDs - /// Could not find ToLibrary with ID - /// Could not find Manga with ID + /// Could not find ToFileLibrary with ID + /// Could not find Obj with ID /// Error during Database Operation [HttpPut("DownloadAvailableChaptersJob/{MangaId}")] [ProducesResponseType(Status201Created, "application/json")] @@ -122,7 +122,7 @@ public class JobController(PgsqlContext context, ILog Log) : Controller { try { - LocalLibrary? l = context.LocalLibraries.Find(record.localLibraryId); + FileLibrary? l = context.LocalLibraries.Find(record.localLibraryId); if (l is null) return BadRequest(); m.Library = l; @@ -166,9 +166,9 @@ public class JobController(PgsqlContext context, ILog Log) : Controller /// /// Create a new UpdateChaptersDownloadedJob /// - /// ID of the Manga + /// ID of the Obj /// Job-IDs - /// Could not find Manga with ID + /// Could not find Obj with ID /// Error during Database Operation [HttpPut("UpdateFilesJob/{MangaId}")] [ProducesResponseType(Status201Created, "application/json")] @@ -183,7 +183,7 @@ public class JobController(PgsqlContext context, ILog Log) : Controller } /// - /// Create a new UpdateMetadataJob for all Manga + /// Create a new UpdateMetadataJob for all Obj /// /// Job-IDs /// Error during Database Operation @@ -209,9 +209,9 @@ public class JobController(PgsqlContext context, ILog Log) : Controller /// /// Not Implemented: Create a new UpdateMetadataJob /// - /// ID of the Manga + /// ID of the Obj /// Job-IDs - /// Could not find Manga with ID + /// Could not find Obj with ID /// Error during Database Operation [HttpPut("UpdateMetadataJob/{MangaId}")] [ProducesResponseType(Status201Created, "application/json")] @@ -223,7 +223,7 @@ public class JobController(PgsqlContext context, ILog Log) : Controller } /// - /// Not Implemented: Create a new UpdateMetadataJob for all Manga + /// Not Implemented: Create a new UpdateMetadataJob for all Obj /// /// Job-IDs /// Error during Database Operation @@ -241,7 +241,7 @@ public class JobController(PgsqlContext context, ILog Log) : Controller { context.Jobs.AddRange(jobs); context.SaveChanges(); - return new CreatedResult((string?)null, jobs.Select(j => j.JobId).ToArray()); + return new CreatedResult((string?)null, jobs.Select(j => j.Key).ToArray()); } catch (Exception e) { @@ -279,15 +279,6 @@ public class JobController(PgsqlContext context, ILog Log) : Controller } } - private IQueryable GetChildJobs(string parentJobId) - { - IQueryable children = context.Jobs.Where(j => j.ParentJobId == parentJobId); - foreach (Job child in children) - foreach (Job grandChild in GetChildJobs(child.JobId)) - children.Append(grandChild); - return children; - } - /// /// Modify Job with ID /// @@ -314,7 +305,7 @@ public class JobController(PgsqlContext context, ILog Log) : Controller ret.Enabled = modifyJobRecord.Enabled ?? ret.Enabled; context.SaveChanges(); - return new AcceptedResult(ret.JobId, ret); + return new AcceptedResult(ret.Key, ret); } catch (Exception e) { diff --git a/API/Controllers/LibraryConnectorController.cs b/API/Controllers/LibraryConnectorController.cs index 0db5748..ccf4419 100644 --- a/API/Controllers/LibraryConnectorController.cs +++ b/API/Controllers/LibraryConnectorController.cs @@ -1,5 +1,4 @@ -using API.Schema; -using API.Schema.Contexts; +using API.Schema.Contexts; using API.Schema.LibraryConnectors; using Asp.Versioning; using log4net; @@ -14,7 +13,7 @@ namespace API.Controllers; public class LibraryConnectorController(LibraryContext context, ILog Log) : Controller { /// - /// Gets all configured ToLibrary-Connectors + /// Gets all configured ToFileLibrary-Connectors /// /// [HttpGet] @@ -26,9 +25,9 @@ public class LibraryConnectorController(LibraryContext context, ILog Log) : Cont } /// - /// Returns ToLibrary-Connector with requested ID + /// Returns ToFileLibrary-Connector with requested ID /// - /// ToLibrary-Connector-ID + /// ToFileLibrary-Connector-ID /// /// Connector with ID not found. [HttpGet("{LibraryControllerId}")] @@ -45,9 +44,9 @@ public class LibraryConnectorController(LibraryContext context, ILog Log) : Cont } /// - /// Creates a new ToLibrary-Connector + /// Creates a new ToFileLibrary-Connector /// - /// ToLibrary-Connector + /// ToFileLibrary-Connector /// /// Error during Database Operation [HttpPut] @@ -69,9 +68,9 @@ public class LibraryConnectorController(LibraryContext context, ILog Log) : Cont } /// - /// Deletes the ToLibrary-Connector with the requested ID + /// Deletes the ToFileLibrary-Connector with the requested ID /// - /// ToLibrary-Connector-ID + /// ToFileLibrary-Connector-ID /// /// Connector with ID not found. /// Error during Database Operation diff --git a/API/Controllers/LocalLibrariesController.cs b/API/Controllers/LocalLibrariesController.cs index 9f09118..8ca8e6d 100644 --- a/API/Controllers/LocalLibrariesController.cs +++ b/API/Controllers/LocalLibrariesController.cs @@ -14,18 +14,18 @@ namespace API.Controllers; public class LocalLibrariesController(PgsqlContext context, ILog Log) : Controller { [HttpGet] - [ProducesResponseType(Status200OK, "application/json")] + [ProducesResponseType(Status200OK, "application/json")] public IActionResult GetLocalLibraries() { return Ok(context.LocalLibraries); } [HttpGet("{LibraryId}")] - [ProducesResponseType(Status200OK, "application/json")] + [ProducesResponseType(Status200OK, "application/json")] [ProducesResponseType(Status404NotFound)] public IActionResult GetLocalLibrary(string LibraryId) { - LocalLibrary? library = context.LocalLibraries.Find(LibraryId); + FileLibrary? library = context.LocalLibraries.Find(LibraryId); if (library is null) return NotFound(); return Ok(library); @@ -38,7 +38,7 @@ public class LocalLibrariesController(PgsqlContext context, ILog Log) : Controll [ProducesResponseType(Status500InternalServerError, "text/plain")] public IActionResult UpdateLocalLibrary(string LibraryId, [FromBody]NewLibraryRecord record) { - LocalLibrary? library = context.LocalLibraries.Find(LibraryId); + FileLibrary? library = context.LocalLibraries.Find(LibraryId); if (library is null) return NotFound(); if (record.Validate() == false) @@ -68,7 +68,7 @@ public class LocalLibrariesController(PgsqlContext context, ILog Log) : Controll { try { - LocalLibrary? library = context.LocalLibraries.Find(LibraryId); + FileLibrary? library = context.LocalLibraries.Find(LibraryId); if (library is null) return NotFound(); @@ -96,7 +96,7 @@ public class LocalLibrariesController(PgsqlContext context, ILog Log) : Controll { try { - LocalLibrary? library = context.LocalLibraries.Find(LibraryId); + FileLibrary? library = context.LocalLibraries.Find(LibraryId); if (library is null) return NotFound(); @@ -116,7 +116,7 @@ public class LocalLibrariesController(PgsqlContext context, ILog Log) : Controll } [HttpPut] - [ProducesResponseType(Status200OK, "application/json")] + [ProducesResponseType(Status200OK, "application/json")] [ProducesResponseType(Status400BadRequest)] [ProducesResponseType(Status500InternalServerError, "text/plain")] public IActionResult CreateNewLibrary([FromBody]NewLibraryRecord library) @@ -125,11 +125,11 @@ public class LocalLibrariesController(PgsqlContext context, ILog Log) : Controll return BadRequest(); try { - LocalLibrary newLibrary = new (library.path, library.name); - context.LocalLibraries.Add(newLibrary); + FileLibrary newFileLibrary = new (library.path, library.name); + context.LocalLibraries.Add(newFileLibrary); context.SaveChanges(); - return Ok(newLibrary); + return Ok(newFileLibrary); } catch (Exception e) { @@ -147,7 +147,7 @@ public class LocalLibrariesController(PgsqlContext context, ILog Log) : Controll try { - LocalLibrary? library = context.LocalLibraries.Find(LibraryId); + FileLibrary? library = context.LocalLibraries.Find(LibraryId); if (library is null) return NotFound(); context.Remove(library); diff --git a/API/Controllers/MangaController.cs b/API/Controllers/MangaController.cs index c5971b7..5079583 100644 --- a/API/Controllers/MangaController.cs +++ b/API/Controllers/MangaController.cs @@ -4,6 +4,7 @@ using API.Schema.Jobs; using Asp.Versioning; using log4net; using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; using Microsoft.Net.Http.Headers; using SixLabors.ImageSharp; using SixLabors.ImageSharp.Formats.Jpeg; @@ -20,7 +21,7 @@ namespace API.Controllers; public class MangaController(PgsqlContext context, ILog Log) : Controller { /// - /// Returns all cached Manga + /// Returns all cached Obj /// /// [HttpGet] @@ -32,24 +33,24 @@ public class MangaController(PgsqlContext context, ILog Log) : Controller } /// - /// Returns all cached Manga with IDs + /// Returns all cached Obj with IDs /// - /// Array of Manga-IDs + /// Array of Obj-IDs /// [HttpPost("WithIDs")] [ProducesResponseType(Status200OK, "application/json")] public IActionResult GetManga([FromBody]string[] ids) { - Manga[] ret = context.Mangas.Where(m => ids.Contains(m.MangaId)).ToArray(); + Manga[] ret = context.Mangas.Where(m => ids.Contains(m.Key)).ToArray(); return Ok(ret); } /// - /// Return Manga with ID + /// Return Obj with ID /// - /// Manga-ID + /// Obj-ID /// - /// Manga with ID not found + /// Obj with ID not found [HttpGet("{MangaId}")] [ProducesResponseType(Status200OK, "application/json")] [ProducesResponseType(Status404NotFound)] @@ -62,11 +63,11 @@ public class MangaController(PgsqlContext context, ILog Log) : Controller } /// - /// Delete Manga with ID + /// Delete Obj with ID /// - /// Manga-ID + /// Obj-ID /// - /// Manga with ID not found + /// Obj with ID not found /// Error during Database Operation [HttpDelete("{MangaId}")] [ProducesResponseType(Status200OK)] @@ -91,16 +92,45 @@ public class MangaController(PgsqlContext context, ILog Log) : Controller } } + /// - /// Returns Cover of Manga + /// Merge two Manga into one. THIS IS NOT REVERSIBLE! /// - /// Manga-ID + /// + /// MangaId not found + /// Error during Database Operation + [HttpPatch("{MangaIdFrom}/MergeInto/{MangaIdTo}")] + [ProducesResponseType(Status200OK,"image/jpeg")] + [ProducesResponseType(Status404NotFound)] + [ProducesResponseType(Status500InternalServerError, "text/plain")] + public IActionResult MergeIntoManga(string MangaIdFrom, string MangaIdTo) + { + if(context.Mangas.Find(MangaIdFrom) is not { } from) + return NotFound(MangaIdFrom); + if(context.Mangas.Find(MangaIdTo) is not { } to) + return NotFound(MangaIdTo); + try + { + to.MergeFrom(from, context); + return Ok(); + } + catch (DbUpdateException e) + { + Log.Error(e); + return StatusCode(500, e.Message); + } + } + + /// + /// Returns Cover of Obj + /// + /// Obj-ID /// If width is provided, height needs to also be provided /// If height is provided, width needs to also be provided /// JPEG Image /// Cover not loaded /// The formatting-request was invalid - /// Manga with ID not found + /// Obj with ID not found /// Retry later, downloading cover [HttpGet("{MangaId}/Cover")] [ProducesResponseType(Status200OK,"image/jpeg")] @@ -115,7 +145,7 @@ public class MangaController(PgsqlContext context, ILog Log) : Controller if (!System.IO.File.Exists(m.CoverFileNameInCache)) { - List coverDownloadJobs = context.Jobs.Where(j => j.JobType == JobType.DownloadMangaCoverJob).ToList(); + List coverDownloadJobs = context.Jobs.Where(j => j.JobType == JobType.DownloadMangaCoverJob).Include(j => ((DownloadMangaCoverJob)j).Manga).ToList(); if (coverDownloadJobs.Any(j => j is DownloadMangaCoverJob dmc && dmc.MangaId == MangaId && dmc.state < JobState.Completed)) { Response.Headers.Append("Retry-After", $"{TrangaSettings.startNewJobTimeoutMs * coverDownloadJobs.Count() * 2 / 1000:D}"); @@ -146,11 +176,11 @@ public class MangaController(PgsqlContext context, ILog Log) : Controller } /// - /// Returns all Chapters of Manga + /// Returns all Chapters of Obj /// - /// Manga-ID + /// Obj-ID /// - /// Manga with ID not found + /// Obj with ID not found [HttpGet("{MangaId}/Chapters")] [ProducesResponseType(Status200OK, "application/json")] [ProducesResponseType(Status404NotFound)] @@ -164,12 +194,12 @@ public class MangaController(PgsqlContext context, ILog Log) : Controller } /// - /// Returns all downloaded Chapters for Manga with ID + /// Returns all downloaded Chapters for Obj with ID /// - /// Manga-ID + /// Obj-ID /// /// No available chapters - /// Manga with ID not found. + /// Obj with ID not found. [HttpGet("{MangaId}/Chapters/Downloaded")] [ProducesResponseType(Status200OK, "application/json")] [ProducesResponseType(Status204NoContent)] @@ -187,12 +217,12 @@ public class MangaController(PgsqlContext context, ILog Log) : Controller } /// - /// Returns all Chapters not downloaded for Manga with ID + /// Returns all Chapters not downloaded for Obj with ID /// - /// Manga-ID + /// Obj-ID /// /// No available chapters - /// Manga with ID not found. + /// Obj with ID not found. [HttpGet("{MangaId}/Chapters/NotDownloaded")] [ProducesResponseType(Status200OK, "application/json")] [ProducesResponseType(Status204NoContent)] @@ -210,12 +240,12 @@ public class MangaController(PgsqlContext context, ILog Log) : Controller } /// - /// Returns the latest Chapter of requested Manga available on Website + /// Returns the latest Chapter of requested Obj available on Website /// - /// Manga-ID + /// Obj-ID /// /// No available chapters - /// Manga with ID not found. + /// Obj with ID not found. /// Could not retrieve the maximum chapter-number /// Retry after timeout, updating value [HttpGet("{MangaId}/Chapter/LatestAvailable")] @@ -232,7 +262,7 @@ public class MangaController(PgsqlContext context, ILog Log) : Controller List chapters = m.Chapters.ToList(); if (chapters.Count == 0) { - List retrieveChapterJobs = context.Jobs.Where(j => j.JobType == JobType.RetrieveChaptersJob).ToList(); + List retrieveChapterJobs = context.Jobs.Where(j => j.JobType == JobType.RetrieveChaptersJob).Include(j => ((RetrieveChaptersJob)j).Manga).ToList(); if (retrieveChapterJobs.Any(j => j is RetrieveChaptersJob rcj && rcj.MangaId == MangaId && rcj.state < JobState.Completed)) { Response.Headers.Append("Retry-After", $"{TrangaSettings.startNewJobTimeoutMs * retrieveChapterJobs.Count() * 2 / 1000:D}"); @@ -249,12 +279,12 @@ public class MangaController(PgsqlContext context, ILog Log) : Controller } /// - /// Returns the latest Chapter of requested Manga that is downloaded + /// Returns the latest Chapter of requested Obj that is downloaded /// - /// Manga-ID + /// Obj-ID /// /// No available chapters - /// Manga with ID not found. + /// Obj with ID not found. /// Could not retrieve the maximum chapter-number /// Retry after timeout, updating value [HttpGet("{MangaId}/Chapter/LatestDownloaded")] @@ -271,7 +301,7 @@ public class MangaController(PgsqlContext context, ILog Log) : Controller List chapters = m.Chapters.ToList(); if (chapters.Count == 0) { - List retrieveChapterJobs = context.Jobs.Where(j => j.JobType == JobType.RetrieveChaptersJob).ToList(); + List retrieveChapterJobs = context.Jobs.Where(j => j.JobType == JobType.RetrieveChaptersJob).Include(j => ((RetrieveChaptersJob)j).Manga).ToList(); if (retrieveChapterJobs.Any(j => j is RetrieveChaptersJob rcj && rcj.MangaId == MangaId && rcj.state < JobState.Completed)) { Response.Headers.Append("Retry-After", $"{TrangaSettings.startNewJobTimeoutMs * retrieveChapterJobs.Count() * 2 / 1000:D}"); @@ -288,12 +318,12 @@ public class MangaController(PgsqlContext context, ILog Log) : Controller } /// - /// Configure the cut-off for Manga + /// Configure the cut-off for Obj /// - /// Manga-ID + /// Obj-ID /// Threshold (Chapter Number) /// - /// Manga with ID not found. + /// Obj with ID not found. /// Error during Database Operation [HttpPatch("{MangaId}/IgnoreChaptersBefore")] [ProducesResponseType(Status200OK)] @@ -319,10 +349,10 @@ public class MangaController(PgsqlContext context, ILog Log) : Controller } /// - /// Move Manga to different ToLibrary + /// Move Obj to different ToFileLibrary /// - /// Manga-ID - /// ToLibrary-Id + /// Obj-ID + /// ToFileLibrary-Id /// Folder is going to be moved /// MangaId or LibraryId not found /// Error during Database Operation diff --git a/API/Controllers/NotificationConnectorController.cs b/API/Controllers/NotificationConnectorController.cs index c0100e0..4ef4655 100644 --- a/API/Controllers/NotificationConnectorController.cs +++ b/API/Controllers/NotificationConnectorController.cs @@ -95,7 +95,7 @@ public class NotificationConnectorController(NotificationsContext context, ILog NotificationConnector gotifyConnector = new NotificationConnector(TokenGen.CreateToken("Gotify"), gotifyData.endpoint, - new Dictionary() { { "X-Gotify-Key", gotifyData.appToken } }, + new Dictionary() { { "X-Gotify-IDOnConnector", gotifyData.appToken } }, "POST", $"{{\"message\": \"%text\", \"title\": \"%title\", \"priority\": {gotifyData.priority}}}"); return CreateConnector(gotifyConnector); diff --git a/API/Controllers/QueryController.cs b/API/Controllers/QueryController.cs index f4d603f..bfa2160 100644 --- a/API/Controllers/QueryController.cs +++ b/API/Controllers/QueryController.cs @@ -69,18 +69,18 @@ public class QueryController(PgsqlContext context, ILog Log) : Controller /// /// AltTitle with ID not found [HttpGet("AltTitle/{AltTitleId}")] - [ProducesResponseType(Status200OK, "application/json")] + [ProducesResponseType(Status200OK, "application/json")] [ProducesResponseType(Status404NotFound)] public IActionResult GetAltTitle(string AltTitleId) { - MangaAltTitle? ret = context.AltTitles.Find(AltTitleId); + AltTitle? ret = context.AltTitles.Find(AltTitleId); if (ret is null) return NotFound(); return Ok(ret); }*/ /// - /// Returns all Manga with Tag + /// Returns all Obj with Tag /// /// /// diff --git a/API/Controllers/SearchController.cs b/API/Controllers/SearchController.cs index 5af0ed2..7b10ec3 100644 --- a/API/Controllers/SearchController.cs +++ b/API/Controllers/SearchController.cs @@ -1,7 +1,5 @@ using API.Schema; using API.Schema.Contexts; -using API.Schema.Jobs; -using API.Schema.MangaConnectors; using Asp.Versioning; using log4net; using Microsoft.AspNetCore.Mvc; @@ -18,7 +16,7 @@ namespace API.Controllers; public class SearchController(PgsqlContext context, ILog Log) : Controller { /// - /// Initiate a search for a Manga on a specific Connector + /// Initiate a search for a Obj on a specific Connector /// /// /// @@ -38,9 +36,9 @@ public class SearchController(PgsqlContext context, ILog Log) : Controller else if (connector.Enabled is false) return StatusCode(Status406NotAcceptable); - Manga[] mangas = connector.SearchManga(Query); + (Manga, MangaConnectorId)[] mangas = connector.SearchManga(Query); List retMangas = new(); - foreach (Manga manga in mangas) + foreach ((Manga manga, MangaConnectorId mcId) manga in mangas) { try { @@ -58,7 +56,7 @@ public class SearchController(PgsqlContext context, ILog Log) : Controller } /// - /// Search for a known Manga + /// Search for a known Obj /// /// /// @@ -73,12 +71,12 @@ public class SearchController(PgsqlContext context, ILog Log) : Controller } /// - /// Returns Manga from MangaConnector associated with URL + /// Returns Obj from MangaConnector associated with URL /// - /// Manga-Page URL + /// Obj-Page URL /// /// Multiple connectors found for URL - /// Manga not found + /// Obj not found /// Error during Database Operation [HttpPost("Url")] [ProducesResponseType(Status200OK, "application/json")] @@ -104,12 +102,13 @@ public class SearchController(PgsqlContext context, ILog Log) : Controller } } - private Manga? AddMangaToContext(Manga manga) + private Manga? AddMangaToContext((Manga, MangaConnectorId) manga) => AddMangaToContext(manga.Item1, manga.Item2, context); + + internal static Manga? AddMangaToContext(Manga addManga, MangaConnectorId addMcId, PgsqlContext context) { - context.Mangas.Load(); - context.Authors.Load(); - context.Tags.Load(); - context.MangaConnectors.Load(); + Manga manga = context.Mangas.Find(addManga.Key) ?? addManga; + MangaConnectorId mcId = context.MangaConnectorToManga.Find(addMcId.Key) ?? addMcId; + mcId.Obj = manga; IEnumerable mergedTags = manga.MangaTags.Select(mt => { @@ -120,26 +119,19 @@ public class SearchController(PgsqlContext context, ILog Log) : Controller IEnumerable mergedAuthors = manga.Authors.Select(ma => { - Author? inDb = context.Authors.Find(ma.AuthorId); + Author? inDb = context.Authors.Find(ma.Key); return inDb ?? ma; }); manga.Authors = mergedAuthors.ToList(); - + try { - - if (context.Mangas.Find(manga.MangaId) is { } r) - { - context.Mangas.Remove(r); - context.SaveChanges(); - } - context.Mangas.Add(manga); - context.Jobs.Add(new DownloadMangaCoverJob(manga)); + if(context.MangaConnectorToManga.Find(addMcId.Key) is null) + context.MangaConnectorToManga.Add(mcId); context.SaveChanges(); } catch (DbUpdateException e) { - Log.Error(e); return null; } return manga; diff --git a/API/Controllers/SettingsController.cs b/API/Controllers/SettingsController.cs index 2c060f3..3fdc2e3 100644 --- a/API/Controllers/SettingsController.cs +++ b/API/Controllers/SettingsController.cs @@ -1,12 +1,10 @@ -using System.Net.Http.Headers; -using API.MangaDownloadClients; +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; using Newtonsoft.Json.Linq; using static Microsoft.AspNetCore.Http.StatusCodes; @@ -210,14 +208,14 @@ public class SettingsController(PgsqlContext context, ILog Log) : Controller /// /// /// Placeholders: - /// %M Manga Name + /// %M Obj Name /// %V Volume /// %C Chapter /// %T Title /// %A Author (first in list) /// %I Chapter Internal ID - /// %i Manga Internal ID - /// %Y Year (Manga) + /// %i Obj Internal ID + /// %Y Year (Obj) /// /// ?_(...) replace _ with a value from above: /// Everything inside the braces will only be added if the value of %_ is not null @@ -235,14 +233,14 @@ public class SettingsController(PgsqlContext context, ILog Log) : Controller /// /// /// Placeholders: - /// %M Manga Name + /// %M Obj Name /// %V Volume /// %C Chapter /// %T Title /// %A Author (first in list) /// %I Chapter Internal ID - /// %i Manga Internal ID - /// %Y Year (Manga) + /// %i Obj Internal ID + /// %Y Year (Obj) /// /// ?_(...) replace _ with a value from above: /// Everything inside the braces will only be added if the value of %_ is not null @@ -271,7 +269,7 @@ public class SettingsController(PgsqlContext context, ILog Log) : Controller } /// - /// Creates a UpdateCoverJob for all Manga + /// Creates a UpdateCoverJob for all Obj /// /// Array of JobIds /// Error during Database Operation @@ -285,7 +283,7 @@ public class SettingsController(PgsqlContext context, ILog Log) : Controller Tranga.RemoveStaleFiles(context); List newJobs = context.Mangas.ToList().Select(m => new UpdateCoverJob(m, 0)).ToList(); context.Jobs.AddRange(newJobs); - return Ok(newJobs.Select(j => j.JobId)); + return Ok(newJobs.Select(j => j.Key)); } catch (Exception e) { diff --git a/API/JobQueueSortable.cs b/API/JobQueueSortable.cs deleted file mode 100644 index 0064f05..0000000 --- a/API/JobQueueSortable.cs +++ /dev/null @@ -1,47 +0,0 @@ -using API.Schema.Jobs; - -namespace API; - -internal static class JobQueueSorter -{ - public static readonly Dictionary JobTypePriority = new() - { - - { JobType.DownloadSingleChapterJob, 50 }, - { JobType.DownloadAvailableChaptersJob, 51 }, - { JobType.MoveFileOrFolderJob, 102 }, - { JobType.DownloadMangaCoverJob, 10 }, - { JobType.RetrieveChaptersJob, 52 }, - { JobType.UpdateChaptersDownloadedJob, 90 }, - { JobType.MoveMangaLibraryJob, 101 }, - { JobType.UpdateCoverJob, 11 }, - }; - - public static byte GetPriority(Job job) - { - return JobTypePriority[job.JobType]; - } - - public static byte GetPriority(JobType jobType) - { - return JobTypePriority[jobType]; - } - - public static IEnumerable Sort(this IEnumerable jobQueueSortables) - { - return jobQueueSortables.Order(); - } - - public static IEnumerable GetStartableJobs(this IEnumerable jobQueueSortables) - { - Job[] sorted = jobQueueSortables.Order().ToArray(); - // Job has to be due, no missing dependenices - // Index - 1, Index is first job that does not match requirements - IEnumerable<(int Index, Job Item)> index = sorted.Index(); - (int i, Job? item) = index.FirstOrDefault(job => - job.Item.NextExecution > DateTime.UtcNow || job.Item.GetDependencies().Any(j => !j.IsCompleted)); - if (item is null) - return sorted; - index. - } -} \ No newline at end of file diff --git a/API/MangaDownloadClients/ChromiumDownloadClient.cs b/API/MangaDownloadClients/ChromiumDownloadClient.cs index 228624e..acde118 100644 --- a/API/MangaDownloadClients/ChromiumDownloadClient.cs +++ b/API/MangaDownloadClients/ChromiumDownloadClient.cs @@ -43,7 +43,7 @@ internal class ChromiumDownloadClient : DownloadClient { Thread.Sleep(TimeSpan.FromHours(1)); Log.Debug("Removing stale pages"); - foreach ((IPage? key, DateTime value) in _openPages.Where(kv => kv.Value.Subtract(DateTime.Now) > TimeSpan.FromHours(1))) + foreach ((IPage key, DateTime _) in _openPages.Where(kv => kv.Value.Subtract(DateTime.Now) > TimeSpan.FromHours(1))) { Log.Debug($"Closing {key.Url}"); key.CloseAsync().Wait(); diff --git a/API/MangaDownloadClients/FlareSolverrDownloadClient.cs b/API/MangaDownloadClients/FlareSolverrDownloadClient.cs index b76e090..03069db 100644 --- a/API/MangaDownloadClients/FlareSolverrDownloadClient.cs +++ b/API/MangaDownloadClients/FlareSolverrDownloadClient.cs @@ -172,7 +172,7 @@ public class FlareSolverrDownloadClient : DownloadClient jsonString = pre.InnerText; return true; } - catch (JsonReaderException) + catch (Exception) { return false; } diff --git a/API/Migrations/pgsql/20250630182650_OofV2.1.Designer.cs b/API/Migrations/pgsql/20250630182650_OofV2.1.Designer.cs new file mode 100644 index 0000000..372412f --- /dev/null +++ b/API/Migrations/pgsql/20250630182650_OofV2.1.Designer.cs @@ -0,0 +1,795 @@ +// +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("20250630182650_OofV2.1")] + partial class OofV21 + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.5") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("API.Schema.Author", b => + { + b.Property("Key") + .HasColumnType("text"); + + b.Property("AuthorName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.HasKey("Key"); + + b.ToTable("Authors"); + }); + + modelBuilder.Entity("API.Schema.Chapter", b => + { + b.Property("Key") + .HasColumnType("text"); + + 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("VolumeNumber") + .HasColumnType("integer"); + + b.HasKey("Key"); + + b.HasIndex("ParentMangaId"); + + b.ToTable("Chapters"); + }); + + modelBuilder.Entity("API.Schema.FileLibrary", b => + { + b.Property("Key") + .HasColumnType("text"); + + b.Property("BasePath") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("LibraryName") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.HasKey("Key"); + + b.ToTable("LocalLibraries"); + }); + + modelBuilder.Entity("API.Schema.Jobs.Job", b => + { + b.Property("Key") + .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("Key"); + + b.HasIndex("ParentJobId"); + + b.ToTable("Jobs"); + + b.HasDiscriminator("JobType"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("API.Schema.Manga", b => + { + b.Property("Key") + .HasColumnType("text"); + + 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("IgnoreChaptersBefore") + .HasColumnType("real"); + + b.Property("LibraryId") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + 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("Year") + .HasColumnType("bigint"); + + b.HasKey("Key"); + + b.HasIndex("LibraryId"); + + b.ToTable("Mangas"); + }); + + modelBuilder.Entity("API.Schema.MangaConnectorId", b => + { + b.Property("Key") + .HasColumnType("text"); + + b.Property("IdOnConnectorSite") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("MangaConnectorName") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("ObjId") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("WebsiteUrl") + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.HasKey("Key"); + + b.HasIndex("MangaConnectorName"); + + b.HasIndex("ObjId"); + + b.ToTable("MangaConnectorToChapter"); + }); + + modelBuilder.Entity("API.Schema.MangaConnectorId", b => + { + b.Property("Key") + .HasColumnType("text"); + + b.Property("IdOnConnectorSite") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("MangaConnectorName") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("ObjId") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("WebsiteUrl") + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.HasKey("Key"); + + b.HasIndex("MangaConnectorName"); + + b.HasIndex("ObjId"); + + b.ToTable("MangaConnectorToManga"); + }); + + 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("text"); + + b.Property("MangaIds") + .HasColumnType("text"); + + b.HasKey("AuthorIds", "MangaIds"); + + b.HasIndex("MangaIds"); + + b.ToTable("AuthorToManga"); + }); + + modelBuilder.Entity("JobJob", b => + { + b.Property("DependsOnJobsKey") + .HasColumnType("text"); + + b.Property("JobKey") + .HasColumnType("text"); + + b.HasKey("DependsOnJobsKey", "JobKey"); + + b.HasIndex("JobKey"); + + b.ToTable("JobJob"); + }); + + modelBuilder.Entity("MangaTagToManga", b => + { + b.Property("MangaTagIds") + .HasColumnType("character varying(64)"); + + b.Property("MangaIds") + .HasColumnType("text"); + + 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("UpdateChaptersDownloadedJob_MangaId"); + }); + + b.HasDiscriminator().HasValue((byte)6); + }); + + modelBuilder.Entity("API.Schema.Jobs.UpdateCoverJob", 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("UpdateCoverJob_MangaId"); + }); + + b.HasDiscriminator().HasValue((byte)9); + }); + + 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.FileLibrary", "Library") + .WithMany() + .HasForeignKey("LibraryId") + .OnDelete(DeleteBehavior.SetNull); + + b.OwnsMany("API.Schema.AltTitle", "AltTitles", b1 => + { + b1.Property("Key") + .HasColumnType("text"); + + b1.Property("Language") + .IsRequired() + .HasMaxLength(8) + .HasColumnType("character varying(8)"); + + b1.Property("MangaKey") + .IsRequired() + .HasColumnType("text"); + + b1.Property("Title") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b1.HasKey("Key"); + + b1.HasIndex("MangaKey"); + + b1.ToTable("AltTitle"); + + b1.WithOwner() + .HasForeignKey("MangaKey"); + }); + + b.OwnsMany("API.Schema.Link", "Links", b1 => + { + b1.Property("Key") + .HasColumnType("text"); + + b1.Property("LinkProvider") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b1.Property("LinkUrl") + .IsRequired() + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b1.Property("MangaKey") + .IsRequired() + .HasColumnType("text"); + + b1.HasKey("Key"); + + b1.HasIndex("MangaKey"); + + b1.ToTable("Link"); + + b1.WithOwner() + .HasForeignKey("MangaKey"); + }); + + b.Navigation("AltTitles"); + + b.Navigation("Library"); + + b.Navigation("Links"); + }); + + modelBuilder.Entity("API.Schema.MangaConnectorId", b => + { + b.HasOne("API.Schema.MangaConnectors.MangaConnector", "MangaConnector") + .WithMany() + .HasForeignKey("MangaConnectorName") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Schema.Chapter", "Obj") + .WithMany("MangaConnectorIds") + .HasForeignKey("ObjId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("MangaConnector"); + + b.Navigation("Obj"); + }); + + modelBuilder.Entity("API.Schema.MangaConnectorId", b => + { + b.HasOne("API.Schema.MangaConnectors.MangaConnector", "MangaConnector") + .WithMany() + .HasForeignKey("MangaConnectorName") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Schema.Manga", "Obj") + .WithMany("MangaConnectorIds") + .HasForeignKey("ObjId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MangaConnector"); + + b.Navigation("Obj"); + }); + + 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("DependsOnJobsKey") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Schema.Jobs.Job", null) + .WithMany() + .HasForeignKey("JobKey") + .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.FileLibrary", "ToFileLibrary") + .WithMany() + .HasForeignKey("ToLibraryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Manga"); + + b.Navigation("ToFileLibrary"); + }); + + 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.UpdateCoverJob", b => + { + b.HasOne("API.Schema.Manga", "Manga") + .WithMany() + .HasForeignKey("MangaId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Manga"); + }); + + modelBuilder.Entity("API.Schema.Chapter", b => + { + b.Navigation("MangaConnectorIds"); + }); + + modelBuilder.Entity("API.Schema.Manga", b => + { + b.Navigation("Chapters"); + + b.Navigation("MangaConnectorIds"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/API/Migrations/pgsql/20250630182650_OofV2.1.cs b/API/Migrations/pgsql/20250630182650_OofV2.1.cs new file mode 100644 index 0000000..a59670f --- /dev/null +++ b/API/Migrations/pgsql/20250630182650_OofV2.1.cs @@ -0,0 +1,1055 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace API.Migrations.pgsql +{ + /// + public partial class OofV21 : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_AuthorToManga_Authors_AuthorIds", + table: "AuthorToManga"); + + migrationBuilder.DropForeignKey( + name: "FK_AuthorToManga_Mangas_MangaIds", + table: "AuthorToManga"); + + migrationBuilder.DropForeignKey( + name: "FK_Chapters_Mangas_ParentMangaId", + table: "Chapters"); + + migrationBuilder.DropForeignKey( + name: "FK_JobJob_Jobs_DependsOnJobsJobId", + table: "JobJob"); + + migrationBuilder.DropForeignKey( + name: "FK_JobJob_Jobs_JobId", + table: "JobJob"); + + migrationBuilder.DropForeignKey( + name: "FK_Jobs_Chapters_ChapterId", + table: "Jobs"); + + migrationBuilder.DropForeignKey( + name: "FK_Jobs_Jobs_ParentJobId", + table: "Jobs"); + + migrationBuilder.DropForeignKey( + name: "FK_Jobs_LocalLibraries_ToLibraryId", + 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_MoveMangaLibraryJob_MangaId", + table: "Jobs"); + + migrationBuilder.DropForeignKey( + name: "FK_Jobs_Mangas_RetrieveChaptersJob_MangaId", + table: "Jobs"); + + migrationBuilder.DropForeignKey( + name: "FK_Jobs_Mangas_UpdateChaptersDownloadedJob_MangaId", + table: "Jobs"); + + migrationBuilder.DropForeignKey( + name: "FK_Jobs_Mangas_UpdateCoverJob_MangaId", + table: "Jobs"); + + migrationBuilder.DropForeignKey( + name: "FK_Link_Mangas_MangaId", + table: "Link"); + + migrationBuilder.DropForeignKey( + name: "FK_Mangas_LocalLibraries_LibraryId", + table: "Mangas"); + + migrationBuilder.DropForeignKey( + name: "FK_Mangas_MangaConnectors_MangaConnectorName", + table: "Mangas"); + + migrationBuilder.DropForeignKey( + name: "FK_MangaTagToManga_Mangas_MangaIds", + table: "MangaTagToManga"); + + migrationBuilder.DropTable( + name: "MangaAltTitle"); + + migrationBuilder.DropPrimaryKey( + name: "PK_Mangas", + table: "Mangas"); + + migrationBuilder.DropIndex( + name: "IX_Mangas_MangaConnectorName", + table: "Mangas"); + + migrationBuilder.DropPrimaryKey( + name: "PK_LocalLibraries", + table: "LocalLibraries"); + + migrationBuilder.DropPrimaryKey( + name: "PK_Link", + table: "Link"); + + migrationBuilder.DropIndex( + name: "IX_Link_MangaId", + table: "Link"); + + migrationBuilder.DropPrimaryKey( + name: "PK_Jobs", + table: "Jobs"); + + migrationBuilder.DropPrimaryKey( + name: "PK_JobJob", + table: "JobJob"); + + migrationBuilder.DropIndex( + name: "IX_JobJob_JobId", + table: "JobJob"); + + migrationBuilder.DropPrimaryKey( + name: "PK_Chapters", + table: "Chapters"); + + migrationBuilder.DropPrimaryKey( + name: "PK_Authors", + table: "Authors"); + + migrationBuilder.DropColumn( + name: "MangaId", + table: "Mangas"); + + migrationBuilder.DropColumn( + name: "IdOnConnectorSite", + table: "Mangas"); + + migrationBuilder.DropColumn( + name: "MangaConnectorName", + table: "Mangas"); + + migrationBuilder.DropColumn( + name: "WebsiteUrl", + table: "Mangas"); + + migrationBuilder.DropColumn( + name: "LocalLibraryId", + table: "LocalLibraries"); + + migrationBuilder.DropColumn( + name: "LinkId", + table: "Link"); + + migrationBuilder.DropColumn( + name: "MangaId", + table: "Link"); + + migrationBuilder.DropColumn( + name: "JobId", + table: "Jobs"); + + migrationBuilder.DropColumn( + name: "DependsOnJobsJobId", + table: "JobJob"); + + migrationBuilder.DropColumn( + name: "JobId", + table: "JobJob"); + + migrationBuilder.DropColumn( + name: "ChapterId", + table: "Chapters"); + + migrationBuilder.DropColumn( + name: "IdOnConnectorSite", + table: "Chapters"); + + migrationBuilder.DropColumn( + name: "Url", + table: "Chapters"); + + migrationBuilder.DropColumn( + name: "AuthorId", + table: "Authors"); + + migrationBuilder.AlterColumn( + name: "MangaIds", + table: "MangaTagToManga", + type: "text", + nullable: false, + oldClrType: typeof(string), + oldType: "character varying(64)"); + + migrationBuilder.AddColumn( + name: "Key", + table: "Mangas", + type: "text", + nullable: false, + defaultValue: ""); + + migrationBuilder.AddColumn( + name: "Key", + table: "LocalLibraries", + type: "text", + nullable: false, + defaultValue: ""); + + migrationBuilder.AddColumn( + name: "Key", + table: "Link", + type: "text", + nullable: false, + defaultValue: ""); + + migrationBuilder.AddColumn( + name: "MangaKey", + table: "Link", + type: "text", + nullable: false, + defaultValue: ""); + + migrationBuilder.AddColumn( + name: "Key", + table: "Jobs", + type: "text", + nullable: false, + defaultValue: ""); + + migrationBuilder.AddColumn( + name: "DependsOnJobsKey", + table: "JobJob", + type: "text", + nullable: false, + defaultValue: ""); + + migrationBuilder.AddColumn( + name: "JobKey", + table: "JobJob", + type: "text", + nullable: false, + defaultValue: ""); + + migrationBuilder.AddColumn( + name: "Key", + table: "Chapters", + type: "text", + nullable: false, + defaultValue: ""); + + migrationBuilder.AlterColumn( + name: "MangaIds", + table: "AuthorToManga", + type: "text", + nullable: false, + oldClrType: typeof(string), + oldType: "character varying(64)"); + + migrationBuilder.AlterColumn( + name: "AuthorIds", + table: "AuthorToManga", + type: "text", + nullable: false, + oldClrType: typeof(string), + oldType: "character varying(64)"); + + migrationBuilder.AddColumn( + name: "Key", + table: "Authors", + type: "text", + nullable: false, + defaultValue: ""); + + migrationBuilder.AddPrimaryKey( + name: "PK_Mangas", + table: "Mangas", + column: "Key"); + + migrationBuilder.AddPrimaryKey( + name: "PK_LocalLibraries", + table: "LocalLibraries", + column: "Key"); + + migrationBuilder.AddPrimaryKey( + name: "PK_Link", + table: "Link", + column: "Key"); + + migrationBuilder.AddPrimaryKey( + name: "PK_Jobs", + table: "Jobs", + column: "Key"); + + migrationBuilder.AddPrimaryKey( + name: "PK_JobJob", + table: "JobJob", + columns: new[] { "DependsOnJobsKey", "JobKey" }); + + migrationBuilder.AddPrimaryKey( + name: "PK_Chapters", + table: "Chapters", + column: "Key"); + + migrationBuilder.AddPrimaryKey( + name: "PK_Authors", + table: "Authors", + column: "Key"); + + migrationBuilder.CreateTable( + name: "AltTitle", + columns: table => new + { + Key = table.Column(type: "text", nullable: false), + Language = table.Column(type: "character varying(8)", maxLength: 8, nullable: false), + Title = table.Column(type: "character varying(256)", maxLength: 256, nullable: false), + MangaKey = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AltTitle", x => x.Key); + table.ForeignKey( + name: "FK_AltTitle_Mangas_MangaKey", + column: x => x.MangaKey, + principalTable: "Mangas", + principalColumn: "Key", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "MangaConnectorToChapter", + columns: table => new + { + Key = table.Column(type: "text", nullable: false), + ObjId = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), + MangaConnectorName = table.Column(type: "character varying(32)", maxLength: 32, nullable: false), + IdOnConnectorSite = table.Column(type: "character varying(256)", maxLength: 256, nullable: false), + WebsiteUrl = table.Column(type: "character varying(512)", maxLength: 512, nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_MangaConnectorToChapter", x => x.Key); + table.ForeignKey( + name: "FK_MangaConnectorToChapter_Chapters_ObjId", + column: x => x.ObjId, + principalTable: "Chapters", + principalColumn: "Key"); + table.ForeignKey( + name: "FK_MangaConnectorToChapter_MangaConnectors_MangaConnectorName", + column: x => x.MangaConnectorName, + principalTable: "MangaConnectors", + principalColumn: "Name", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "MangaConnectorToManga", + columns: table => new + { + Key = table.Column(type: "text", nullable: false), + ObjId = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), + MangaConnectorName = table.Column(type: "character varying(32)", maxLength: 32, nullable: false), + IdOnConnectorSite = table.Column(type: "character varying(256)", maxLength: 256, nullable: false), + WebsiteUrl = table.Column(type: "character varying(512)", maxLength: 512, nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_MangaConnectorToManga", x => x.Key); + table.ForeignKey( + name: "FK_MangaConnectorToManga_MangaConnectors_MangaConnectorName", + column: x => x.MangaConnectorName, + principalTable: "MangaConnectors", + principalColumn: "Name", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_MangaConnectorToManga_Mangas_ObjId", + column: x => x.ObjId, + principalTable: "Mangas", + principalColumn: "Key", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_Link_MangaKey", + table: "Link", + column: "MangaKey"); + + migrationBuilder.CreateIndex( + name: "IX_JobJob_JobKey", + table: "JobJob", + column: "JobKey"); + + migrationBuilder.CreateIndex( + name: "IX_AltTitle_MangaKey", + table: "AltTitle", + column: "MangaKey"); + + migrationBuilder.CreateIndex( + name: "IX_MangaConnectorToChapter_MangaConnectorName", + table: "MangaConnectorToChapter", + column: "MangaConnectorName"); + + migrationBuilder.CreateIndex( + name: "IX_MangaConnectorToChapter_ObjId", + table: "MangaConnectorToChapter", + column: "ObjId"); + + migrationBuilder.CreateIndex( + name: "IX_MangaConnectorToManga_MangaConnectorName", + table: "MangaConnectorToManga", + column: "MangaConnectorName"); + + migrationBuilder.CreateIndex( + name: "IX_MangaConnectorToManga_ObjId", + table: "MangaConnectorToManga", + column: "ObjId"); + + migrationBuilder.AddForeignKey( + name: "FK_AuthorToManga_Authors_AuthorIds", + table: "AuthorToManga", + column: "AuthorIds", + principalTable: "Authors", + principalColumn: "Key", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_AuthorToManga_Mangas_MangaIds", + table: "AuthorToManga", + column: "MangaIds", + principalTable: "Mangas", + principalColumn: "Key", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_Chapters_Mangas_ParentMangaId", + table: "Chapters", + column: "ParentMangaId", + principalTable: "Mangas", + principalColumn: "Key", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_JobJob_Jobs_DependsOnJobsKey", + table: "JobJob", + column: "DependsOnJobsKey", + principalTable: "Jobs", + principalColumn: "Key", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_JobJob_Jobs_JobKey", + table: "JobJob", + column: "JobKey", + principalTable: "Jobs", + principalColumn: "Key", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_Jobs_Chapters_ChapterId", + table: "Jobs", + column: "ChapterId", + principalTable: "Chapters", + principalColumn: "Key", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_Jobs_Jobs_ParentJobId", + table: "Jobs", + column: "ParentJobId", + principalTable: "Jobs", + principalColumn: "Key", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_Jobs_LocalLibraries_ToLibraryId", + table: "Jobs", + column: "ToLibraryId", + principalTable: "LocalLibraries", + principalColumn: "Key", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_Jobs_Mangas_DownloadAvailableChaptersJob_MangaId", + table: "Jobs", + column: "DownloadAvailableChaptersJob_MangaId", + principalTable: "Mangas", + principalColumn: "Key", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_Jobs_Mangas_MangaId", + table: "Jobs", + column: "MangaId", + principalTable: "Mangas", + principalColumn: "Key", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_Jobs_Mangas_MoveMangaLibraryJob_MangaId", + table: "Jobs", + column: "MoveMangaLibraryJob_MangaId", + principalTable: "Mangas", + principalColumn: "Key", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_Jobs_Mangas_RetrieveChaptersJob_MangaId", + table: "Jobs", + column: "RetrieveChaptersJob_MangaId", + principalTable: "Mangas", + principalColumn: "Key", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_Jobs_Mangas_UpdateChaptersDownloadedJob_MangaId", + table: "Jobs", + column: "UpdateChaptersDownloadedJob_MangaId", + principalTable: "Mangas", + principalColumn: "Key", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_Jobs_Mangas_UpdateCoverJob_MangaId", + table: "Jobs", + column: "UpdateCoverJob_MangaId", + principalTable: "Mangas", + principalColumn: "Key", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_Link_Mangas_MangaKey", + table: "Link", + column: "MangaKey", + principalTable: "Mangas", + principalColumn: "Key", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_Mangas_LocalLibraries_LibraryId", + table: "Mangas", + column: "LibraryId", + principalTable: "LocalLibraries", + principalColumn: "Key", + onDelete: ReferentialAction.SetNull); + + migrationBuilder.AddForeignKey( + name: "FK_MangaTagToManga_Mangas_MangaIds", + table: "MangaTagToManga", + column: "MangaIds", + principalTable: "Mangas", + principalColumn: "Key", + onDelete: ReferentialAction.Cascade); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_AuthorToManga_Authors_AuthorIds", + table: "AuthorToManga"); + + migrationBuilder.DropForeignKey( + name: "FK_AuthorToManga_Mangas_MangaIds", + table: "AuthorToManga"); + + migrationBuilder.DropForeignKey( + name: "FK_Chapters_Mangas_ParentMangaId", + table: "Chapters"); + + migrationBuilder.DropForeignKey( + name: "FK_JobJob_Jobs_DependsOnJobsKey", + table: "JobJob"); + + migrationBuilder.DropForeignKey( + name: "FK_JobJob_Jobs_JobKey", + table: "JobJob"); + + migrationBuilder.DropForeignKey( + name: "FK_Jobs_Chapters_ChapterId", + table: "Jobs"); + + migrationBuilder.DropForeignKey( + name: "FK_Jobs_Jobs_ParentJobId", + table: "Jobs"); + + migrationBuilder.DropForeignKey( + name: "FK_Jobs_LocalLibraries_ToLibraryId", + 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_MoveMangaLibraryJob_MangaId", + table: "Jobs"); + + migrationBuilder.DropForeignKey( + name: "FK_Jobs_Mangas_RetrieveChaptersJob_MangaId", + table: "Jobs"); + + migrationBuilder.DropForeignKey( + name: "FK_Jobs_Mangas_UpdateChaptersDownloadedJob_MangaId", + table: "Jobs"); + + migrationBuilder.DropForeignKey( + name: "FK_Jobs_Mangas_UpdateCoverJob_MangaId", + table: "Jobs"); + + migrationBuilder.DropForeignKey( + name: "FK_Link_Mangas_MangaKey", + table: "Link"); + + migrationBuilder.DropForeignKey( + name: "FK_Mangas_LocalLibraries_LibraryId", + table: "Mangas"); + + migrationBuilder.DropForeignKey( + name: "FK_MangaTagToManga_Mangas_MangaIds", + table: "MangaTagToManga"); + + migrationBuilder.DropTable( + name: "AltTitle"); + + migrationBuilder.DropTable( + name: "MangaConnectorToChapter"); + + migrationBuilder.DropTable( + name: "MangaConnectorToManga"); + + migrationBuilder.DropPrimaryKey( + name: "PK_Mangas", + table: "Mangas"); + + migrationBuilder.DropPrimaryKey( + name: "PK_LocalLibraries", + table: "LocalLibraries"); + + migrationBuilder.DropPrimaryKey( + name: "PK_Link", + table: "Link"); + + migrationBuilder.DropIndex( + name: "IX_Link_MangaKey", + table: "Link"); + + migrationBuilder.DropPrimaryKey( + name: "PK_Jobs", + table: "Jobs"); + + migrationBuilder.DropPrimaryKey( + name: "PK_JobJob", + table: "JobJob"); + + migrationBuilder.DropIndex( + name: "IX_JobJob_JobKey", + table: "JobJob"); + + migrationBuilder.DropPrimaryKey( + name: "PK_Chapters", + table: "Chapters"); + + migrationBuilder.DropPrimaryKey( + name: "PK_Authors", + table: "Authors"); + + migrationBuilder.DropColumn( + name: "Key", + table: "Mangas"); + + migrationBuilder.DropColumn( + name: "Key", + table: "LocalLibraries"); + + migrationBuilder.DropColumn( + name: "Key", + table: "Link"); + + migrationBuilder.DropColumn( + name: "MangaKey", + table: "Link"); + + migrationBuilder.DropColumn( + name: "Key", + table: "Jobs"); + + migrationBuilder.DropColumn( + name: "DependsOnJobsKey", + table: "JobJob"); + + migrationBuilder.DropColumn( + name: "JobKey", + table: "JobJob"); + + migrationBuilder.DropColumn( + name: "Key", + table: "Chapters"); + + migrationBuilder.DropColumn( + name: "Key", + table: "Authors"); + + migrationBuilder.AlterColumn( + name: "MangaIds", + table: "MangaTagToManga", + type: "character varying(64)", + nullable: false, + oldClrType: typeof(string), + oldType: "text"); + + migrationBuilder.AddColumn( + name: "MangaId", + table: "Mangas", + type: "character varying(64)", + maxLength: 64, + nullable: false, + defaultValue: ""); + + migrationBuilder.AddColumn( + name: "IdOnConnectorSite", + table: "Mangas", + type: "character varying(256)", + maxLength: 256, + nullable: false, + defaultValue: ""); + + migrationBuilder.AddColumn( + name: "MangaConnectorName", + table: "Mangas", + type: "character varying(32)", + maxLength: 32, + nullable: false, + defaultValue: ""); + + migrationBuilder.AddColumn( + name: "WebsiteUrl", + table: "Mangas", + type: "character varying(512)", + maxLength: 512, + nullable: false, + defaultValue: ""); + + migrationBuilder.AddColumn( + name: "LocalLibraryId", + table: "LocalLibraries", + type: "character varying(64)", + maxLength: 64, + nullable: false, + defaultValue: ""); + + migrationBuilder.AddColumn( + name: "LinkId", + table: "Link", + type: "character varying(64)", + maxLength: 64, + nullable: false, + defaultValue: ""); + + migrationBuilder.AddColumn( + name: "MangaId", + table: "Link", + type: "character varying(64)", + nullable: false, + defaultValue: ""); + + migrationBuilder.AddColumn( + name: "JobId", + table: "Jobs", + type: "character varying(64)", + maxLength: 64, + nullable: false, + defaultValue: ""); + + migrationBuilder.AddColumn( + name: "DependsOnJobsJobId", + table: "JobJob", + type: "character varying(64)", + nullable: false, + defaultValue: ""); + + migrationBuilder.AddColumn( + name: "JobId", + table: "JobJob", + type: "character varying(64)", + nullable: false, + defaultValue: ""); + + migrationBuilder.AddColumn( + name: "ChapterId", + table: "Chapters", + type: "character varying(64)", + maxLength: 64, + nullable: false, + defaultValue: ""); + + migrationBuilder.AddColumn( + name: "IdOnConnectorSite", + table: "Chapters", + type: "character varying(256)", + maxLength: 256, + nullable: true); + + migrationBuilder.AddColumn( + name: "Url", + table: "Chapters", + type: "character varying(2048)", + maxLength: 2048, + nullable: false, + defaultValue: ""); + + migrationBuilder.AlterColumn( + name: "MangaIds", + table: "AuthorToManga", + type: "character varying(64)", + nullable: false, + oldClrType: typeof(string), + oldType: "text"); + + migrationBuilder.AlterColumn( + name: "AuthorIds", + table: "AuthorToManga", + type: "character varying(64)", + nullable: false, + oldClrType: typeof(string), + oldType: "text"); + + migrationBuilder.AddColumn( + name: "AuthorId", + table: "Authors", + type: "character varying(64)", + maxLength: 64, + nullable: false, + defaultValue: ""); + + migrationBuilder.AddPrimaryKey( + name: "PK_Mangas", + table: "Mangas", + column: "MangaId"); + + migrationBuilder.AddPrimaryKey( + name: "PK_LocalLibraries", + table: "LocalLibraries", + column: "LocalLibraryId"); + + migrationBuilder.AddPrimaryKey( + name: "PK_Link", + table: "Link", + column: "LinkId"); + + migrationBuilder.AddPrimaryKey( + name: "PK_Jobs", + table: "Jobs", + column: "JobId"); + + migrationBuilder.AddPrimaryKey( + name: "PK_JobJob", + table: "JobJob", + columns: new[] { "DependsOnJobsJobId", "JobId" }); + + migrationBuilder.AddPrimaryKey( + name: "PK_Chapters", + table: "Chapters", + column: "ChapterId"); + + migrationBuilder.AddPrimaryKey( + name: "PK_Authors", + table: "Authors", + column: "AuthorId"); + + migrationBuilder.CreateTable( + name: "MangaAltTitle", + 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), + MangaId = table.Column(type: "character varying(64)", nullable: false), + Title = table.Column(type: "character varying(256)", maxLength: 256, nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_MangaAltTitle", x => x.AltTitleId); + table.ForeignKey( + name: "FK_MangaAltTitle_Mangas_MangaId", + column: x => x.MangaId, + principalTable: "Mangas", + principalColumn: "MangaId", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_Mangas_MangaConnectorName", + table: "Mangas", + column: "MangaConnectorName"); + + migrationBuilder.CreateIndex( + name: "IX_Link_MangaId", + table: "Link", + column: "MangaId"); + + migrationBuilder.CreateIndex( + name: "IX_JobJob_JobId", + table: "JobJob", + column: "JobId"); + + migrationBuilder.CreateIndex( + name: "IX_MangaAltTitle_MangaId", + table: "MangaAltTitle", + column: "MangaId"); + + migrationBuilder.AddForeignKey( + name: "FK_AuthorToManga_Authors_AuthorIds", + table: "AuthorToManga", + column: "AuthorIds", + principalTable: "Authors", + principalColumn: "AuthorId", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_AuthorToManga_Mangas_MangaIds", + table: "AuthorToManga", + column: "MangaIds", + principalTable: "Mangas", + principalColumn: "MangaId", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_Chapters_Mangas_ParentMangaId", + table: "Chapters", + column: "ParentMangaId", + principalTable: "Mangas", + principalColumn: "MangaId", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_JobJob_Jobs_DependsOnJobsJobId", + table: "JobJob", + column: "DependsOnJobsJobId", + principalTable: "Jobs", + principalColumn: "JobId", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_JobJob_Jobs_JobId", + table: "JobJob", + column: "JobId", + principalTable: "Jobs", + principalColumn: "JobId", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_Jobs_Chapters_ChapterId", + table: "Jobs", + column: "ChapterId", + principalTable: "Chapters", + principalColumn: "ChapterId", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_Jobs_Jobs_ParentJobId", + table: "Jobs", + column: "ParentJobId", + principalTable: "Jobs", + principalColumn: "JobId", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_Jobs_LocalLibraries_ToLibraryId", + table: "Jobs", + column: "ToLibraryId", + principalTable: "LocalLibraries", + principalColumn: "LocalLibraryId", + 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_MoveMangaLibraryJob_MangaId", + table: "Jobs", + column: "MoveMangaLibraryJob_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_UpdateChaptersDownloadedJob_MangaId", + table: "Jobs", + column: "UpdateChaptersDownloadedJob_MangaId", + principalTable: "Mangas", + principalColumn: "MangaId", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_Jobs_Mangas_UpdateCoverJob_MangaId", + table: "Jobs", + column: "UpdateCoverJob_MangaId", + principalTable: "Mangas", + principalColumn: "MangaId", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_Link_Mangas_MangaId", + table: "Link", + column: "MangaId", + principalTable: "Mangas", + principalColumn: "MangaId", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_Mangas_LocalLibraries_LibraryId", + table: "Mangas", + column: "LibraryId", + principalTable: "LocalLibraries", + principalColumn: "LocalLibraryId", + onDelete: ReferentialAction.SetNull); + + migrationBuilder.AddForeignKey( + name: "FK_Mangas_MangaConnectors_MangaConnectorName", + table: "Mangas", + column: "MangaConnectorName", + principalTable: "MangaConnectors", + principalColumn: "Name", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_MangaTagToManga_Mangas_MangaIds", + table: "MangaTagToManga", + column: "MangaIds", + principalTable: "Mangas", + principalColumn: "MangaId", + onDelete: ReferentialAction.Cascade); + } + } +} diff --git a/API/Migrations/pgsql/PgsqlContextModelSnapshot.cs b/API/Migrations/pgsql/PgsqlContextModelSnapshot.cs index c6044a7..1272fa2 100644 --- a/API/Migrations/pgsql/PgsqlContextModelSnapshot.cs +++ b/API/Migrations/pgsql/PgsqlContextModelSnapshot.cs @@ -24,25 +24,23 @@ namespace API.Migrations.pgsql modelBuilder.Entity("API.Schema.Author", b => { - b.Property("AuthorId") - .HasMaxLength(64) - .HasColumnType("character varying(64)"); + b.Property("Key") + .HasColumnType("text"); b.Property("AuthorName") .IsRequired() .HasMaxLength(128) .HasColumnType("character varying(128)"); - b.HasKey("AuthorId"); + b.HasKey("Key"); b.ToTable("Authors"); }); modelBuilder.Entity("API.Schema.Chapter", b => { - b.Property("ChapterId") - .HasMaxLength(64) - .HasColumnType("character varying(64)"); + b.Property("Key") + .HasColumnType("text"); b.Property("ChapterNumber") .IsRequired() @@ -57,38 +55,49 @@ namespace API.Migrations.pgsql .HasMaxLength(256) .HasColumnType("character varying(256)"); - b.Property("IdOnConnectorSite") - .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.HasKey("Key"); b.HasIndex("ParentMangaId"); b.ToTable("Chapters"); }); + modelBuilder.Entity("API.Schema.FileLibrary", b => + { + b.Property("Key") + .HasColumnType("text"); + + b.Property("BasePath") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("LibraryName") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.HasKey("Key"); + + b.ToTable("LocalLibraries"); + }); + modelBuilder.Entity("API.Schema.Jobs.Job", b => { - b.Property("JobId") - .HasMaxLength(64) - .HasColumnType("character varying(64)"); + b.Property("Key") + .HasColumnType("text"); b.Property("Enabled") .HasColumnType("boolean"); @@ -109,7 +118,7 @@ namespace API.Migrations.pgsql b.Property("state") .HasColumnType("smallint"); - b.HasKey("JobId"); + b.HasKey("Key"); b.HasIndex("ParentJobId"); @@ -120,32 +129,10 @@ namespace API.Migrations.pgsql 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("Key") + .HasColumnType("text"); b.Property("CoverFileNameInCache") .HasMaxLength(512) @@ -165,11 +152,6 @@ namespace API.Migrations.pgsql .HasMaxLength(1024) .HasColumnType("character varying(1024)"); - b.Property("IdOnConnectorSite") - .IsRequired() - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - b.Property("IgnoreChaptersBefore") .HasColumnType("real"); @@ -177,11 +159,6 @@ namespace API.Migrations.pgsql .HasMaxLength(64) .HasColumnType("character varying(64)"); - b.Property("MangaConnectorName") - .IsRequired() - .HasMaxLength(32) - .HasColumnType("character varying(32)"); - b.Property("Name") .IsRequired() .HasMaxLength(512) @@ -194,21 +171,80 @@ namespace API.Migrations.pgsql b.Property("ReleaseStatus") .HasColumnType("smallint"); - b.Property("WebsiteUrl") - .IsRequired() - .HasMaxLength(512) - .HasColumnType("character varying(512)"); - b.Property("Year") .HasColumnType("bigint"); - b.HasKey("MangaId"); + b.HasKey("Key"); b.HasIndex("LibraryId"); + b.ToTable("Mangas"); + }); + + modelBuilder.Entity("API.Schema.MangaConnectorId", b => + { + b.Property("Key") + .HasColumnType("text"); + + b.Property("IdOnConnectorSite") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("MangaConnectorName") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("ObjId") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("WebsiteUrl") + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.HasKey("Key"); + b.HasIndex("MangaConnectorName"); - b.ToTable("Mangas"); + b.HasIndex("ObjId"); + + b.ToTable("MangaConnectorToChapter"); + }); + + modelBuilder.Entity("API.Schema.MangaConnectorId", b => + { + b.Property("Key") + .HasColumnType("text"); + + b.Property("IdOnConnectorSite") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("MangaConnectorName") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("ObjId") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("WebsiteUrl") + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.HasKey("Key"); + + b.HasIndex("MangaConnectorName"); + + b.HasIndex("ObjId"); + + b.ToTable("MangaConnectorToManga"); }); modelBuilder.Entity("API.Schema.MangaConnectors.MangaConnector", b => @@ -258,10 +294,10 @@ namespace API.Migrations.pgsql modelBuilder.Entity("AuthorToManga", b => { b.Property("AuthorIds") - .HasColumnType("character varying(64)"); + .HasColumnType("text"); b.Property("MangaIds") - .HasColumnType("character varying(64)"); + .HasColumnType("text"); b.HasKey("AuthorIds", "MangaIds"); @@ -272,15 +308,15 @@ namespace API.Migrations.pgsql modelBuilder.Entity("JobJob", b => { - b.Property("DependsOnJobsJobId") - .HasColumnType("character varying(64)"); + b.Property("DependsOnJobsKey") + .HasColumnType("text"); - b.Property("JobId") - .HasColumnType("character varying(64)"); + b.Property("JobKey") + .HasColumnType("text"); - b.HasKey("DependsOnJobsJobId", "JobId"); + b.HasKey("DependsOnJobsKey", "JobKey"); - b.HasIndex("JobId"); + b.HasIndex("JobKey"); b.ToTable("JobJob"); }); @@ -291,7 +327,7 @@ namespace API.Migrations.pgsql .HasColumnType("character varying(64)"); b.Property("MangaIds") - .HasColumnType("character varying(64)"); + .HasColumnType("text"); b.HasKey("MangaTagIds", "MangaIds"); @@ -501,22 +537,44 @@ namespace API.Migrations.pgsql modelBuilder.Entity("API.Schema.Manga", b => { - b.HasOne("API.Schema.LocalLibrary", "Library") + b.HasOne("API.Schema.FileLibrary", "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.AltTitle", "AltTitles", b1 => + { + b1.Property("Key") + .HasColumnType("text"); + + b1.Property("Language") + .IsRequired() + .HasMaxLength(8) + .HasColumnType("character varying(8)"); + + b1.Property("MangaKey") + .IsRequired() + .HasColumnType("text"); + + b1.Property("Title") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b1.HasKey("Key"); + + b1.HasIndex("MangaKey"); + + b1.ToTable("AltTitle"); + + b1.WithOwner() + .HasForeignKey("MangaKey"); + }); b.OwnsMany("API.Schema.Link", "Links", b1 => { - b1.Property("LinkId") - .HasMaxLength(64) - .HasColumnType("character varying(64)"); + b1.Property("Key") + .HasColumnType("text"); b1.Property("LinkProvider") .IsRequired() @@ -528,48 +586,18 @@ namespace API.Migrations.pgsql .HasMaxLength(2048) .HasColumnType("character varying(2048)"); - b1.Property("MangaId") + b1.Property("MangaKey") .IsRequired() - .HasColumnType("character varying(64)"); + .HasColumnType("text"); - b1.HasKey("LinkId"); + b1.HasKey("Key"); - b1.HasIndex("MangaId"); + b1.HasIndex("MangaKey"); 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"); + .HasForeignKey("MangaKey"); }); b.Navigation("AltTitles"); @@ -577,8 +605,44 @@ namespace API.Migrations.pgsql b.Navigation("Library"); b.Navigation("Links"); + }); + + modelBuilder.Entity("API.Schema.MangaConnectorId", b => + { + b.HasOne("API.Schema.MangaConnectors.MangaConnector", "MangaConnector") + .WithMany() + .HasForeignKey("MangaConnectorName") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Schema.Chapter", "Obj") + .WithMany("MangaConnectorIds") + .HasForeignKey("ObjId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); b.Navigation("MangaConnector"); + + b.Navigation("Obj"); + }); + + modelBuilder.Entity("API.Schema.MangaConnectorId", b => + { + b.HasOne("API.Schema.MangaConnectors.MangaConnector", "MangaConnector") + .WithMany() + .HasForeignKey("MangaConnectorName") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Schema.Manga", "Obj") + .WithMany("MangaConnectorIds") + .HasForeignKey("ObjId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MangaConnector"); + + b.Navigation("Obj"); }); modelBuilder.Entity("AuthorToManga", b => @@ -600,13 +664,13 @@ namespace API.Migrations.pgsql { b.HasOne("API.Schema.Jobs.Job", null) .WithMany() - .HasForeignKey("DependsOnJobsJobId") + .HasForeignKey("DependsOnJobsKey") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); b.HasOne("API.Schema.Jobs.Job", null) .WithMany() - .HasForeignKey("JobId") + .HasForeignKey("JobKey") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); }); @@ -667,7 +731,7 @@ namespace API.Migrations.pgsql .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.HasOne("API.Schema.LocalLibrary", "ToLibrary") + b.HasOne("API.Schema.FileLibrary", "ToFileLibrary") .WithMany() .HasForeignKey("ToLibraryId") .OnDelete(DeleteBehavior.Cascade) @@ -675,7 +739,7 @@ namespace API.Migrations.pgsql b.Navigation("Manga"); - b.Navigation("ToLibrary"); + b.Navigation("ToFileLibrary"); }); modelBuilder.Entity("API.Schema.Jobs.RetrieveChaptersJob", b => @@ -711,9 +775,16 @@ namespace API.Migrations.pgsql b.Navigation("Manga"); }); + modelBuilder.Entity("API.Schema.Chapter", b => + { + b.Navigation("MangaConnectorIds"); + }); + modelBuilder.Entity("API.Schema.Manga", b => { b.Navigation("Chapters"); + + b.Navigation("MangaConnectorIds"); }); #pragma warning restore 612, 618 } diff --git a/API/Program.cs b/API/Program.cs index c1d3591..75b5038 100644 --- a/API/Program.cs +++ b/API/Program.cs @@ -1,5 +1,6 @@ using System.Reflection; using API; +using API.Controllers; using API.Schema; using API.Schema.Contexts; using API.Schema.Jobs; @@ -108,7 +109,26 @@ app.UseMiddleware(); using (IServiceScope scope = app.Services.CreateScope()) { PgsqlContext context = scope.ServiceProvider.GetRequiredService(); - context.Database.Migrate(); + if (context.Database.GetMigrations().Contains("20250630182650_OofV2.1") == false) + { + IQueryable<(string, string)> mangas = context.Database.SqlQuery<(string, string)>($"SELECT MangaConnectorName, IdOnConnectorSite as ID FROM Mangas"); + context.Database.Migrate(); + foreach ((string mangaConnectorName, string idOnConnectorSite) manga in mangas) + { + if(context.MangaConnectors.Find(manga.mangaConnectorName) is not { } mangaConnector) + continue; + if(mangaConnector.GetMangaFromId(manga.idOnConnectorSite) is not { } result) + continue; + if (SearchController.AddMangaToContext(result.Item1, result.Item2, context) is { } added) + { + RetrieveChaptersJob retrieveChaptersJob = new (added, "en", 0); + UpdateChaptersDownloadedJob update = new(added, 0, null, [retrieveChaptersJob]); + context.Jobs.AddRange([retrieveChaptersJob, update]); + } + } + } else + context.Database.Migrate(); + MangaConnector[] connectors = [ @@ -119,7 +139,7 @@ using (IServiceScope scope = app.Services.CreateScope()) MangaConnector[] newConnectors = connectors.Where(c => !context.MangaConnectors.Contains(c)).ToArray(); context.MangaConnectors.AddRange(newConnectors); if (!context.LocalLibraries.Any()) - context.LocalLibraries.Add(new LocalLibrary(TrangaSettings.downloadLocation, "Default Library")); + context.LocalLibraries.Add(new FileLibrary(TrangaSettings.downloadLocation, "Default FileLibrary")); context.Jobs.AddRange(context.Jobs.Where(j => j.JobType == JobType.DownloadAvailableChaptersJob) .Include(downloadAvailableChaptersJob => ((DownloadAvailableChaptersJob)downloadAvailableChaptersJob).Manga) diff --git a/API/Schema/AltTitle.cs b/API/Schema/AltTitle.cs new file mode 100644 index 0000000..2cc0b0d --- /dev/null +++ b/API/Schema/AltTitle.cs @@ -0,0 +1,17 @@ +using System.ComponentModel.DataAnnotations; +using Microsoft.EntityFrameworkCore; + +namespace API.Schema; + +[PrimaryKey("Key")] +public class AltTitle(string language, string title) : Identifiable(TokenGen.CreateToken("AltTitle")) +{ + [StringLength(8)] + [Required] + public string Language { get; init; } = language; + [StringLength(256)] + [Required] + public string Title { get; init; } = title; + + public override string ToString() => $"{base.ToString()} {Language} {Title}"; +} \ No newline at end of file diff --git a/API/Schema/Author.cs b/API/Schema/Author.cs index b3149af..4517b43 100644 --- a/API/Schema/Author.cs +++ b/API/Schema/Author.cs @@ -3,18 +3,12 @@ using Microsoft.EntityFrameworkCore; namespace API.Schema; -[PrimaryKey("AuthorId")] -public class Author(string authorName) +[PrimaryKey("Key")] +public class Author(string authorName) : Identifiable(TokenGen.CreateToken(typeof(Author), authorName)) { - [StringLength(64)] - [Required] - public string AuthorId { get; init; } = TokenGen.CreateToken(typeof(Author), authorName); [StringLength(128)] [Required] public string AuthorName { get; init; } = authorName; - public override string ToString() - { - return $"{AuthorId} {AuthorName}"; - } + public override string ToString() => $"{base.ToString()} {AuthorName}"; } \ No newline at end of file diff --git a/API/Schema/Chapter.cs b/API/Schema/Chapter.cs index 71ce9fe..47cfd66 100644 --- a/API/Schema/Chapter.cs +++ b/API/Schema/Chapter.cs @@ -9,15 +9,11 @@ using Newtonsoft.Json; namespace API.Schema; -[PrimaryKey("ChapterId")] -public class Chapter : IComparable +[PrimaryKey("Key")] +public class Chapter : Identifiable, IComparable { - [StringLength(64)] [Required] public string ChapterId { get; init; } - - [StringLength(256)]public string? IdOnConnectorSite { get; init; } - [StringLength(64)] [Required] public string ParentMangaId { get; init; } = null!; - private Manga? _parentManga = null!; + private Manga? _parentManga; [JsonIgnore] public Manga ParentManga @@ -25,41 +21,43 @@ public class Chapter : IComparable get => _lazyLoader.Load(this, ref _parentManga) ?? throw new InvalidOperationException(); init { - ParentMangaId = value.MangaId; + ParentMangaId = value.Key; _parentManga = value; } } - private MangaConnectorMangaEntry? _mangaConnectorMangaEntry = null!; + [NotMapped] + public Dictionary IdsOnMangaConnectors => + MangaConnectorIds.ToDictionary(id => id.MangaConnectorName, id => id.IdOnConnectorSite); + + private ICollection>? _mangaConnectorIds; [JsonIgnore] - public MangaConnectorMangaEntry MangaConnectorMangaEntry + public ICollection> MangaConnectorIds { - get => _lazyLoader.Load(this, ref _mangaConnectorMangaEntry) ?? throw new InvalidOperationException(); - init => _mangaConnectorMangaEntry = value; + get => _lazyLoader.Load(this, ref _mangaConnectorIds) ?? throw new InvalidOperationException(); + init => _mangaConnectorIds = value; } public int? VolumeNumber { 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; } [Required] public bool Downloaded { get; internal set; } - [NotMapped] public string FullArchiveFilePath => Path.Join(MangaConnectorMangaEntry.Manga.FullDirectoryPath, FileName); + [NotMapped] public string FullArchiveFilePath => Path.Join(ParentManga.FullDirectoryPath, FileName); private readonly ILazyLoader _lazyLoader = null!; - public Chapter(MangaConnectorMangaEntry mangaConnectorMangaEntry, string url, string chapterNumber, int? volumeNumber = null, string? idOnConnectorSite = null, string? title = null) + public Chapter(Manga parentManga, string chapterNumber, + int? volumeNumber, string? title = null) + : base(TokenGen.CreateToken(typeof(Chapter), parentManga.Key, chapterNumber)) { - this.ChapterId = TokenGen.CreateToken(typeof(Chapter), mangaConnectorMangaEntry.MangaId, chapterNumber); - this.MangaConnectorMangaEntry = mangaConnectorMangaEntry; - this.IdOnConnectorSite = idOnConnectorSite; + this.ParentManga = parentManga; + this.MangaConnectorIds = []; this.VolumeNumber = volumeNumber; this.ChapterNumber = chapterNumber; - this.Url = url; this.Title = title; this.FileName = GetArchiveFilePath(); this.Downloaded = false; @@ -68,14 +66,12 @@ public class Chapter : IComparable /// /// EF ONLY!!! /// - internal Chapter(ILazyLoader lazyLoader, string chapterId, int? volumeNumber, string chapterNumber, string url, string? idOnConnectorSite, string? title, string fileName, bool downloaded) + internal Chapter(ILazyLoader lazyLoader, string key, int? volumeNumber, string chapterNumber, string? title, string fileName, bool downloaded) + : base(key) { this._lazyLoader = lazyLoader; - this.ChapterId = chapterId; - this.IdOnConnectorSite = idOnConnectorSite; this.VolumeNumber = volumeNumber; this.ChapterNumber = chapterNumber; - this.Url = url; this.Title = title; this.FileName = fileName; this.Downloaded = downloaded; @@ -100,14 +96,14 @@ public class Chapter : IComparable public bool CheckDownloaded() => File.Exists(FullArchiveFilePath); /// Placeholders: - /// %M Manga Name + /// %M Obj Name /// %V Volume /// %C Chapter /// %T Title /// %A Author (first in list) /// %I Chapter Internal ID - /// %i Manga Internal ID - /// %Y Year (Manga) + /// %i Obj Internal ID + /// %Y Year (Obj) private static readonly Regex NullableRex = new(@"\?([a-zA-Z])\(([^\)]*)\)|(.+?)"); private static readonly Regex ReplaceRexx = new(@"%([a-zA-Z])|(.+?)"); private string GetArchiveFilePath() @@ -125,14 +121,12 @@ public class Chapter : IComparable char placeholder = nullable.Groups[1].Value[0]; bool isNull = placeholder switch { - 'M' => MangaConnectorMangaEntry.Manga?.Name is null, + 'M' => ParentManga?.Name is null, 'V' => VolumeNumber is null, 'C' => ChapterNumber is null, 'T' => Title is null, - 'A' => MangaConnectorMangaEntry.Manga?.Authors?.FirstOrDefault()?.AuthorName is null, - 'I' => ChapterId is null, - 'i' => MangaConnectorMangaEntry.Manga?.MangaId is null, - 'Y' => MangaConnectorMangaEntry.Manga?.Year is null, + 'A' => ParentManga?.Authors?.FirstOrDefault()?.AuthorName is null, + 'Y' => ParentManga?.Year is null, _ => true }; if(!isNull) @@ -153,14 +147,12 @@ public class Chapter : IComparable char placeholder = replace.Groups[1].Value[0]; string? value = placeholder switch { - 'M' => MangaConnectorMangaEntry.Manga?.Name, + 'M' => ParentManga?.Name, 'V' => VolumeNumber?.ToString(), 'C' => ChapterNumber, 'T' => Title, - 'A' => MangaConnectorMangaEntry.Manga?.Authors?.FirstOrDefault()?.AuthorName, - 'I' => ChapterId, - 'i' => MangaConnectorMangaEntry.Manga?.MangaId, - 'Y' => MangaConnectorMangaEntry.Manga?.Year.ToString(), + 'A' => ParentManga?.Authors?.FirstOrDefault()?.AuthorName, + 'Y' => ParentManga?.Year.ToString(), _ => null }; stringBuilder.Append(value); @@ -201,21 +193,18 @@ public class Chapter : IComparable ); if(Title is not null) comicInfo.Add(new XElement("Title", Title)); - if(MangaConnectorMangaEntry.Manga.MangaTags.Count > 0) - comicInfo.Add(new XElement("Tags", string.Join(',', MangaConnectorMangaEntry.Manga.MangaTags.Select(tag => tag.Tag)))); + if(ParentManga.MangaTags.Count > 0) + comicInfo.Add(new XElement("Tags", string.Join(',', ParentManga.MangaTags.Select(tag => tag.Tag)))); if(VolumeNumber is not null) comicInfo.Add(new XElement("Volume", VolumeNumber)); - if(MangaConnectorMangaEntry.Manga.Authors.Count > 0) - comicInfo.Add(new XElement("Writer", string.Join(',', MangaConnectorMangaEntry.Manga.Authors.Select(author => author.AuthorName)))); - if(MangaConnectorMangaEntry.Manga.OriginalLanguage is not null) - comicInfo.Add(new XElement("LanguageISO", MangaConnectorMangaEntry.Manga.OriginalLanguage)); - if(MangaConnectorMangaEntry.Manga.Description != string.Empty) - comicInfo.Add(new XElement("Summary", MangaConnectorMangaEntry.Manga.Description)); + if(ParentManga.Authors.Count > 0) + comicInfo.Add(new XElement("Writer", string.Join(',', ParentManga.Authors.Select(author => author.AuthorName)))); + if(ParentManga.OriginalLanguage is not null) + comicInfo.Add(new XElement("LanguageISO", ParentManga.OriginalLanguage)); + if(ParentManga.Description != string.Empty) + comicInfo.Add(new XElement("Summary", ParentManga.Description)); return comicInfo.ToString(); } - public override string ToString() - { - return $"{ChapterId} Vol.{VolumeNumber} Ch.{ChapterNumber} - {Title}"; - } + public override string ToString() => $"{base.ToString()} Vol.{VolumeNumber} Ch.{ChapterNumber} - {Title}"; } \ No newline at end of file diff --git a/API/Schema/Contexts/PgsqlContext.cs b/API/Schema/Contexts/PgsqlContext.cs index 6b73821..3c2c927 100644 --- a/API/Schema/Contexts/PgsqlContext.cs +++ b/API/Schema/Contexts/PgsqlContext.cs @@ -11,10 +11,12 @@ public class PgsqlContext(DbContextOptions options) : DbContext(op public DbSet Jobs { get; set; } public DbSet MangaConnectors { get; set; } public DbSet Mangas { get; set; } - public DbSet LocalLibraries { get; set; } + public DbSet LocalLibraries { get; set; } public DbSet Chapters { get; set; } public DbSet Authors { get; set; } public DbSet Tags { get; set; } + public DbSet> MangaConnectorToManga { get; set; } + public DbSet> MangaConnectorToChapter { get; set; } private ILog Log => LogManager.GetLogger(GetType()); protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) @@ -40,40 +42,30 @@ public class PgsqlContext(DbContextOptions options) : DbContext(op .HasValue(JobType.RetrieveChaptersJob) .HasValue(JobType.UpdateCoverJob) .HasValue(JobType.UpdateChaptersDownloadedJob); - modelBuilder.Entity() - .HasDiscriminator(j => j.GetType().IsSubclassOf(typeof(JobWithDownloading))) - .HasValue(true); - - //Job specification - modelBuilder.Entity() - .HasOne(j => j.MangaConnector) - .WithMany() - .HasForeignKey(j => j.MangaConnectorName) - .OnDelete(DeleteBehavior.Cascade); - modelBuilder.Entity() - .Navigation(j => j.MangaConnector) - .EnableLazyLoading(); modelBuilder.Entity() - .HasOne(j => j.MangaConnectorMangaEntry) + .HasOne(j => j.Manga) .WithMany() + .HasForeignKey(j => j.MangaId) .OnDelete(DeleteBehavior.Cascade); modelBuilder.Entity() - .Navigation(j => j.MangaConnectorMangaEntry) + .Navigation(j => j.Manga) .EnableLazyLoading(); modelBuilder.Entity() - .HasOne(j => j.MangaConnectorMangaEntry) + .HasOne(j => j.Manga) .WithMany() + .HasForeignKey(j => j.MangaId) .OnDelete(DeleteBehavior.Cascade); modelBuilder.Entity() - .Navigation(j => j.MangaConnectorMangaEntry) + .Navigation(j => j.Manga) .EnableLazyLoading(); modelBuilder.Entity() - .HasOne(j => j.MangaConnectorMangaEntry) + .HasOne(j => j.Chapter) .WithMany() + .HasForeignKey(j => j.ChapterId) .OnDelete(DeleteBehavior.Cascade); modelBuilder.Entity() - .Navigation(j => j.MangaConnectorMangaEntry) + .Navigation(j => j.Chapter) .EnableLazyLoading(); modelBuilder.Entity() .HasOne(j => j.Manga) @@ -84,19 +76,20 @@ public class PgsqlContext(DbContextOptions options) : DbContext(op .Navigation(j => j.Manga) .EnableLazyLoading(); modelBuilder.Entity() - .HasOne(j => j.ToLibrary) + .HasOne(j => j.ToFileLibrary) .WithMany() .HasForeignKey(j => j.ToLibraryId) .OnDelete(DeleteBehavior.Cascade); modelBuilder.Entity() - .Navigation(j => j.ToLibrary) + .Navigation(j => j.ToFileLibrary) .EnableLazyLoading(); modelBuilder.Entity() - .HasOne(j => j.MangaConnectorMangaEntry) + .HasOne(j => j.Manga) .WithMany() + .HasForeignKey(j => j.MangaId) .OnDelete(DeleteBehavior.Cascade); modelBuilder.Entity() - .Navigation(j => j.MangaConnectorMangaEntry) + .Navigation(j => j.Manga) .EnableLazyLoading(); modelBuilder.Entity() .HasOne(j => j.Manga) @@ -111,15 +104,17 @@ public class PgsqlContext(DbContextOptions options) : DbContext(op modelBuilder.Entity() .HasOne(childJob => childJob.ParentJob) .WithMany() - .HasForeignKey(childjob => childjob.ParentJobId) + .HasForeignKey(childJob => childJob.ParentJobId) .OnDelete(DeleteBehavior.Cascade); + modelBuilder.Entity() + .Navigation(childJob => childJob.ParentJob) + .EnableLazyLoading(); //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 @@ -137,11 +132,27 @@ public class PgsqlContext(DbContextOptions options) : DbContext(op .OnDelete(DeleteBehavior.Cascade); modelBuilder.Entity() .Navigation(m => m.Chapters) - .AutoInclude(false) + .EnableLazyLoading(); + modelBuilder.Entity() + .Navigation(c => c.ParentManga) + .EnableLazyLoading(); + //Chapter has MangaConnectorIds + modelBuilder.Entity() + .HasMany>(c => c.MangaConnectorIds) + .WithOne(id => id.Obj) + .HasForeignKey(id => id.ObjId) + .OnDelete(DeleteBehavior.NoAction); + modelBuilder.Entity>() + .HasOne(id => id.MangaConnector) + .WithMany() + .HasForeignKey(id => id.MangaConnectorName) + .OnDelete(DeleteBehavior.Cascade); + modelBuilder.Entity>() + .Navigation(entry => entry.MangaConnector) .EnableLazyLoading(); //Manga owns MangaAltTitles modelBuilder.Entity() - .OwnsMany(m => m.AltTitles) + .OwnsMany(m => m.AltTitles) .WithOwner(); modelBuilder.Entity() .Navigation(m => m.AltTitles) @@ -153,50 +164,57 @@ public class PgsqlContext(DbContextOptions options) : DbContext(op modelBuilder.Entity() .Navigation(m => m.Links) .AutoInclude(); - //Manga has many Tags associated with many Manga + //Manga has many Tags associated with many Obj 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)), + r => r.HasOne(typeof(Manga)).WithMany().HasForeignKey("MangaIds").HasPrincipalKey(nameof(Manga.Key)), j => j.HasKey("MangaTagIds", "MangaIds") ); modelBuilder.Entity() .Navigation(m => m.MangaTags) .AutoInclude(); - //Manga has many Authors associated with many Manga + //Manga has many Authors associated with many Obj 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)), + l=> l.HasOne(typeof(Author)).WithMany().HasForeignKey("AuthorIds").HasPrincipalKey(nameof(Author.Key)), + r => r.HasOne(typeof(Manga)).WithMany().HasForeignKey("MangaIds").HasPrincipalKey(nameof(Manga.Key)), j => j.HasKey("AuthorIds", "MangaIds") ); modelBuilder.Entity() .Navigation(m => m.Authors) .AutoInclude(); - //Manga has many MangaConnectorMangaEntries with one MangaConnector + //Manga has many MangaIds modelBuilder.Entity() - .HasMany(m => m.MangaConnectorLinkedToManga) - .WithOne(e => e.Manga) - .HasForeignKey(e => e.MangaId) + .HasMany>(m => m.MangaConnectorIds) + .WithOne(id => id.Obj) + .HasForeignKey(id => id.ObjId) .OnDelete(DeleteBehavior.Cascade); - modelBuilder.Entity() - .HasOne(e => e.MangaConnector) + modelBuilder.Entity() + .Navigation(m => m.MangaConnectorIds) + .EnableLazyLoading(); + modelBuilder.Entity>() + .HasOne(id => id.MangaConnector) .WithMany() - .HasForeignKey(e => e.MangaConnectorName) + .HasForeignKey(id => id.MangaConnectorName) .OnDelete(DeleteBehavior.Cascade); + modelBuilder.Entity>() + .Navigation(entry => entry.MangaConnector) + .EnableLazyLoading(); - //LocalLibrary has many Mangas - modelBuilder.Entity() + + //FileLibrary has many Mangas + modelBuilder.Entity() .HasMany() .WithOne(m => m.Library) .HasForeignKey(m => m.LibraryId) .OnDelete(DeleteBehavior.SetNull); modelBuilder.Entity() .Navigation(m => m.Library) - .AutoInclude(); + .EnableLazyLoading(); } } \ No newline at end of file diff --git a/API/Schema/FileLibrary.cs b/API/Schema/FileLibrary.cs new file mode 100644 index 0000000..c01ab48 --- /dev/null +++ b/API/Schema/FileLibrary.cs @@ -0,0 +1,19 @@ +using System.ComponentModel.DataAnnotations; +using Microsoft.EntityFrameworkCore; + +namespace API.Schema; + +[PrimaryKey("Key")] +public class FileLibrary(string basePath, string libraryName) + : Identifiable(TokenGen.CreateToken(typeof(FileLibrary), basePath)) +{ + [StringLength(256)] + [Required] + public string BasePath { get; internal set; } = basePath; + + [StringLength(512)] + [Required] + public string LibraryName { get; internal set; } = libraryName; + + public override string ToString() => $"{base.ToString()} {LibraryName} - {BasePath}"; +} \ No newline at end of file diff --git a/API/Schema/Identifiable.cs b/API/Schema/Identifiable.cs new file mode 100644 index 0000000..2638cd6 --- /dev/null +++ b/API/Schema/Identifiable.cs @@ -0,0 +1,11 @@ +using Microsoft.EntityFrameworkCore; + +namespace API.Schema; + +[PrimaryKey("Key")] +public abstract class Identifiable(string key) +{ + public string Key { get; init; } = key; + + public override string ToString() => Key; +} \ No newline at end of file diff --git a/API/Schema/Jobs/DownloadAvailableChaptersJob.cs b/API/Schema/Jobs/DownloadAvailableChaptersJob.cs index 023a467..fd768e5 100644 --- a/API/Schema/Jobs/DownloadAvailableChaptersJob.cs +++ b/API/Schema/Jobs/DownloadAvailableChaptersJob.cs @@ -1,4 +1,5 @@ -using API.Schema.Contexts; +using System.ComponentModel.DataAnnotations; +using API.Schema.Contexts; using Microsoft.EntityFrameworkCore.Infrastructure; using Newtonsoft.Json; @@ -6,31 +7,44 @@ namespace API.Schema.Jobs; public class DownloadAvailableChaptersJob : JobWithDownloading { - private MangaConnectorMangaEntry? _mangaConnectorMangaEntry = null!; + [StringLength(64)] [Required] public string MangaId { get; init; } = null!; + private Manga? _manga; + [JsonIgnore] - public MangaConnectorMangaEntry MangaConnectorMangaEntry + public Manga Manga { - get => LazyLoader.Load(this, ref _mangaConnectorMangaEntry) ?? throw new InvalidOperationException(); - init => _mangaConnectorMangaEntry = value; + get => LazyLoader.Load(this, ref _manga) ?? throw new InvalidOperationException(); + init + { + MangaId = value.Key; + _manga = value; + } } - public DownloadAvailableChaptersJob(MangaConnectorMangaEntry mangaConnectorMangaEntry, ulong recurrenceMs, Job? parentJob = null, ICollection? dependsOnJobs = null) - : base(TokenGen.CreateToken(typeof(DownloadAvailableChaptersJob)), JobType.DownloadAvailableChaptersJob, recurrenceMs, mangaConnectorMangaEntry.MangaConnector, parentJob, dependsOnJobs) + public DownloadAvailableChaptersJob(Manga manga, ulong recurrenceMs, Job? parentJob = null, ICollection? dependsOnJobs = null) + : base(TokenGen.CreateToken(typeof(DownloadAvailableChaptersJob)), JobType.DownloadAvailableChaptersJob, recurrenceMs, parentJob, dependsOnJobs) { - this.MangaConnectorMangaEntry = mangaConnectorMangaEntry; + this.Manga = manga; } /// /// EF ONLY!!! /// - internal DownloadAvailableChaptersJob(ILazyLoader lazyLoader, string jobId, ulong recurrenceMs, string mangaConnectorName, string? parentJobId) - : base(lazyLoader, jobId, JobType.DownloadAvailableChaptersJob, recurrenceMs, mangaConnectorName, parentJobId) + internal DownloadAvailableChaptersJob(ILazyLoader lazyLoader, string key, string mangaId, ulong recurrenceMs, string? parentJobId) + : base(lazyLoader, key, JobType.DownloadAvailableChaptersJob, recurrenceMs, parentJobId) { - + this.MangaId = mangaId; } protected override IEnumerable RunInternal(PgsqlContext context) { - return MangaConnectorMangaEntry.Manga.Chapters.Where(c => c.Downloaded == false).Select(chapter => new DownloadSingleChapterJob(chapter, this.MangaConnectorMangaEntry)); + // Chapters that aren't downloaded and for which no downloading-Job exists + IEnumerable newChapters = Manga.Chapters + .Where(c => + c.Downloaded == false && + context.Jobs.Any(j => + j.JobType == JobType.DownloadSingleChapterJob && + ((DownloadSingleChapterJob)j).Chapter.ParentMangaId == MangaId) == false); + return newChapters.Select(c => new DownloadSingleChapterJob(c, this)); } } \ No newline at end of file diff --git a/API/Schema/Jobs/DownloadMangaCoverJob.cs b/API/Schema/Jobs/DownloadMangaCoverJob.cs index 501b2b0..fca54ee 100644 --- a/API/Schema/Jobs/DownloadMangaCoverJob.cs +++ b/API/Schema/Jobs/DownloadMangaCoverJob.cs @@ -8,34 +8,42 @@ namespace API.Schema.Jobs; public class DownloadMangaCoverJob : JobWithDownloading { - private MangaConnectorMangaEntry? _mangaConnectorMangaEntry = null!; - [JsonIgnore] - public MangaConnectorMangaEntry MangaConnectorMangaEntry - { - get => LazyLoader.Load(this, ref _mangaConnectorMangaEntry) ?? throw new InvalidOperationException(); - init => _mangaConnectorMangaEntry = value; - } + [StringLength(64)] [Required] public string MangaId { get; init; } = null!; + private Manga? _manga; - public DownloadMangaCoverJob(MangaConnectorMangaEntry mangaConnectorEntry, Job? parentJob = null, ICollection? dependsOnJobs = null) - : base(TokenGen.CreateToken(typeof(DownloadMangaCoverJob)), JobType.DownloadMangaCoverJob, 0, mangaConnectorEntry.MangaConnector, parentJob, dependsOnJobs) + [JsonIgnore] + public Manga Manga { - this.MangaConnectorMangaEntry = mangaConnectorEntry; + get => LazyLoader.Load(this, ref _manga) ?? throw new InvalidOperationException(); + init + { + MangaId = value.Key; + _manga = value; + } + } + + public DownloadMangaCoverJob(Manga manga, Job? parentJob = null, ICollection? dependsOnJobs = null) + : base(TokenGen.CreateToken(typeof(DownloadMangaCoverJob)), JobType.DownloadMangaCoverJob, 0, parentJob, dependsOnJobs) + { + this.Manga = manga; } /// /// EF ONLY!!! /// - internal DownloadMangaCoverJob(ILazyLoader lazyLoader, string jobId, ulong recurrenceMs, string mangaConnectorName, string? parentJobId) - : base(lazyLoader, jobId, JobType.DownloadMangaCoverJob, recurrenceMs, mangaConnectorName, parentJobId) + internal DownloadMangaCoverJob(ILazyLoader lazyLoader, string key, string mangaId, ulong recurrenceMs, string? parentJobId) + : base(lazyLoader, key, JobType.DownloadMangaCoverJob, recurrenceMs, parentJobId) { - + this.MangaId = mangaId; } protected override IEnumerable RunInternal(PgsqlContext context) { + //TODO MangaConnector Selection + MangaConnectorId mcId = Manga.MangaConnectorIds.First(); try { - MangaConnectorMangaEntry.Manga.CoverFileNameInCache = MangaConnectorMangaEntry.MangaConnector.SaveCoverImageToCache(MangaConnectorMangaEntry.Manga); + Manga.CoverFileNameInCache = mcId.MangaConnector.SaveCoverImageToCache(mcId); context.SaveChanges(); } catch (DbUpdateException e) diff --git a/API/Schema/Jobs/DownloadSingleChapterJob.cs b/API/Schema/Jobs/DownloadSingleChapterJob.cs index 0153bd2..582d36e 100644 --- a/API/Schema/Jobs/DownloadSingleChapterJob.cs +++ b/API/Schema/Jobs/DownloadSingleChapterJob.cs @@ -16,39 +16,30 @@ namespace API.Schema.Jobs; public class DownloadSingleChapterJob : JobWithDownloading { [StringLength(64)] [Required] public string ChapterId { get; init; } = null!; - private Chapter? _chapter = null!; - + private Chapter? _chapter; + [JsonIgnore] public Chapter Chapter { get => LazyLoader.Load(this, ref _chapter) ?? throw new InvalidOperationException(); init { - ChapterId = value.ChapterId; + ChapterId = value.Key; _chapter = value; } } - private MangaConnectorMangaEntry? _mangaConnectorMangaEntry = null!; - [JsonIgnore] - public MangaConnectorMangaEntry MangaConnectorMangaEntry - { - get => LazyLoader.Load(this, ref _mangaConnectorMangaEntry) ?? throw new InvalidOperationException(); - init => _mangaConnectorMangaEntry = value; - } - - public DownloadSingleChapterJob(Chapter chapter, MangaConnectorMangaEntry mangaConnectorMangaEntry, Job? parentJob = null, ICollection? dependsOnJobs = null) - : base(TokenGen.CreateToken(typeof(DownloadSingleChapterJob)), JobType.DownloadSingleChapterJob, 0, mangaConnectorMangaEntry.MangaConnector, parentJob, dependsOnJobs) + public DownloadSingleChapterJob(Chapter chapter, Job? parentJob = null, ICollection? dependsOnJobs = null) + : base(TokenGen.CreateToken(typeof(DownloadSingleChapterJob)), JobType.DownloadSingleChapterJob, 0, parentJob, dependsOnJobs) { this.Chapter = chapter; - this.MangaConnectorMangaEntry = mangaConnectorMangaEntry; } /// /// EF ONLY!!! /// - internal DownloadSingleChapterJob(ILazyLoader lazyLoader, string jobId, ulong recurrenceMs, string mangaConnectorName, string chapterId, string? parentJobId) - : base(lazyLoader, jobId, JobType.DownloadSingleChapterJob, recurrenceMs, mangaConnectorName, parentJobId) + internal DownloadSingleChapterJob(ILazyLoader lazyLoader, string key, string chapterId, ulong recurrenceMs, string? parentJobId) + : base(lazyLoader, key, JobType.DownloadSingleChapterJob, recurrenceMs, parentJobId) { this.ChapterId = chapterId; } @@ -60,13 +51,16 @@ public class DownloadSingleChapterJob : JobWithDownloading Log.Info("Chapter was already downloaded."); return []; } - string[] imageUrls = MangaConnectorMangaEntry.MangaConnector.GetChapterImageUrls(Chapter); + + //TODO MangaConnector Selection + MangaConnectorId mcId = Chapter.MangaConnectorIds.First(); + + string[] imageUrls = mcId.MangaConnector.GetChapterImageUrls(mcId); if (imageUrls.Length < 1) { - Log.Info($"No imageUrls for chapter {ChapterId}"); + Log.Info($"No imageUrls for chapter {Chapter}"); return []; } - context.Entry(Chapter.MangaConnectorMangaEntry.Manga).Reference(m => m.Library).Load(); //Need to explicitly load, because we are not accessing navigation directly... string saveArchiveFilePath = Chapter.FullArchiveFilePath; Log.Debug($"Chapter path: {saveArchiveFilePath}"); @@ -98,7 +92,7 @@ public class DownloadSingleChapterJob : JobWithDownloading string tempFolder = Directory.CreateTempSubdirectory("trangatemp").FullName; Log.Debug($"Created temp folder: {tempFolder}"); - Log.Info($"Downloading images: {ChapterId}"); + Log.Info($"Downloading images: {Chapter}"); int chapterNum = 0; //Download all Images to temporary Folder foreach (string imageUrl in imageUrls) @@ -113,12 +107,12 @@ public class DownloadSingleChapterJob : JobWithDownloading } } - CopyCoverFromCacheToDownloadLocation(Chapter.MangaConnectorMangaEntry.Manga); + CopyCoverFromCacheToDownloadLocation(Chapter.ParentManga); - Log.Debug($"Creating ComicInfo.xml {ChapterId}"); + Log.Debug($"Creating ComicInfo.xml {Chapter}"); File.WriteAllText(Path.Join(tempFolder, "ComicInfo.xml"), Chapter.GetComicInfoXmlString()); - Log.Debug($"Packaging images to archive {ChapterId}"); + Log.Debug($"Packaging images to archive {Chapter}"); //ZIP-it and ship-it ZipFile.CreateFromDirectory(tempFolder, saveArchiveFilePath); if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) @@ -133,11 +127,11 @@ public class DownloadSingleChapterJob : JobWithDownloading if (j.JobType != JobType.UpdateChaptersDownloadedJob) return false; UpdateChaptersDownloadedJob job = (UpdateChaptersDownloadedJob)j; - return job.MangaId == this.Chapter.MangaConnectorMangaEntry.MangaId; + return job.MangaId == Chapter.ParentMangaId; })) return []; - return [new UpdateChaptersDownloadedJob(Chapter.MangaConnectorMangaEntry.Manga, 0, this.ParentJob)]; + return [new UpdateChaptersDownloadedJob(Chapter.ParentManga, 0, this.ParentJob)]; } private void ProcessImage(string imagePath) @@ -188,9 +182,12 @@ public class DownloadSingleChapterJob : JobWithDownloading Log.Debug($"Cover already exists at {publicationFolder}"); return; } + + //TODO MangaConnector Selection + MangaConnectorId mcId = manga.MangaConnectorIds.First(); Log.Info($"Copying cover to {publicationFolder}"); - string? fileInCache = manga.CoverFileNameInCache ?? MangaConnectorMangaEntry.MangaConnector.SaveCoverImageToCache(manga); + string? fileInCache = manga.CoverFileNameInCache ?? mcId.MangaConnector.SaveCoverImageToCache(mcId); 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 89b1942..cacfcfc 100644 --- a/API/Schema/Jobs/Job.cs +++ b/API/Schema/Jobs/Job.cs @@ -8,19 +8,15 @@ using Newtonsoft.Json; namespace API.Schema.Jobs; -[PrimaryKey("JobId")] -public abstract class Job : IComparable +[PrimaryKey("Key")] +public abstract class Job : Identifiable, IComparable { - [StringLength(64)] - [Required] - public string JobId { get; init; } - [StringLength(64)] public string? ParentJobId { get; private set; } [JsonIgnore] public Job? ParentJob { get; internal set; } - private ICollection _dependsOnJobs = null!; + private ICollection? _dependsOnJobs; [JsonIgnore] public ICollection DependsOnJobs { - get => LazyLoader.Load(this, ref _dependsOnJobs); + get => LazyLoader.Load(this, ref _dependsOnJobs) ?? throw new InvalidOperationException(); init => _dependsOnJobs = value; } @@ -37,14 +33,14 @@ public abstract class Job : IComparable [JsonIgnore] [NotMapped] internal bool IsCompleted => state is >= (JobState)128 and < (JobState)192; [NotMapped] [JsonIgnore] protected ILog Log { get; init; } - [NotMapped] [JsonIgnore] protected ILazyLoader LazyLoader { get; init; } + [NotMapped] [JsonIgnore] protected ILazyLoader LazyLoader { get; init; } = null!; - protected Job(string jobId, JobType jobType, ulong recurrenceMs, Job? parentJob = null, ICollection? dependsOnJobs = null) + protected Job(string key, JobType jobType, ulong recurrenceMs, Job? parentJob = null, ICollection? dependsOnJobs = null) + : base(key) { - this.JobId = jobId; this.JobType = jobType; this.RecurrenceMs = recurrenceMs; - this.ParentJobId = parentJob?.JobId; + this.ParentJobId = parentJob?.Key; this.ParentJob = parentJob; this.DependsOnJobs = dependsOnJobs ?? []; @@ -54,10 +50,10 @@ public abstract class Job : IComparable /// /// EF ONLY!!! /// - protected internal Job(ILazyLoader lazyLoader, string jobId, JobType jobType, ulong recurrenceMs, string? parentJobId) + protected internal Job(ILazyLoader lazyLoader, string key, JobType jobType, ulong recurrenceMs, string? parentJobId) + : base(key) { this.LazyLoader = lazyLoader; - this.JobId = jobId; this.JobType = jobType; this.RecurrenceMs = recurrenceMs; this.ParentJobId = parentJobId; @@ -68,7 +64,7 @@ public abstract class Job : IComparable public IEnumerable Run(PgsqlContext context, ref bool running) { - Log.Info($"Running job {JobId}"); + Log.Info($"Running job {this}"); DateTime jobStart = DateTime.UtcNow; Job[]? ret = null; @@ -78,7 +74,7 @@ public abstract class Job : IComparable context.SaveChanges(); running = true; ret = RunInternal(context).ToArray(); - Log.Info($"Job {JobId} completed. Generated {ret.Length} new jobs."); + Log.Info($"Job {this} completed. Generated {ret.Length} new jobs."); this.state = this.RecurrenceMs > 0 ? JobState.CompletedWaiting : JobState.Completed; this.LastExecution = DateTime.UtcNow; context.SaveChanges(); @@ -87,7 +83,7 @@ public abstract class Job : IComparable { if (e is not DbUpdateException) { - Log.Error($"Failed to run job {JobId}", e); + Log.Error($"Failed to run job {this}", e); this.state = JobState.Failed; this.Enabled = false; this.LastExecution = DateTime.UtcNow; @@ -95,7 +91,7 @@ public abstract class Job : IComparable } else { - Log.Error($"Failed to update Database {JobId}", e); + Log.Error($"Failed to update Database {this}", e); } } @@ -109,10 +105,10 @@ public abstract class Job : IComparable } catch (DbUpdateException e) { - Log.Error($"Failed to update Database {JobId}", e); + Log.Error($"Failed to update Database {this}", e); } - Log.Info($"Finished Job {JobId}! (took {DateTime.UtcNow.Subtract(jobStart).TotalMilliseconds}ms)"); + Log.Info($"Finished Job {this}! (took {DateTime.UtcNow.Subtract(jobStart).TotalMilliseconds}ms)"); return ret ?? []; } @@ -146,14 +142,8 @@ public abstract class Job : IComparable // Sort by NextExecution-time if (this.NextExecution < other.NextExecution) return -1; - // Sort by JobPriority - if (JobQueueSorter.GetPriority(this) > JobQueueSorter.GetPriority(other)) - return -1; return 1; } - public override string ToString() - { - return $"{JobId}"; - } + public override string ToString() => base.ToString(); } \ No newline at end of file diff --git a/API/Schema/Jobs/JobWithDownloading.cs b/API/Schema/Jobs/JobWithDownloading.cs index a49f5e2..92cb76d 100644 --- a/API/Schema/Jobs/JobWithDownloading.cs +++ b/API/Schema/Jobs/JobWithDownloading.cs @@ -1,37 +1,18 @@ -using System.ComponentModel.DataAnnotations; -using API.Schema.MangaConnectors; using Microsoft.EntityFrameworkCore.Infrastructure; -using Newtonsoft.Json; namespace API.Schema.Jobs; public abstract class JobWithDownloading : Job { - [StringLength(32)] [Required] public string MangaConnectorName { get; private set; } = null!; - [JsonIgnore] private MangaConnector? _mangaConnector; - [JsonIgnore] - public MangaConnector MangaConnector - { - get => LazyLoader.Load(this, ref _mangaConnector) ?? throw new InvalidOperationException(); - init - { - MangaConnectorName = value.Name; - _mangaConnector = value; - } - } - protected JobWithDownloading(string jobId, JobType jobType, ulong recurrenceMs, MangaConnector mangaConnector, Job? parentJob = null, ICollection? dependsOnJobs = null) - : base(jobId, jobType, recurrenceMs, parentJob, dependsOnJobs) + public JobWithDownloading(string key, JobType jobType, ulong recurrenceMs, Job? parentJob = null, ICollection? dependsOnJobs = null) + : base(key, jobType, recurrenceMs, parentJob, dependsOnJobs) { - this.MangaConnector = mangaConnector; + } - - /// - /// EF CORE ONLY!!! - /// - internal JobWithDownloading(ILazyLoader lazyLoader, string jobId, JobType jobType, ulong recurrenceMs, string mangaConnectorName, string? parentJobId) - : base(lazyLoader, jobId, jobType, recurrenceMs, parentJobId) + public JobWithDownloading(ILazyLoader lazyLoader, string key, JobType jobType, ulong recurrenceMs, string? parentJobId) + : base(lazyLoader, key, jobType, recurrenceMs, parentJobId) { - this.MangaConnectorName = mangaConnectorName; + } } \ No newline at end of file diff --git a/API/Schema/Jobs/MoveFileOrFolderJob.cs b/API/Schema/Jobs/MoveFileOrFolderJob.cs index c7663f4..aa3cf61 100644 --- a/API/Schema/Jobs/MoveFileOrFolderJob.cs +++ b/API/Schema/Jobs/MoveFileOrFolderJob.cs @@ -23,8 +23,8 @@ public class MoveFileOrFolderJob : Job /// /// EF ONLY!!! /// - internal MoveFileOrFolderJob(ILazyLoader lazyLoader, string jobId, ulong recurrenceMs, string fromLocation, string toLocation, string? parentJobId) - : base(lazyLoader, jobId, JobType.MoveFileOrFolderJob, recurrenceMs, parentJobId) + internal MoveFileOrFolderJob(ILazyLoader lazyLoader, string key, ulong recurrenceMs, string fromLocation, string toLocation, string? parentJobId) + : base(lazyLoader, key, JobType.MoveFileOrFolderJob, recurrenceMs, parentJobId) { this.FromLocation = fromLocation; this.ToLocation = toLocation; diff --git a/API/Schema/Jobs/MoveMangaLibraryJob.cs b/API/Schema/Jobs/MoveMangaLibraryJob.cs index f70b285..5225b9f 100644 --- a/API/Schema/Jobs/MoveMangaLibraryJob.cs +++ b/API/Schema/Jobs/MoveMangaLibraryJob.cs @@ -8,43 +8,45 @@ namespace API.Schema.Jobs; public class MoveMangaLibraryJob : Job { - [StringLength(64)] [Required] public string MangaId { get; init; } + [StringLength(64)] [Required] public string MangaId { get; init; } = null!; + private Manga? _manga; - private Manga? _manga = null!; - [JsonIgnore] - public Manga Manga + public Manga Manga { get => LazyLoader.Load(this, ref _manga) ?? throw new InvalidOperationException(); - init => _manga = value; - } - - [StringLength(64)] [Required] public string ToLibraryId { get; private set; } = null!; - private LocalLibrary? _toLibrary = null!; - [JsonIgnore] - public LocalLibrary ToLibrary - { - get => LazyLoader.Load(this, ref _toLibrary) ?? throw new InvalidOperationException(); init { - ToLibraryId = value.LocalLibraryId; - _toLibrary = value; + MangaId = value.Key; + _manga = value; } } - public MoveMangaLibraryJob(Manga manga, LocalLibrary toLibrary, Job? parentJob = null, ICollection? dependsOnJobs = null) + [StringLength(64)] [Required] public string ToLibraryId { get; private set; } = null!; + private FileLibrary? _toFileLibrary; + [JsonIgnore] + public FileLibrary ToFileLibrary + { + get => LazyLoader.Load(this, ref _toFileLibrary) ?? throw new InvalidOperationException(); + init + { + ToLibraryId = value.Key; + _toFileLibrary = value; + } + } + + public MoveMangaLibraryJob(Manga manga, FileLibrary toFileLibrary, Job? parentJob = null, ICollection? dependsOnJobs = null) : base(TokenGen.CreateToken(typeof(MoveMangaLibraryJob)), JobType.MoveMangaLibraryJob, 0, parentJob, dependsOnJobs) { - this.MangaId = manga.MangaId; this.Manga = manga; - this.ToLibrary = toLibrary; + this.ToFileLibrary = toFileLibrary; } /// /// EF ONLY!!! /// - internal MoveMangaLibraryJob(ILazyLoader lazyLoader, string jobId, ulong recurrenceMs, string mangaId, string toLibraryId, string? parentJobId) - : base(lazyLoader, jobId, JobType.MoveMangaLibraryJob, recurrenceMs, parentJobId) + internal MoveMangaLibraryJob(ILazyLoader lazyLoader, string key, ulong recurrenceMs, string mangaId, string toLibraryId, string? parentJobId) + : base(lazyLoader, key, JobType.MoveMangaLibraryJob, recurrenceMs, parentJobId) { this.MangaId = mangaId; this.ToLibraryId = toLibraryId; @@ -52,9 +54,9 @@ public class MoveMangaLibraryJob : Job protected override IEnumerable RunInternal(PgsqlContext context) { - context.Entry(Manga).Reference(m => m.Library).Load(); + context.Entry(Manga).Reference(m => m.Library).Load(); Dictionary oldPath = Manga.Chapters.ToDictionary(c => c, c => c.FullArchiveFilePath); - Manga.Library = ToLibrary; + Manga.Library = ToFileLibrary; try { context.SaveChanges(); diff --git a/API/Schema/Jobs/RetrieveChaptersJob.cs b/API/Schema/Jobs/RetrieveChaptersJob.cs index 6a9fccb..939b5f6 100644 --- a/API/Schema/Jobs/RetrieveChaptersJob.cs +++ b/API/Schema/Jobs/RetrieveChaptersJob.cs @@ -8,43 +8,56 @@ namespace API.Schema.Jobs; public class RetrieveChaptersJob : JobWithDownloading { - private MangaConnectorMangaEntry? _mangaConnectorMangaEntry = null!; + [StringLength(64)] [Required] public string MangaId { get; init; } = null!; + private Manga? _manga; + [JsonIgnore] - public MangaConnectorMangaEntry MangaConnectorMangaEntry + public Manga Manga { - get => LazyLoader.Load(this, ref _mangaConnectorMangaEntry) ?? throw new InvalidOperationException(); - init => _mangaConnectorMangaEntry = value; + get => LazyLoader.Load(this, ref _manga) ?? throw new InvalidOperationException(); + init + { + MangaId = value.Key; + _manga = value; + } } [StringLength(8)] [Required] public string Language { get; private set; } - public RetrieveChaptersJob(MangaConnectorMangaEntry mangaConnectorMangaEntry, string language, ulong recurrenceMs, Job? parentJob = null, ICollection? dependsOnJobs = null) - : base(TokenGen.CreateToken(typeof(RetrieveChaptersJob)), JobType.RetrieveChaptersJob, recurrenceMs, mangaConnectorMangaEntry.MangaConnector, parentJob, dependsOnJobs) + 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.MangaConnectorMangaEntry = mangaConnectorMangaEntry; + this.Manga = manga; this.Language = language; } /// /// EF ONLY!!! /// - internal RetrieveChaptersJob(ILazyLoader lazyLoader, string jobId, ulong recurrenceMs, string mangaConnectorName, string language, string? parentJobId) - : base(lazyLoader, jobId, JobType.RetrieveChaptersJob, recurrenceMs, mangaConnectorName, parentJobId) + internal RetrieveChaptersJob(ILazyLoader lazyLoader, string key, string mangaId, ulong recurrenceMs, string language, string? parentJobId) + : base(lazyLoader, key, JobType.RetrieveChaptersJob, recurrenceMs, parentJobId) { + this.MangaId = mangaId; this.Language = language; } protected override IEnumerable RunInternal(PgsqlContext context) { + //TODO MangaConnector Selection + MangaConnectorId mcId = Manga.MangaConnectorIds.First(); + // This gets all chapters that are not downloaded - Chapter[] allChapters = MangaConnectorMangaEntry.MangaConnector.GetChapters(MangaConnectorMangaEntry, Language).DistinctBy(c => c.ChapterId).ToArray(); - Chapter[] newChapters = allChapters.Where(chapter => MangaConnectorMangaEntry.Manga.Chapters.Select(c => c.ChapterId).Contains(chapter.ChapterId) == false).ToArray(); - Log.Info($"{MangaConnectorMangaEntry.Manga.Chapters.Count} existing + {newChapters.Length} new chapters."); + (Chapter, MangaConnectorId)[] allChapters = mcId.MangaConnector.GetChapters(mcId, Language).DistinctBy(c => c.Item1.Key).ToArray(); + (Chapter, MangaConnectorId)[] newChapters = allChapters.Where(chapter => Manga.Chapters.Any(ch => chapter.Item1.Key == ch.Key && ch.Downloaded) == false).ToArray(); + Log.Info($"{Manga.Chapters.Count} existing + {newChapters.Length} new chapters."); try { - foreach (Chapter newChapter in newChapters) - MangaConnectorMangaEntry.Manga.Chapters.Add(newChapter); + foreach ((Chapter chapter, MangaConnectorId mcId) newChapter in newChapters) + { + Manga.Chapters.Add(newChapter.chapter); + context.MangaConnectorToChapter.Add(newChapter.mcId); + } context.SaveChanges(); } catch (DbUpdateException e) diff --git a/API/Schema/Jobs/UpdateChaptersDownloadedJob.cs b/API/Schema/Jobs/UpdateChaptersDownloadedJob.cs index 5fbfc47..fc026c2 100644 --- a/API/Schema/Jobs/UpdateChaptersDownloadedJob.cs +++ b/API/Schema/Jobs/UpdateChaptersDownloadedJob.cs @@ -8,36 +8,38 @@ namespace API.Schema.Jobs; public class UpdateChaptersDownloadedJob : Job { - [StringLength(64)] [Required] public string MangaId { get; init; } + [StringLength(64)] [Required] public string MangaId { get; init; } = null!; + private Manga? _manga; - private Manga _manga = null!; - [JsonIgnore] - public Manga Manga + public Manga Manga { - get => LazyLoader.Load(this, ref _manga); - init => _manga = value; + get => LazyLoader.Load(this, ref _manga) ?? throw new InvalidOperationException(); + init + { + MangaId = value.Key; + _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 jobId, ulong recurrenceMs, string mangaId, string? parentJobId) - : base(lazyLoader, jobId, JobType.UpdateChaptersDownloadedJob, recurrenceMs, parentJobId) + internal UpdateChaptersDownloadedJob(ILazyLoader lazyLoader, string key, ulong recurrenceMs, string mangaId, string? parentJobId) + : base(lazyLoader, key, JobType.UpdateChaptersDownloadedJob, recurrenceMs, parentJobId) { this.MangaId = mangaId; } protected override IEnumerable RunInternal(PgsqlContext context) { - context.Entry(Manga).Reference(m => m.Library).Load(); + context.Entry(Manga).Reference(m => m.Library).Load(); foreach (Chapter mangaChapter in Manga.Chapters) { mangaChapter.Downloaded = mangaChapter.CheckDownloaded(); diff --git a/API/Schema/Jobs/UpdateCoverJob.cs b/API/Schema/Jobs/UpdateCoverJob.cs index 66d7d95..ab64c40 100644 --- a/API/Schema/Jobs/UpdateCoverJob.cs +++ b/API/Schema/Jobs/UpdateCoverJob.cs @@ -8,41 +8,48 @@ namespace API.Schema.Jobs; public class UpdateCoverJob : Job { - private MangaConnectorMangaEntry? _mangaConnectorMangaEntry = null!; + [StringLength(64)] [Required] public string MangaId { get; init; } = null!; + private Manga? _manga; + [JsonIgnore] - public MangaConnectorMangaEntry MangaConnectorMangaEntry + public Manga Manga { - get => LazyLoader.Load(this, ref _mangaConnectorMangaEntry) ?? throw new InvalidOperationException(); - init => _mangaConnectorMangaEntry = value; + get => LazyLoader.Load(this, ref _manga) ?? throw new InvalidOperationException(); + init + { + MangaId = value.Key; + _manga = value; + } } - public UpdateCoverJob(MangaConnectorMangaEntry mangaConnectorMangaEntry, ulong recurrenceMs, Job? parentJob = null, ICollection? dependsOnJobs = null) + public UpdateCoverJob(Manga manga, ulong recurrenceMs, Job? parentJob = null, ICollection? dependsOnJobs = null) : base(TokenGen.CreateToken(typeof(UpdateCoverJob)), JobType.UpdateCoverJob, recurrenceMs, parentJob, dependsOnJobs) { - this.MangaConnectorMangaEntry = mangaConnectorMangaEntry; + this.Manga = manga; } /// /// EF ONLY!!! /// - internal UpdateCoverJob(ILazyLoader lazyLoader, string jobId, ulong recurrenceMs, string? parentJobId) - : base(lazyLoader, jobId, JobType.UpdateCoverJob, recurrenceMs, parentJobId) + internal UpdateCoverJob(ILazyLoader lazyLoader, string key, string mangaId, ulong recurrenceMs, string? parentJobId) + : base(lazyLoader, key, JobType.UpdateCoverJob, recurrenceMs, parentJobId) { + this.MangaId = mangaId; } protected override IEnumerable RunInternal(PgsqlContext context) { bool keepCover = context.Jobs .Any(job => job.JobType == JobType.DownloadAvailableChaptersJob - && ((DownloadAvailableChaptersJob)job).MangaConnectorMangaEntry.MangaId == MangaConnectorMangaEntry.MangaId); + && ((DownloadAvailableChaptersJob)job).MangaId == MangaId); if (!keepCover) { - if(File.Exists(MangaConnectorMangaEntry.Manga.CoverFileNameInCache)) - File.Delete(MangaConnectorMangaEntry.Manga.CoverFileNameInCache); + if(File.Exists(Manga.CoverFileNameInCache)) + File.Delete(Manga.CoverFileNameInCache); try { - MangaConnectorMangaEntry.Manga.CoverFileNameInCache = null; + Manga.CoverFileNameInCache = null; context.Jobs.Remove(this); context.SaveChanges(); } @@ -53,7 +60,7 @@ public class UpdateCoverJob : Job } else { - return [new DownloadMangaCoverJob(MangaConnectorMangaEntry, this)]; + return [new DownloadMangaCoverJob(Manga, this)]; } return []; } diff --git a/API/Schema/LibraryConnectors/Kavita.cs b/API/Schema/LibraryConnectors/Kavita.cs index 9a674b9..c6f5964 100644 --- a/API/Schema/LibraryConnectors/Kavita.cs +++ b/API/Schema/LibraryConnectors/Kavita.cs @@ -44,8 +44,9 @@ public class Kavita : LibraryConnector { } } - catch (HttpRequestException e) + catch (HttpRequestException) { + } return ""; } @@ -53,13 +54,13 @@ public class Kavita : LibraryConnector protected override void UpdateLibraryInternal() { foreach (KavitaLibrary lib in GetLibraries()) - NetClient.MakePost($"{BaseUrl}/api/ToLibrary/scan?libraryId={lib.id}", "Bearer", Auth); + NetClient.MakePost($"{BaseUrl}/api/ToFileLibrary/scan?libraryId={lib.id}", "Bearer", Auth); } internal override bool Test() { foreach (KavitaLibrary lib in GetLibraries()) - if (NetClient.MakePost($"{BaseUrl}/api/ToLibrary/scan?libraryId={lib.id}", "Bearer", Auth)) + if (NetClient.MakePost($"{BaseUrl}/api/ToFileLibrary/scan?libraryId={lib.id}", "Bearer", Auth)) return true; return false; } @@ -70,7 +71,7 @@ public class Kavita : LibraryConnector /// Array of KavitaLibrary private IEnumerable GetLibraries() { - Stream data = NetClient.MakeRequest($"{BaseUrl}/api/ToLibrary/libraries", "Bearer", Auth); + Stream data = NetClient.MakeRequest($"{BaseUrl}/api/ToFileLibrary/libraries", "Bearer", Auth); if (data == Stream.Null) { Log.Info("No libraries found"); @@ -90,7 +91,7 @@ public class Kavita : LibraryConnector JsonObject? jObject = (JsonObject?)jsonNode; if(jObject is null) continue; - int libraryId = jObject!["id"]!.GetValue(); + int libraryId = jObject["id"]!.GetValue(); string libraryName = jObject["name"]!.GetValue(); ret.Add(new KavitaLibrary(libraryId, libraryName)); } diff --git a/API/Schema/Link.cs b/API/Schema/Link.cs index e797a1e..a96bb77 100644 --- a/API/Schema/Link.cs +++ b/API/Schema/Link.cs @@ -3,12 +3,9 @@ using Microsoft.EntityFrameworkCore; namespace API.Schema; -[PrimaryKey("LinkId")] -public class Link(string linkProvider, string linkUrl) +[PrimaryKey("Key")] +public class Link(string linkProvider, string linkUrl) : Identifiable(TokenGen.CreateToken(typeof(Link), linkProvider, linkUrl)) { - [StringLength(64)] - [Required] - public string LinkId { get; init; } = TokenGen.CreateToken(typeof(Link), linkProvider, linkUrl); [StringLength(64)] [Required] public string LinkProvider { get; init; } = linkProvider; @@ -17,8 +14,5 @@ public class Link(string linkProvider, string linkUrl) [Url] public string LinkUrl { get; init; } = linkUrl; - public override string ToString() - { - return $"{LinkId} {LinkProvider} {LinkUrl}"; - } + public override string ToString() => $"{base.ToString()} {LinkProvider} {LinkUrl}"; } \ No newline at end of file diff --git a/API/Schema/LocalLibrary.cs b/API/Schema/LocalLibrary.cs deleted file mode 100644 index 274b763..0000000 --- a/API/Schema/LocalLibrary.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System.ComponentModel.DataAnnotations; - -namespace API.Schema; - -public class LocalLibrary(string basePath, string libraryName) -{ - [StringLength(64)] - [Required] - public string LocalLibraryId { get; init; } = TokenGen.CreateToken(typeof(LocalLibrary), basePath); - [StringLength(256)] - [Required] - public string BasePath { get; internal set; } = basePath; - - [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 9fd80b4..5415cb0 100644 --- a/API/Schema/Manga.cs +++ b/API/Schema/Manga.cs @@ -2,6 +2,8 @@ using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; using System.Runtime.InteropServices; using System.Text; +using API.Schema.Contexts; +using API.Schema.Jobs; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Newtonsoft.Json; @@ -9,23 +11,22 @@ using static System.IO.UnixFileMode; namespace API.Schema; -[PrimaryKey("MangaId")] -public class Manga +[PrimaryKey("Key")] +public class Manga : Identifiable { - [StringLength(64)] [Required] public string MangaId { get; init; } [StringLength(512)] [Required] public string Name { get; internal set; } [Required] public string Description { get; internal set; } [JsonIgnore] [Url] [StringLength(512)] public string CoverUrl { get; internal set; } [Required] public MangaReleaseStatus ReleaseStatus { get; internal set; } [StringLength(64)] public string? LibraryId { get; private set; } - private LocalLibrary? _library = null!; + private FileLibrary? _library; [JsonIgnore] - public LocalLibrary? Library + public FileLibrary? Library { get => _lazyLoader.Load(this, ref _library); set { - LibraryId = value?.LocalLibraryId; + LibraryId = value?.Key; _library = value; } } @@ -33,10 +34,10 @@ public class Manga 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!; + 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; + [JsonIgnore] [StringLength(512)] public string? CoverFileNameInCache { get; internal set; } public uint? Year { get; internal init; } [StringLength(8)] public string? OriginalLanguage { get; internal init; } @@ -44,8 +45,8 @@ public class Manga [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 ICollection? _chapters = null!; + [NotMapped] public ICollection ChapterIds => Chapters.Select(c => c.Key).ToList(); + private ICollection? _chapters; [JsonIgnore] public ICollection Chapters { @@ -53,24 +54,23 @@ public class Manga init => _chapters = value; } - [NotMapped] - public ICollection LinkedMangaConnectors => - MangaConnectorLinkedToManga.Select(l => l.MangaConnectorName).ToList(); - private ICollection? _mangaConnectorLinkedToManga = null!; + [NotMapped] public Dictionary IdsOnMangaConnectors => + MangaConnectorIds.ToDictionary(id => id.MangaConnectorName, id => id.IdOnConnectorSite); + private ICollection>? _mangaConnectorIds; [JsonIgnore] - public ICollection MangaConnectorLinkedToManga + public ICollection> MangaConnectorIds { - get => _lazyLoader.Load(this, ref _mangaConnectorLinkedToManga) ?? throw new InvalidOperationException(); - init => _mangaConnectorLinkedToManga = value; + get => _lazyLoader.Load(this, ref _mangaConnectorIds) ?? throw new InvalidOperationException(); + private set => _mangaConnectorIds = value; } - + private readonly ILazyLoader _lazyLoader = null!; public Manga(string name, string description, string coverUrl, MangaReleaseStatus releaseStatus, - ICollection authors, ICollection mangaTags, ICollection links, ICollection altTitles, - LocalLibrary? library = null, float ignoreChaptersBefore = 0f, uint? year = null, string? originalLanguage = null) + ICollection authors, ICollection mangaTags, ICollection links, ICollection altTitles, + FileLibrary? library = null, float ignoreChaptersBefore = 0f, uint? year = null, string? originalLanguage = null) + :base(TokenGen.CreateToken(typeof(Manga), name)) { - this.MangaId = TokenGen.CreateToken(typeof(Manga), name); this.Name = name; this.Description = description; this.CoverUrl = coverUrl; @@ -90,11 +90,12 @@ public class Manga /// /// EF ONLY!!! /// - public Manga(ILazyLoader lazyLoader, string mangaId, string name, string description, string coverUrl, MangaReleaseStatus releaseStatus, + public Manga(ILazyLoader lazyLoader, string key, string name, string description, string coverUrl, + MangaReleaseStatus releaseStatus, string directoryName, float ignoreChaptersBefore, string? libraryId, uint? year, string? originalLanguage) + : base(key) { this._lazyLoader = lazyLoader; - this.MangaId = mangaId; this.Name = name; this.Description = description; this.CoverUrl = coverUrl; @@ -155,8 +156,53 @@ public class Manga return sb.ToString(); } - public override string ToString() + /// + /// + /// + /// + /// + /// + public void MergeFrom(Manga other, PgsqlContext context) { - return $"{MangaId} {Name}"; + try + { + context.Mangas.Remove(other); + List newJobs = new(); + + this.MangaConnectorIds = this.MangaConnectorIds + .UnionBy(other.MangaConnectorIds, id => id.MangaConnectorName) + .ToList(); + + foreach (Chapter otherChapter in other.Chapters) + { + string oldPath = otherChapter.FullArchiveFilePath; + Chapter newChapter = new(this, otherChapter.ChapterNumber, otherChapter.VolumeNumber, + otherChapter.Title); + this.Chapters.Add(newChapter); + string newPath = newChapter.FullArchiveFilePath; + newJobs.Add(new MoveFileOrFolderJob(oldPath, newPath)); + } + + if (other.Chapters.Count > 0) + newJobs.Add(new UpdateChaptersDownloadedJob(this, 0, null, newJobs)); + + context.Jobs.AddRange(newJobs); + context.SaveChanges(); + } + catch (DbUpdateException e) + { + throw new DbUpdateException(e.Message, e.InnerException, e.Entries); + } } + + public override string ToString() => $"{base.ToString()} {Name}"; +} + +public enum MangaReleaseStatus : byte +{ + Continuing = 0, + Completed = 1, + OnHiatus = 2, + Cancelled = 3, + Unreleased = 4 } \ No newline at end of file diff --git a/API/Schema/MangaAltTitle.cs b/API/Schema/MangaAltTitle.cs deleted file mode 100644 index 5775a6c..0000000 --- a/API/Schema/MangaAltTitle.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using Microsoft.EntityFrameworkCore; - -namespace API.Schema; - -[PrimaryKey("AltTitleId")] -public class MangaAltTitle(string language, string title) -{ - [StringLength(64)] - [Required] - 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/MangaConnectorMangaEntry.cs b/API/Schema/MangaConnectorId.cs similarity index 52% rename from API/Schema/MangaConnectorMangaEntry.cs rename to API/Schema/MangaConnectorId.cs index 6d5a4d2..d4bb47e 100644 --- a/API/Schema/MangaConnectorMangaEntry.cs +++ b/API/Schema/MangaConnectorId.cs @@ -6,21 +6,25 @@ using Newtonsoft.Json; namespace API.Schema; -[PrimaryKey("MangaId", "MangaConnectorName")] -public class MangaConnectorMangaEntry +[PrimaryKey("Key")] +public class MangaConnectorId : Identifiable where T : Identifiable { - [StringLength(64)] [Required] public string MangaId { get; private set; } = null!; - [JsonIgnore] private Manga? _manga = null!; + [StringLength(64)] [Required] public string ObjId { get; private set; } = null!; + [JsonIgnore] private T? _obj; [JsonIgnore] - public Manga Manga + public T Obj { - get => _lazyLoader.Load(this, ref _manga) ?? throw new InvalidOperationException(); - init => _manga = value; + get => _lazyLoader.Load(this, ref _obj) ?? throw new InvalidOperationException(); + internal set + { + ObjId = value.Key; + _obj = value; + } } [StringLength(32)] [Required] public string MangaConnectorName { get; private set; } = null!; - [JsonIgnore] private MangaConnector? _mangaConnector = null!; + [JsonIgnore] private MangaConnector? _mangaConnector; [JsonIgnore] public MangaConnector MangaConnector { @@ -33,13 +37,14 @@ public class MangaConnectorMangaEntry } [StringLength(256)] [Required] public string IdOnConnectorSite { get; init; } - [Url] [StringLength(512)] [Required] public string WebsiteUrl { get; internal init; } + [Url] [StringLength(512)] public string? WebsiteUrl { get; internal init; } private readonly ILazyLoader _lazyLoader = null!; - public MangaConnectorMangaEntry(Manga manga, MangaConnector mangaConnector, string idOnConnectorSite, string websiteUrl) + public MangaConnectorId(T obj, MangaConnector mangaConnector, string idOnConnectorSite, string? websiteUrl) + : base(TokenGen.CreateToken(typeof(MangaConnectorId), mangaConnector.Name, idOnConnectorSite)) { - this.Manga = manga; + this.Obj = obj; this.MangaConnector = mangaConnector; this.IdOnConnectorSite = idOnConnectorSite; this.WebsiteUrl = websiteUrl; @@ -48,12 +53,15 @@ public class MangaConnectorMangaEntry /// /// EF CORE ONLY!!! /// - public MangaConnectorMangaEntry(ILazyLoader lazyLoader, string mangaId, string mangaConnectorName, string idOnConnectorSite, string websiteUrl) + public MangaConnectorId(ILazyLoader lazyLoader, string key, string objId, string mangaConnectorName, string idOnConnectorSite, string? websiteUrl) + : base(key) { this._lazyLoader = lazyLoader; - this.MangaId = mangaId; + this.ObjId = objId; this.MangaConnectorName = mangaConnectorName; this.IdOnConnectorSite = idOnConnectorSite; this.WebsiteUrl = websiteUrl; } + + public override string ToString() => $"{base.ToString()} {_obj}"; } \ No newline at end of file diff --git a/API/Schema/MangaConnectors/ComickIo.cs b/API/Schema/MangaConnectors/ComickIo.cs index 8fd44a3..beb1c7c 100644 --- a/API/Schema/MangaConnectors/ComickIo.cs +++ b/API/Schema/MangaConnectors/ComickIo.cs @@ -17,9 +17,9 @@ public class ComickIo : MangaConnector this.downloadClient = new HttpDownloadClient(); } - public override MangaConnectorMangaEntry[] SearchManga(string mangaSearchName) + public override (Manga, MangaConnectorId)[] SearchManga(string mangaSearchName) { - Log.Info($"Searching Manga: {mangaSearchName}"); + Log.Info($"Searching Obj: {mangaSearchName}"); List slugs = new(); int page = 1; @@ -46,20 +46,26 @@ public class ComickIo : MangaConnector } Log.Debug($"Search {mangaSearchName} yielded {slugs.Count} slugs. Requesting mangas now..."); - List mangas = slugs.Select(GetMangaFromId).ToList()!; + + List<(Manga, MangaConnectorId)> mangas = new (); + foreach (string slug in slugs) + { + if(GetMangaFromId(slug) is { } entry) + mangas.Add(entry); + } Log.Info($"Search {mangaSearchName} yielded {mangas.Count} results."); return mangas.ToArray(); } private readonly Regex _getSlugFromTitleRex = new(@"https?:\/\/comick\.io\/comic\/(.+)(?:\/.*)*"); - public override MangaConnectorMangaEntry? GetMangaFromUrl(string url) + public override (Manga, MangaConnectorId)? GetMangaFromUrl(string url) { Match m = _getSlugFromTitleRex.Match(url); return m.Groups[1].Success ? GetMangaFromId(m.Groups[1].Value) : null; } - public override MangaConnectorMangaEntry? GetMangaFromId(string mangaIdOnSite) + public override (Manga, MangaConnectorId)? GetMangaFromId(string mangaIdOnSite) { string requestUrl = $"https://api.comick.fun/comic/{mangaIdOnSite}"; @@ -75,14 +81,15 @@ public class ComickIo : MangaConnector return ParseMangaFromJToken(data); } - public override Chapter[] GetChapters(MangaConnectorMangaEntry mangaConnectorMangaEntry, string? language = null) + public override (Chapter, MangaConnectorId)[] GetChapters(MangaConnectorId mangaConnectorId, + string? language = null) { - Log.Info($"Getting Chapters: {mangaConnectorMangaEntry.IdOnConnectorSite}"); - List chapters = new(); + Log.Info($"Getting Chapters: {mangaConnectorId.IdOnConnectorSite}"); + List<(Chapter, MangaConnectorId)> chapters = new(); int page = 1; while(page < 50) { - string requestUrl = $"https://api.comick.fun/comic/{mangaConnectorMangaEntry.IdOnConnectorSite}/chapters?limit=100&page={page}&lang={language}"; + string requestUrl = $"https://api.comick.fun/comic/{mangaConnectorId.IdOnConnectorSite}/chapters?limit=100&page={page}&lang={language}"; RequestResult result = downloadClient.MakeRequest(requestUrl, RequestType.MangaInfo); if ((int)result.statusCode < 200 || (int)result.statusCode >= 300) @@ -98,7 +105,7 @@ public class ComickIo : MangaConnector if (chaptersArray is null || chaptersArray.Count < 1) break; - chapters.AddRange(ParseChapters(mangaConnectorMangaEntry, chaptersArray)); + chapters.AddRange(ParseChapters(mangaConnectorId, chaptersArray)); page++; } @@ -107,11 +114,22 @@ public class ComickIo : MangaConnector } private readonly Regex _hidFromUrl = new(@"https?:\/\/comick\.io\/comic\/.+\/([^-]+).*"); - internal override string[] GetChapterImageUrls(Chapter chapter) + internal override string[] GetChapterImageUrls(MangaConnectorId chapterId) { - Match m = _hidFromUrl.Match(chapter.Url); - if (!m.Groups[1].Success) + + Log.Info($"Getting Chapter Image-Urls: {chapterId.Obj}"); + if (chapterId.WebsiteUrl is null || !UrlMatchesConnector(chapterId.WebsiteUrl)) + { + Log.Debug($"Url is not for Connector. {chapterId.WebsiteUrl}"); return []; + } + + Match m = _hidFromUrl.Match(chapterId.WebsiteUrl); + if (!m.Groups[1].Success) + { + Log.Debug($"Could not parse hid from url. {chapterId.WebsiteUrl}"); + return []; + } string hid = m.Groups[1].Value; @@ -133,7 +151,7 @@ public class ComickIo : MangaConnector }).ToArray(); } - private MangaConnectorMangaEntry ParseMangaFromJToken(JToken json) + private (Manga manga, MangaConnectorId id) ParseMangaFromJToken(JToken json) { string? hid = json["comic"]?.Value("hid"); string? slug = json["comic"]?.Value("slug"); @@ -156,15 +174,15 @@ public class ComickIo : MangaConnector 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")!)) + List altTitles = altTitlesArray? + .Select(token => new AltTitle(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) + .DistinctBy(a => a.Key) .ToList()!; JArray? genreArray = json["comic"]?["md_comic_md_genres"] as JArray; @@ -192,7 +210,7 @@ public class ComickIo : MangaConnector "al" => "AniList", "ap" => "Anime Planet", "bw" => "BookWalker", - "mu" => "Manga Updates", + "mu" => "Obj Updates", "nu" => "Novel Updates", "kt" => "Kitsu.io", "amz" => "Amazon", @@ -213,12 +231,12 @@ public class ComickIo : MangaConnector Manga manga = new (name, description??"", coverUrl, status, authors, tags, links, altTitles, year: year, originalLanguage: originalLanguage); - return new MangaConnectorMangaEntry(manga, this, hid, url); + return (manga, new MangaConnectorId(manga, this, hid, url)); } - private List ParseChapters(MangaConnectorMangaEntry mangaConnectorMangaEntry, JArray chaptersArray) + private List<(Chapter, MangaConnectorId)> ParseChapters(MangaConnectorId mcIdManga, JArray chaptersArray) { - List chapters = new (); + List<(Chapter, MangaConnectorId)> chapters = new (); foreach (JToken chapter in chaptersArray) { string? chapterNum = chapter.Value("chap"); @@ -226,12 +244,14 @@ public class ComickIo : MangaConnector int? volumeNum = volumeNumStr is null ? null : int.Parse(volumeNumStr); string? title = chapter.Value("title"); string? hid = chapter.Value("hid"); - string url = $"https://comick.io/comic/{mangaConnectorMangaEntry.IdOnConnectorSite}/{hid}"; + string url = $"https://comick.io/comic/{mcIdManga.IdOnConnectorSite}/{hid}"; if(chapterNum is null || hid is null) continue; + + Chapter ch = new (mcIdManga.Obj, chapterNum, volumeNum, title); - chapters.Add(new (mangaConnectorMangaEntry, url, chapterNum, volumeNum, hid, title)); + chapters.Add((ch, new (ch, this, hid, url))); } return chapters; } diff --git a/API/Schema/MangaConnectors/Global.cs b/API/Schema/MangaConnectors/Global.cs index 6fdac62..3c54300 100644 --- a/API/Schema/MangaConnectors/Global.cs +++ b/API/Schema/MangaConnectors/Global.cs @@ -10,14 +10,14 @@ public class Global : MangaConnector this.context = context; } - public override MangaConnectorMangaEntry[] SearchManga(string mangaSearchName) + public override (Manga, MangaConnectorId)[] 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[] tasks = - enabledConnectors.Select(c => new Task(() => c.SearchManga(mangaSearchName))).ToArray(); + //Create Task for each MangaConnector to search simultaneously + Task<(Manga, MangaConnectorId)[]>[] tasks = + enabledConnectors.Select(c => new Task<(Manga, MangaConnectorId)[]>(() => c.SearchManga(mangaSearchName))).ToArray(); foreach (var task in tasks) task.Start(); @@ -28,28 +28,29 @@ public class Global : MangaConnector }while(tasks.Any(t => t.Status < TaskStatus.RanToCompletion)); //Concatenate all results into one - MangaConnectorMangaEntry[] ret = tasks.Select(t => t.IsCompletedSuccessfully ? t.Result : []).ToArray().SelectMany(i => i).ToArray(); + (Manga, MangaConnectorId)[] ret = tasks.Select(t => t.IsCompletedSuccessfully ? t.Result : []).ToArray().SelectMany(i => i).ToArray(); return ret; } - public override MangaConnectorMangaEntry? GetMangaFromUrl(string url) + public override (Manga, MangaConnectorId)? GetMangaFromUrl(string url) { MangaConnector? mc = context.MangaConnectors.ToArray().FirstOrDefault(c => c.UrlMatchesConnector(url)); return mc?.GetMangaFromUrl(url) ?? null; } - public override MangaConnectorMangaEntry? GetMangaFromId(string mangaIdOnSite) + public override (Manga, MangaConnectorId)? GetMangaFromId(string mangaIdOnSite) { return null; } - public override Chapter[] GetChapters(MangaConnectorMangaEntry mangaConnectorMangaEntry, string? language = null) + public override (Chapter, MangaConnectorId)[] GetChapters(MangaConnectorId manga, + string? language = null) { - return mangaConnectorMangaEntry.MangaConnector.GetChapters(mangaConnectorMangaEntry, language); + return manga.MangaConnector.GetChapters(manga, language); } - internal override string[] GetChapterImageUrls(Chapter chapter) + internal override string[] GetChapterImageUrls(MangaConnectorId chapterId) { - return chapter.MangaConnectorMangaEntry.MangaConnector.GetChapterImageUrls(chapter); + return chapterId.MangaConnector.GetChapterImageUrls(chapterId); } } \ No newline at end of file diff --git a/API/Schema/MangaConnectors/MangaConnector.cs b/API/Schema/MangaConnectors/MangaConnector.cs index 0afb878..4b82725 100644 --- a/API/Schema/MangaConnectors/MangaConnector.cs +++ b/API/Schema/MangaConnectors/MangaConnector.cs @@ -34,35 +34,36 @@ public abstract class MangaConnector(string name, string[] supportedLanguages, s [Required] public bool Enabled { get; internal set; } = true; - public abstract MangaConnectorMangaEntry[] SearchManga(string mangaSearchName); + public abstract (Manga, MangaConnectorId)[] SearchManga(string mangaSearchName); - public abstract MangaConnectorMangaEntry? GetMangaFromUrl(string url); + public abstract (Manga, MangaConnectorId)? GetMangaFromUrl(string url); - public abstract MangaConnectorMangaEntry? GetMangaFromId(string mangaIdOnSite); + public abstract (Manga, MangaConnectorId)? GetMangaFromId(string mangaIdOnSite); - public abstract Chapter[] GetChapters(MangaConnectorMangaEntry mangaConnectorMangaEntry, string? language = null); + public abstract (Chapter, MangaConnectorId)[] GetChapters(MangaConnectorId mangaId, + string? language = null); - internal abstract string[] GetChapterImageUrls(Chapter chapter); + internal abstract string[] GetChapterImageUrls(MangaConnectorId chapterId); public bool UrlMatchesConnector(string url) => BaseUris.Any(baseUri => Regex.IsMatch(url, "https?://" + baseUri + "/.*")); - internal string? SaveCoverImageToCache(Manga manga, int retries = 3) + internal string? SaveCoverImageToCache(MangaConnectorId mangaId, 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}"; + Match match = urlRex.Match(mangaId.Obj.CoverUrl); + string filename = $"{match.Groups[1].Value}-{mangaId.Key}.{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}"); + RequestResult coverResult = downloadClient.MakeRequest(mangaId.Obj.CoverUrl, RequestType.MangaCover, $"https://{match.Groups[1].Value}"); if ((int)coverResult.statusCode < 200 || (int)coverResult.statusCode >= 300) - return SaveCoverImageToCache(manga, --retries); + return SaveCoverImageToCache(mangaId, --retries); using MemoryStream ms = new(); coverResult.result.CopyTo(ms); diff --git a/API/Schema/MangaConnectors/MangaDex.cs b/API/Schema/MangaConnectors/MangaDex.cs index 31308d3..12576be 100644 --- a/API/Schema/MangaConnectors/MangaDex.cs +++ b/API/Schema/MangaConnectors/MangaDex.cs @@ -18,10 +18,10 @@ public class MangaDex : MangaConnector } private const int Limit = 100; - public override MangaConnectorMangaEntry[] SearchManga(string mangaSearchName) + public override (Manga, MangaConnectorId)[] SearchManga(string mangaSearchName) { - Log.Info($"Searching Manga: {mangaSearchName}"); - List mangas = new (); + Log.Info($"Searching Obj: {mangaSearchName}"); + List<(Manga, MangaConnectorId)> mangas = new (); int offset = 0; int total = int.MaxValue; @@ -67,9 +67,9 @@ public class MangaDex : MangaConnector } private static readonly Regex GetMangaIdFromUrl = new(@"https?:\/\/mangadex\.org\/title\/([a-z0-9-]+)\/?.*"); - public override MangaConnectorMangaEntry? GetMangaFromUrl(string url) + public override (Manga, MangaConnectorId)? GetMangaFromUrl(string url) { - Log.Info($"Getting Manga: {url}"); + Log.Info($"Getting Obj: {url}"); if (!UrlMatchesConnector(url)) { Log.Debug($"Url is not for Connector. {url}"); @@ -87,9 +87,9 @@ public class MangaDex : MangaConnector return GetMangaFromId(id); } - public override MangaConnectorMangaEntry? GetMangaFromId(string mangaIdOnSite) + public override (Manga, MangaConnectorId)? GetMangaFromId(string mangaIdOnSite) { - Log.Info($"Getting Manga: {mangaIdOnSite}"); + Log.Info($"Getting Obj: {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'"; @@ -121,17 +121,17 @@ public class MangaDex : MangaConnector return ParseMangaFromJToken(data); } - public override Chapter[] GetChapters(MangaConnectorMangaEntry mangaConnectorMangaEntry, string? language = null) + public override (Chapter, MangaConnectorId)[] GetChapters(MangaConnectorId manga, string? language = null) { - Log.Info($"Getting Chapters: {mangaConnectorMangaEntry.IdOnConnectorSite}"); - List chapters = new (); + Log.Info($"Getting Chapters: {manga.IdOnConnectorSite}"); + List<(Chapter, MangaConnectorId)> chapters = new (); int offset = 0; int total = int.MaxValue; while(offset < total) { string requestUrl = - $"https://api.mangadex.org/manga/{mangaConnectorMangaEntry.IdOnConnectorSite}/feed?limit={Limit}&offset={offset}&" + + $"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; @@ -162,27 +162,27 @@ public class MangaDex : MangaConnector return []; } - chapters.AddRange(data.Select(d => ParseChapterFromJToken(mangaConnectorMangaEntry, d))); + chapters.AddRange(data.Select(d => ParseChapterFromJToken(manga, d))); } - Log.Info($"Request for chapters for {mangaConnectorMangaEntry.Manga.Name} yielded {chapters.Count} results."); + Log.Info($"Request for chapters for {manga.Obj.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) + internal override string[] GetChapterImageUrls(MangaConnectorId chapterId) { - Log.Info($"Getting Chapter Image-Urls: {chapter.Url}"); - if (!UrlMatchesConnector(chapter.Url)) + Log.Info($"Getting Chapter Image-Urls: {chapterId.Obj}"); + if (chapterId.WebsiteUrl is null || !UrlMatchesConnector(chapterId.WebsiteUrl)) { - Log.Debug($"Url is not for Connector. {chapter.Url}"); + Log.Debug($"Url is not for Connector. {chapterId.WebsiteUrl}"); return []; } - Match match = GetChapterIdFromUrl.Match(chapter.Url); + Match match = GetChapterIdFromUrl.Match(chapterId.WebsiteUrl); if (!match.Success || !match.Groups[1].Success) { - Log.Debug($"Url is not for Connector (Could not retrieve id). {chapter.Url}"); + Log.Debug($"Url is not for Connector (Could not retrieve id). {chapterId.WebsiteUrl}"); return []; } @@ -222,7 +222,7 @@ public class MangaDex : MangaConnector return urls.ToArray(); } - private MangaConnectorMangaEntry ParseMangaFromJToken(JToken jToken) + private (Manga manga, MangaConnectorId id) ParseMangaFromJToken(JToken jToken) { string? id = jToken.Value("id"); if(id is null) @@ -266,7 +266,7 @@ public class MangaDex : MangaConnector "al" => "AniList", "ap" => "Anime Planet", "bw" => "BookWalker", - "mu" => "Manga Updates", + "mu" => "Obj Updates", "nu" => "Novel Updates", "kt" => "Kitsu.io", "amz" => "Amazon", @@ -278,14 +278,14 @@ public class MangaDex : MangaConnector return new Link(key, url); }).ToList()!; - List altTitles = (altTitlesJArray??[]) + 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()); + return new AltTitle(p.Name, p.Value.ToString()); }).Where(x => x is not null).ToList()!; List tags = (tagsJArray??[]) @@ -314,24 +314,25 @@ public class MangaDex : MangaConnector Manga manga = new Manga(name, description, coverUrl, releaseStatus, authors, tags, links,altTitles, null, 0f, year, originalLanguage); - return new MangaConnectorMangaEntry(manga, this, id, websiteUrl); + return (manga, new MangaConnectorId(manga, this, id, websiteUrl)); } - private Chapter ParseChapterFromJToken(MangaConnectorMangaEntry mangaConnectorMangaEntry, JToken jToken) + private (Chapter chapter, MangaConnectorId id) ParseChapterFromJToken(MangaConnectorId mcIdManga, JToken jToken) { string? id = jToken.Value("id"); JToken? attributes = jToken["attributes"]; - string? chapter = attributes?.Value("chapter"); + string? chapterStr = attributes?.Value("chapter"); string? volumeStr = attributes?.Value("volume"); - int? volume = null; + int? volumeNumber = null; string? title = attributes?.Value("title"); - if(id is null || chapter is null) + if(id is null || chapterStr is null) throw new Exception("jToken was not in expected format"); if(volumeStr is not null) - volume = int.Parse(volumeStr); + volumeNumber = int.Parse(volumeStr); - string url = $"https://mangadex.org/chapter/{id}"; - return new Chapter(mangaConnectorMangaEntry, url, chapter, volume, id, title); + string websiteUrl = $"https://mangadex.org/chapter/{id}"; + Chapter chapter = new (mcIdManga.Obj, chapterStr, volumeNumber, title); + return (chapter, new MangaConnectorId(chapter, this, id, websiteUrl)); } } \ No newline at end of file diff --git a/API/Schema/MangaReleaseStatus.cs b/API/Schema/MangaReleaseStatus.cs deleted file mode 100644 index e405b0e..0000000 --- a/API/Schema/MangaReleaseStatus.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace API.Schema; - -public enum MangaReleaseStatus : byte -{ - Continuing = 0, - Completed = 1, - OnHiatus = 2, - Cancelled = 3, - Unreleased = 4 -} \ No newline at end of file diff --git a/API/Tranga.cs b/API/Tranga.cs index 8087e59..ecfa0fc 100644 --- a/API/Tranga.cs +++ b/API/Tranga.cs @@ -137,18 +137,7 @@ public static class Tranga List dueJobs = waitingJobs.FilterDueJobs(); List jobsWithoutDependencies = dueJobs.FilterJobDependencies(); - List jobsWithoutDownloading = jobsWithoutDependencies.FilterJobsWithoutDownloading(); - - //Match running and waiting jobs per Connector - Dictionary>> runningJobsPerConnector = - runningJobs.GetJobsPerJobTypeAndConnector(); - Dictionary>> waitingJobsPerConnector = - jobsWithoutDependencies.GetJobsPerJobTypeAndConnector(); - List jobsNotHeldBackByConnector = - MatchJobsRunningAndWaiting(runningJobsPerConnector, waitingJobsPerConnector); - - - List startJobs = jobsWithoutDownloading.Concat(jobsNotHeldBackByConnector).ToList(); + List startJobs = dueJobs; Log.Debug($"Jobs Filtered! (took {DateTime.UtcNow.Subtract(filterStart).TotalMilliseconds}ms)"); @@ -160,7 +149,7 @@ public static class Tranga { using IServiceScope jobScope = serviceProvider.CreateScope(); PgsqlContext jobContext = jobScope.ServiceProvider.GetRequiredService(); - if (jobContext.Jobs.Find(job.JobId) is not { } inContext) + if (jobContext.Jobs.Find(job.Key) is not { } inContext) return; inContext.Run(jobContext, ref running); //FIND the job IN THE NEW CONTEXT!!!!!!! SO WE DON'T GET TRACKING PROBLEMS AND AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA }); @@ -174,14 +163,12 @@ public static class Tranga $"Waiting: {waitingJobs.Count} Due: {dueJobs.Count}\n" + $"{string.Join("\n", dueJobs.Select(s => "\t- " + s))}\n" + $"of which {jobsWithoutDependencies.Count} without missing dependencies, of which\n" + - $"\t{jobsWithoutDownloading.Count} without downloading\n" + - $"\t{jobsNotHeldBackByConnector.Count} not held back by Connector\n" + $"{startJobs.Count} were started:\n" + $"{string.Join("\n", startJobs.Select(s => "\t- " + s))}"); if (Log.IsDebugEnabled && dueJobs.Count < 1) if(waitingJobs.MinBy(j => j.NextExecution) is { } nextJob) - Log.Debug($"Next job in {nextJob.NextExecution.Subtract(DateTime.UtcNow)} (at {nextJob.NextExecution}): {nextJob.JobId}"); + Log.Debug($"Next job in {nextJob.NextExecution.Subtract(DateTime.UtcNow)} (at {nextJob.NextExecution}): {nextJob.Key}"); (Thread, Job)[] removeFromThreadsList = RunningJobs.Where(t => !t.Key.IsAlive) .Select(t => (t.Key, t.Value)).ToArray(); @@ -252,26 +239,6 @@ public static class Tranga Log.Debug($"Filtering Jobs without Download took {end.Subtract(start).TotalMilliseconds}ms"); return ret; } - - - private static Dictionary>> GetJobsPerJobTypeAndConnector(this List jobs) - { - DateTime start = DateTime.UtcNow; - Dictionary>> ret = new(); - foreach (Job job in jobs) - { - if(GetJobConnectorName(job) is not { } connector) - continue; - if (!ret.ContainsKey(connector)) - ret.Add(connector, new()); - if (!ret[connector].ContainsKey(job.JobType)) - ret[connector].Add(job.JobType, new()); - ret[connector][job.JobType].Add(job); - } - DateTime end = DateTime.UtcNow; - Log.Debug($"Fetching connector per Job for jobs took {end.Subtract(start).TotalMilliseconds}ms"); - return ret; - } private static List MatchJobsRunningAndWaiting(Dictionary>> running, Dictionary>> waiting) @@ -321,18 +288,4 @@ public static class Tranga Log.Debug($"Getting eligible jobs (not held back by Connector) took {end.Subtract(start).TotalMilliseconds}ms"); return ret; } - - - private static string? GetJobConnectorName(Job job) - { - if (job is DownloadAvailableChaptersJob dacj) - return dacj.Manga.MangaConnectorName; - if (job is DownloadMangaCoverJob dmcj) - return dmcj.Manga.MangaConnectorName; - if (job is DownloadSingleChapterJob dscj) - return dscj.Chapter.ParentManga.MangaConnectorName; - if (job is RetrieveChaptersJob rcj) - return rcj.Manga.MangaConnectorName; - return null; - } } \ No newline at end of file diff --git a/API/TrangaSettings.cs b/API/TrangaSettings.cs index 6c4419b..1857d6c 100644 --- a/API/TrangaSettings.cs +++ b/API/TrangaSettings.cs @@ -8,7 +8,7 @@ namespace API; public static class TrangaSettings { - public static string downloadLocation { get; private set; } = (RuntimeInformation.IsOSPlatform(OSPlatform.Linux) ? "/Manga" : Path.Join(Directory.GetCurrentDirectory(), "Downloads")); + public static string downloadLocation { get; private set; } = (RuntimeInformation.IsOSPlatform(OSPlatform.Linux) ? "/Obj" : Path.Join(Directory.GetCurrentDirectory(), "Downloads")); public static string workingDirectory { get; private set; } = Path.Join(RuntimeInformation.IsOSPlatform(OSPlatform.Linux) ? "/usr/share" : Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "tranga-api"); [JsonIgnore] internal static readonly string DefaultUserAgent = $"Tranga/2.0 ({Enum.GetName(Environment.OSVersion.Platform)}; {(Environment.Is64BitOperatingSystem ? "x64" : "")})"; @@ -18,14 +18,14 @@ public static class TrangaSettings public static string flareSolverrUrl { get; private set; } = string.Empty; /// /// Placeholders: - /// %M Manga Name + /// %M Obj Name /// %V Volume /// %C Chapter /// %T Title /// %A Author (first in list) /// %I Chapter Internal ID - /// %i Manga Internal ID - /// %Y Year (Manga) + /// %i Obj Internal ID + /// %Y Year (Obj) /// /// ?_(...) replace _ with a value from above: /// Everything inside the braces will only be added if the value of %_ is not null diff --git a/DB-Layout.png b/DB-Layout.png new file mode 100644 index 0000000000000000000000000000000000000000..38bfd9fb1f749805264702e1ce998f37acd487ad GIT binary patch literal 37522 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 literal 0 HcmV?d00001 diff --git a/DB-Layout.uxf b/DB-Layout.uxf new file mode 100644 index 0000000..3f6fa0e --- /dev/null +++ b/DB-Layout.uxf @@ -0,0 +1,460 @@ + + + 10 + + UMLClass + + 1160 + 680 + 100 + 40 + + Manga + + + + UMLClass + + 900 + 680 + 140 + 40 + + MangaConnector + + + + UMLClass + + 500 + 800 + 80 + 40 + + /Job/ + + + + UMLClass + + 680 + 800 + 160 + 40 + + /JobWithDownload/ + + + + UMLClass + + 680 + 680 + 160 + 40 + + RetrieveChaptersJob + + + + + Relation + + 570 + 810 + 130 + 30 + + lt=<<- + 10.0;10.0;110.0;10.0 + + + Relation + + 750 + 710 + 30 + 110 + + lt=<<- + 10.0;90.0;10.0;10.0 + + + Relation + + 1170 + 710 + 30 + 230 + + lt=- + 10.0;210.0;10.0;10.0 + + + Relation + + 1030 + 820 + 150 + 140 + + lt=- + 130.0;120.0;70.0;120.0;70.0;10.0;10.0;10.0 + + + Relation + + 960 + 710 + 30 + 110 + + lt=- + 10.0;90.0;10.0;10.0 + + + Relation + + 830 + 810 + 90 + 30 + + lt=- + 70.0;10.0;10.0;10.0 + + + UMLClass + + 1410 + 680 + 100 + 40 + + FileLibrary + + + + Relation + + 1250 + 690 + 180 + 30 + + lt=- + 160.0;10.0;10.0;10.0 + + + UMLClass + + 1410 + 620 + 100 + 40 + + Link + + + + UMLClass + + 1410 + 560 + 100 + 40 + + Author + + + + UMLClass + + 1410 + 500 + 100 + 40 + + MangaTag + + + + Relation + + 1200 + 510 + 230 + 190 + + lt=- + 210.0;10.0;10.0;10.0;10.0;170.0 + + + Relation + + 1230 + 570 + 200 + 130 + + lt=- + 180.0;10.0;10.0;10.0;10.0;110.0 + + + Relation + + 1250 + 630 + 180 + 70 + + lt=- + 160.0;10.0;90.0;10.0;90.0;50.0;10.0;50.0 + + + UMLClass + + 1410 + 440 + 100 + 40 + + AltTitle + + + + Relation + + 1170 + 450 + 260 + 250 + + lt=- + 240.0;10.0;10.0;10.0;10.0;230.0 + + + UMLClass + + 1380 + 800 + 160 + 40 + + MangaMetadataEntry + + + + Relation + + 1230 + 710 + 170 + 130 + + lt=- + 150.0;110.0;10.0;110.0;10.0;10.0 + + + UMLClass + + 1650 + 800 + 140 + 40 + + MetadataFetcher + + + + Relation + + 1530 + 810 + 140 + 30 + + lt=- + 120.0;10.0;10.0;10.0 + + + UMLUseCase + + 1660 + 680 + 120 + 40 + + Path + + + + Relation + + 1500 + 690 + 180 + 30 + + lt=- + 160.0;10.0;10.0;10.0 + + + UMLClass + + 900 + 800 + 140 + 40 + + MangaConnectorID + + + + Relation + + 1030 + 690 + 150 + 140 + + lt=- + 130.0;10.0;70.0;10.0;70.0;120.0;10.0;120.0 + + + UMLClass + + 1160 + 920 + 100 + 40 + + Chapter + + + + + UMLClass + + 460 + 680 + 160 + 40 + + UpdateChapters +DownloadedJob + + + + Relation + + 530 + 710 + 30 + 110 + + lt=<<- + 10.0;90.0;10.0;10.0 + + + UMLClass + + 1970 + 640 + 110 + 40 + + lw=2 +Komga + + + + UMLClass + + 1970 + 710 + 110 + 40 + + lw=2 +Kavita + + + + UMLPackage + + 1930 + 600 + 190 + 170 + + Library + + + + Relation + + 1770 + 690 + 180 + 30 + + lt=- + 160.0;10.0;10.0;10.0 + + + UMLClass + + 710 + 910 + 100 + 30 + + /Identifiable/ + + + + Relation + + 750 + 830 + 30 + 100 + + lt=<<- + 10.0;80.0;10.0;10.0 + + + Relation + + 530 + 830 + 200 + 120 + + lt=<<- + 180.0;100.0;10.0;100.0;10.0;10.0 + + + Relation + + 750 + 930 + 480 + 110 + + lt=<<- + 10.0;10.0;10.0;90.0;460.0;90.0;460.0;30.0 + + + Relation + + 800 + 670 + 380 + 280 + + lt=<<- + 10.0;260.0;260.0;260.0;260.0;10.0;360.0;10.0 + + diff --git a/README.md b/README.md index ddfcf70..bc848b6 100644 --- a/README.md +++ b/README.md @@ -154,6 +154,8 @@ Tranga is using a code-first Entity-Framework Core approach. If you modify the d **A broad overview of where is what:**
+![Image](DB-Layout.png) + - `Program.cs` Configuration for ASP.NET, Swagger (also in `NamedSwaggerGenOptions.cs`) - `Tranga.cs` Worker-Logic - `Schema/` Entity-Framework diff --git a/Tranga.sln.DotSettings b/Tranga.sln.DotSettings index e47766a..c9d6e11 100644 --- a/Tranga.sln.DotSettings +++ b/Tranga.sln.DotSettings @@ -1,8 +1,10 @@  True True + True True True + True True True True @@ -10,5 +12,6 @@ True True True + True True True \ No newline at end of file