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 0000000..38bfd9f Binary files /dev/null and b/DB-Layout.png differ 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