diff --git a/API/Controllers/JobController.cs b/API/Controllers/JobController.cs index 72dee34..2ccb3a2 100644 --- a/API/Controllers/JobController.cs +++ b/API/Controllers/JobController.cs @@ -203,7 +203,6 @@ public class JobController(PgsqlContext context) : Controller Job? ret = context.Jobs.Find(id); if (ret is null) return NotFound(); - ret.NextExecution = DateTime.UnixEpoch; try { context.Update(ret); diff --git a/API/Migrations/PgsqlContextModelSnapshot.cs b/API/Migrations/PgsqlContextModelSnapshot.cs index 461fb19..41a4ae8 100644 --- a/API/Migrations/PgsqlContextModelSnapshot.cs +++ b/API/Migrations/PgsqlContextModelSnapshot.cs @@ -47,10 +47,6 @@ namespace API.Migrations .IsRequired() .HasColumnType("text"); - b.Property("ChapterIds") - .IsRequired() - .HasColumnType("text"); - b.Property("ChapterNumber") .HasColumnType("real"); @@ -59,7 +55,6 @@ namespace API.Migrations b.Property("ParentMangaId") .IsRequired() - .HasMaxLength(64) .HasColumnType("character varying(64)"); b.Property("Title") @@ -85,7 +80,7 @@ namespace API.Migrations .HasMaxLength(64) .HasColumnType("character varying(64)"); - b.PrimitiveCollection("DependsOnJobIds") + b.PrimitiveCollection("DependsOnJobsIds") .HasMaxLength(64) .HasColumnType("text[]"); @@ -98,9 +93,6 @@ namespace API.Migrations b.Property("LastExecution") .HasColumnType("timestamp with time zone"); - b.Property("NextExecution") - .HasColumnType("timestamp with time zone"); - b.Property("ParentJobId") .HasMaxLength(64) .HasColumnType("character varying(64)"); @@ -115,6 +107,8 @@ namespace API.Migrations b.HasIndex("JobId1"); + b.HasIndex("ParentJobId"); + b.ToTable("Jobs"); b.HasDiscriminator("JobType"); @@ -154,9 +148,6 @@ namespace API.Migrations .HasMaxLength(64) .HasColumnType("character varying(64)"); - b.Property("LinkIds") - .HasColumnType("text"); - b.Property("LinkProvider") .IsRequired() .HasColumnType("text"); @@ -166,7 +157,6 @@ namespace API.Migrations .HasColumnType("text"); b.Property("MangaId") - .IsRequired() .HasColumnType("character varying(64)"); b.HasKey("LinkId"); @@ -182,14 +172,6 @@ namespace API.Migrations .HasMaxLength(64) .HasColumnType("character varying(64)"); - b.PrimitiveCollection("AltTitleIds") - .IsRequired() - .HasColumnType("text[]"); - - b.PrimitiveCollection("AuthorIds") - .IsRequired() - .HasColumnType("text[]"); - b.Property("ConnectorId") .IsRequired() .HasMaxLength(64) @@ -213,24 +195,10 @@ namespace API.Migrations b.Property("IgnoreChapterBefore") .HasColumnType("real"); - b.Property("LatestChapterAvailableId") - .HasColumnType("character varying(64)"); - - b.Property("LatestChapterDownloadedId") - .HasColumnType("character varying(64)"); - - b.PrimitiveCollection("LinkIds") - .IsRequired() - .HasColumnType("text[]"); - - b.Property("MangaConnectorName") + b.Property("MangaConnectorId") .IsRequired() .HasColumnType("character varying(32)"); - b.Property("MangaIds") - .IsRequired() - .HasColumnType("text"); - b.Property("Name") .IsRequired() .HasColumnType("text"); @@ -241,26 +209,16 @@ namespace API.Migrations b.Property("ReleaseStatus") .HasColumnType("smallint"); - b.PrimitiveCollection("TagIds") - .IsRequired() - .HasColumnType("text[]"); - b.Property("WebsiteUrl") .IsRequired() .HasColumnType("text"); - b.Property("year") + b.Property("Year") .HasColumnType("bigint"); b.HasKey("MangaId"); - b.HasIndex("LatestChapterAvailableId") - .IsUnique(); - - b.HasIndex("LatestChapterDownloadedId") - .IsUnique(); - - b.HasIndex("MangaConnectorName"); + b.HasIndex("MangaConnectorId"); b.ToTable("Manga"); }); @@ -271,16 +229,12 @@ namespace API.Migrations .HasMaxLength(64) .HasColumnType("character varying(64)"); - b.Property("AltTitleIds") - .HasColumnType("text"); - b.Property("Language") .IsRequired() .HasMaxLength(8) .HasColumnType("character varying(8)"); b.Property("MangaId") - .IsRequired() .HasColumnType("character varying(64)"); b.Property("Title") @@ -333,6 +287,9 @@ namespace API.Migrations .HasMaxLength(64) .HasColumnType("character varying(64)"); + b.Property("Date") + .HasColumnType("timestamp with time zone"); + b.Property("Message") .IsRequired() .HasColumnType("text"); @@ -367,49 +324,34 @@ namespace API.Migrations b.UseTphMappingStrategy(); }); - modelBuilder.Entity("MangaAuthor", b => + modelBuilder.Entity("AuthorManga", b => { + b.Property("AuthorsAuthorId") + .HasColumnType("character varying(64)"); + b.Property("MangaId") .HasColumnType("character varying(64)"); - b.Property("AuthorId") - .HasColumnType("character varying(64)"); + b.HasKey("AuthorsAuthorId", "MangaId"); - b.Property("AuthorIds") - .HasColumnType("text"); + b.HasIndex("MangaId"); - b.Property("MangaIds") - .HasColumnType("text"); - - b.HasKey("MangaId", "AuthorId"); - - b.HasIndex("AuthorId"); - - b.ToTable("MangaAuthor"); + b.ToTable("AuthorManga"); }); - modelBuilder.Entity("MangaTag", b => + modelBuilder.Entity("MangaMangaTag", b => { b.Property("MangaId") .HasColumnType("character varying(64)"); - b.Property("Tag") + b.Property("TagsTag") .HasColumnType("text"); - b.Property("MangaIds") - .IsRequired() - .HasColumnType("text"); + b.HasKey("MangaId", "TagsTag"); - b.Property("TagIds") - .HasColumnType("text"); + b.HasIndex("TagsTag"); - b.HasKey("MangaId", "Tag"); - - b.HasIndex("MangaIds"); - - b.HasIndex("Tag"); - - b.ToTable("MangaTag"); + b.ToTable("MangaMangaTag"); }); modelBuilder.Entity("API.Schema.Jobs.DownloadNewChaptersJob", b => @@ -528,7 +470,7 @@ namespace API.Migrations { b.HasBaseType("API.Schema.MangaConnectors.MangaConnector"); - b.HasDiscriminator().HasValue("MangaLife"); + b.HasDiscriminator().HasValue("Manga4Life"); }); modelBuilder.Entity("API.Schema.MangaConnectors.Manganato", b => @@ -620,7 +562,7 @@ namespace API.Migrations modelBuilder.Entity("API.Schema.Chapter", b => { b.HasOne("API.Schema.Manga", "ParentManga") - .WithMany("Chapters") + .WithMany() .HasForeignKey("ParentMangaId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); @@ -633,58 +575,44 @@ namespace API.Migrations b.HasOne("API.Schema.Jobs.Job", null) .WithMany("DependsOnJobs") .HasForeignKey("JobId1"); + + b.HasOne("API.Schema.Jobs.Job", "ParentJob") + .WithMany() + .HasForeignKey("ParentJobId"); + + b.Navigation("ParentJob"); }); modelBuilder.Entity("API.Schema.Link", b => { - b.HasOne("API.Schema.Manga", "Manga") + b.HasOne("API.Schema.Manga", null) .WithMany("Links") - .HasForeignKey("MangaId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Manga"); + .HasForeignKey("MangaId"); }); modelBuilder.Entity("API.Schema.Manga", b => { - b.HasOne("API.Schema.Chapter", "LatestChapterAvailable") - .WithOne() - .HasForeignKey("API.Schema.Manga", "LatestChapterAvailableId"); - - b.HasOne("API.Schema.Chapter", "LatestChapterDownloaded") - .WithOne() - .HasForeignKey("API.Schema.Manga", "LatestChapterDownloadedId"); - b.HasOne("API.Schema.MangaConnectors.MangaConnector", "MangaConnector") - .WithMany("Mangas") - .HasForeignKey("MangaConnectorName") + .WithMany() + .HasForeignKey("MangaConnectorId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.Navigation("LatestChapterAvailable"); - - b.Navigation("LatestChapterDownloaded"); - b.Navigation("MangaConnector"); }); modelBuilder.Entity("API.Schema.MangaAltTitle", b => { - b.HasOne("API.Schema.Manga", "Manga") + b.HasOne("API.Schema.Manga", null) .WithMany("AltTitles") - .HasForeignKey("MangaId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Manga"); + .HasForeignKey("MangaId"); }); - modelBuilder.Entity("MangaAuthor", b => + modelBuilder.Entity("AuthorManga", b => { b.HasOne("API.Schema.Author", null) .WithMany() - .HasForeignKey("AuthorId") + .HasForeignKey("AuthorsAuthorId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); @@ -695,7 +623,7 @@ namespace API.Migrations .IsRequired(); }); - modelBuilder.Entity("MangaTag", b => + modelBuilder.Entity("MangaMangaTag", b => { b.HasOne("API.Schema.Manga", null) .WithMany() @@ -705,13 +633,7 @@ namespace API.Migrations b.HasOne("API.Schema.MangaTag", null) .WithMany() - .HasForeignKey("MangaIds") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Schema.MangaTag", null) - .WithMany() - .HasForeignKey("Tag") + .HasForeignKey("TagsTag") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); }); @@ -758,15 +680,8 @@ namespace API.Migrations { b.Navigation("AltTitles"); - b.Navigation("Chapters"); - b.Navigation("Links"); }); - - modelBuilder.Entity("API.Schema.MangaConnectors.MangaConnector", b => - { - b.Navigation("Mangas"); - }); #pragma warning restore 612, 618 } } diff --git a/API/Schema/Jobs/DownloadMangaCoverJob.cs b/API/Schema/Jobs/DownloadMangaCoverJob.cs new file mode 100644 index 0000000..4de3a6b --- /dev/null +++ b/API/Schema/Jobs/DownloadMangaCoverJob.cs @@ -0,0 +1,136 @@ +using System.ComponentModel.DataAnnotations; +using System.IO.Compression; +using System.Runtime.InteropServices; +using API.MangaDownloadClients; +using API.Schema.MangaConnectors; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Formats.Jpeg; +using SixLabors.ImageSharp.Processing; +using SixLabors.ImageSharp.Processing.Processors.Binarization; +using static System.IO.UnixFileMode; + +namespace API.Schema.Jobs; + +public class DownloadMangaCoverJob(string chapterId, string? parentJobId = null, ICollection? dependsOnJobsIds = null) + : Job(TokenGen.CreateToken(typeof(DownloadMangaCoverJob), 64), JobType.DownloadMangaCoverJob, 0, parentJobId, dependsOnJobsIds) +{ + [MaxLength(64)] + public string ChapterId { get; init; } = chapterId; + public Chapter? Chapter { get; init; } + + protected override IEnumerable RunInternal(PgsqlContext context) + { + MangaConnector connector = Chapter.ParentManga?.MangaConnector ?? context.MangaConnectors.Find(context.Manga.Find(Chapter.ParentMangaId)?.MangaId)!; + DownloadChapterImages(Chapter, connector); + return []; + } + + private bool DownloadChapterImages(Chapter chapter, MangaConnector connector) + { + string[] imageUrls = connector.GetChapterImageUrls(Chapter); + string saveArchiveFilePath = chapter.GetArchiveFilePath(); + + //Check if Publication Directory already exists + string directoryPath = Path.GetDirectoryName(saveArchiveFilePath)!; + if (!Directory.Exists(directoryPath)) + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + Directory.CreateDirectory(directoryPath, + UserRead | UserWrite | UserExecute | GroupRead | GroupWrite | GroupExecute ); + else + Directory.CreateDirectory(directoryPath); + + if (File.Exists(saveArchiveFilePath)) //Don't download twice. Redownload + File.Delete(saveArchiveFilePath); + + //Create a temporary folder to store images + string tempFolder = Directory.CreateTempSubdirectory("trangatemp").FullName; + + int chapterNum = 0; + //Download all Images to temporary Folder + if (imageUrls.Length == 0) + { + Directory.Delete(tempFolder, true); + return false; + } + + foreach (string imageUrl in imageUrls) + { + string extension = imageUrl.Split('.')[^1].Split('?')[0]; + string imagePath = Path.Join(tempFolder, $"{chapterNum++}.{extension}"); + bool status = DownloadImage(imageUrl, imagePath); + if (status is false) + return false; + } + + CopyCoverFromCacheToDownloadLocation(); + + File.WriteAllText(Path.Join(tempFolder, "ComicInfo.xml"), chapter.GetComicInfoXmlString()); + + //ZIP-it and ship-it + ZipFile.CreateFromDirectory(tempFolder, saveArchiveFilePath); + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + File.SetUnixFileMode(saveArchiveFilePath, UserRead | UserWrite | UserExecute | GroupRead | GroupWrite | GroupExecute | OtherRead | OtherExecute); + Directory.Delete(tempFolder, true); //Cleanup + + return true; + } + + private void ProcessImage(string imagePath) + { + if (!TrangaSettings.bwImages && TrangaSettings.compression == 100) + return; + DateTime start = DateTime.Now; + using Image image = Image.Load(imagePath); + File.Delete(imagePath); + if(TrangaSettings.bwImages) + image.Mutate(i => i.ApplyProcessor(new AdaptiveThresholdProcessor())); + image.SaveAsJpeg(imagePath, new JpegEncoder() + { + Quality = TrangaSettings.compression + }); + } + + private void CopyCoverFromCacheToDownloadLocation(int? retries = 1) + { + //Check if Publication already has a Folder and cover + string publicationFolder = Chapter.ParentManga.CreatePublicationFolder(); + DirectoryInfo dirInfo = new (publicationFolder); + if (dirInfo.EnumerateFiles().Any(info => info.Name.Contains("cover", StringComparison.InvariantCultureIgnoreCase))) + { + return; + } + + string? fileInCache = Chapter.ParentManga.CoverFileNameInCache; + if (fileInCache is null || !File.Exists(fileInCache)) + { + if (retries > 0 && Chapter.ParentManga.CoverUrl is not null) + { + Chapter.ParentManga.SaveCoverImageToCache(); + CopyCoverFromCacheToDownloadLocation(--retries); + } + + return; + } + string newFilePath = Path.Join(publicationFolder, $"cover.{Path.GetFileName(fileInCache).Split('.')[^1]}" ); + File.Copy(fileInCache, newFilePath, true); + if(RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + File.SetUnixFileMode(newFilePath, GroupRead | GroupWrite | UserRead | UserWrite); + } + + private bool DownloadImage(string imageUrl, string savePath) + { + HttpDownloadClient downloadClient = new(); + RequestResult requestResult = downloadClient.MakeRequest(imageUrl, RequestType.MangaImage); + + if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300) + return false; + if (requestResult.result == Stream.Null) + return false; + + FileStream fs = new (savePath, FileMode.Create); + requestResult.result.CopyTo(fs); + fs.Close(); + ProcessImage(savePath); + return true; + } +} \ No newline at end of file diff --git a/API/Schema/Jobs/DownloadNewChaptersJob.cs b/API/Schema/Jobs/DownloadNewChaptersJob.cs index 2690e46..241a051 100644 --- a/API/Schema/Jobs/DownloadNewChaptersJob.cs +++ b/API/Schema/Jobs/DownloadNewChaptersJob.cs @@ -1,22 +1,22 @@ using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; using API.Schema.MangaConnectors; -using Newtonsoft.Json; namespace API.Schema.Jobs; -public class DownloadNewChaptersJob(ulong recurrenceMs, string mangaId, string? parentJobId = null, string[]? dependsOnJobIds = null) - : Job(TokenGen.CreateToken(typeof(DownloadNewChaptersJob), 64), JobType.DownloadNewChaptersJob, recurrenceMs, parentJobId, dependsOnJobIds) +public class DownloadNewChaptersJob(ulong recurrenceMs, string mangaId, string? parentJobId = null, ICollection? dependsOnJobsIds = null) + : Job(TokenGen.CreateToken(typeof(DownloadNewChaptersJob), 64), JobType.DownloadNewChaptersJob, recurrenceMs, parentJobId, dependsOnJobsIds) { [MaxLength(64)] public string MangaId { get; init; } = mangaId; - public virtual Manga Manga { get; init; } + public Manga? Manga { get; init; } protected override IEnumerable RunInternal(PgsqlContext context) { - MangaConnector connector = Manga.MangaConnector; - Chapter[] newChapters = connector.GetNewChapters(Manga); + Manga m = Manga ?? context.Manga.Find(MangaId)!; + MangaConnector connector = m.MangaConnector ?? context.MangaConnectors.Find(m.MangaConnectorId)!; + Chapter[] newChapters = connector.GetNewChapters(m); context.Chapters.AddRangeAsync(newChapters).Wait(); + context.SaveChangesAsync().Wait(); return newChapters.Select(chapter => new DownloadSingleChapterJob(chapter.ChapterId, this.JobId)); } } \ No newline at end of file diff --git a/API/Schema/Jobs/DownloadSingleChapterJob.cs b/API/Schema/Jobs/DownloadSingleChapterJob.cs index 0868c9b..cecc901 100644 --- a/API/Schema/Jobs/DownloadSingleChapterJob.cs +++ b/API/Schema/Jobs/DownloadSingleChapterJob.cs @@ -11,24 +11,25 @@ using static System.IO.UnixFileMode; namespace API.Schema.Jobs; -public class DownloadSingleChapterJob(string chapterId, string? parentJobId = null, string[]? dependsOnJobIds = null) - : Job(TokenGen.CreateToken(typeof(DownloadSingleChapterJob), 64), JobType.DownloadSingleChapterJob, 0, parentJobId, dependsOnJobIds) +public class DownloadSingleChapterJob(string chapterId, string? parentJobId = null, ICollection? dependsOnJobsIds = null) + : Job(TokenGen.CreateToken(typeof(DownloadSingleChapterJob), 64), JobType.DownloadSingleChapterJob, 0, parentJobId, dependsOnJobsIds) { [MaxLength(64)] public string ChapterId { get; init; } = chapterId; - public virtual Chapter Chapter { get; init; } + public Chapter? Chapter { get; init; } protected override IEnumerable RunInternal(PgsqlContext context) { - MangaConnector connector = Chapter.ParentManga.MangaConnector; - DownloadChapterImages(Chapter); + Chapter c = Chapter ?? context.Chapters.Find(ChapterId)!; + Manga m = c.ParentManga ?? context.Manga.Find(c.ParentMangaId)!; + MangaConnector connector = m.MangaConnector ?? context.MangaConnectors.Find(m.MangaConnectorId)!; + DownloadChapterImages(c, connector, m); return []; } - private bool DownloadChapterImages(Chapter chapter) + private bool DownloadChapterImages(Chapter chapter, MangaConnector connector, Manga manga) { - MangaConnector connector = Chapter.ParentManga.MangaConnector; - string[] imageUrls = connector.GetChapterImageUrls(Chapter); + string[] imageUrls = connector.GetChapterImageUrls(chapter); string saveArchiveFilePath = chapter.GetArchiveFilePath(); //Check if Publication Directory already exists @@ -63,7 +64,7 @@ public class DownloadSingleChapterJob(string chapterId, string? parentJobId = nu return false; } - CopyCoverFromCacheToDownloadLocation(); + CopyCoverFromCacheToDownloadLocation(manga); File.WriteAllText(Path.Join(tempFolder, "ComicInfo.xml"), chapter.GetComicInfoXmlString()); @@ -91,23 +92,23 @@ public class DownloadSingleChapterJob(string chapterId, string? parentJobId = nu }); } - private void CopyCoverFromCacheToDownloadLocation(int? retries = 1) + private void CopyCoverFromCacheToDownloadLocation(Manga manga, int? retries = 1) { //Check if Publication already has a Folder and cover - string publicationFolder = Chapter.ParentManga.CreatePublicationFolder(); + string publicationFolder = manga.CreatePublicationFolder(); DirectoryInfo dirInfo = new (publicationFolder); if (dirInfo.EnumerateFiles().Any(info => info.Name.Contains("cover", StringComparison.InvariantCultureIgnoreCase))) { return; } - string? fileInCache = Chapter.ParentManga.CoverFileNameInCache; + string? fileInCache = manga.CoverFileNameInCache; if (fileInCache is null || !File.Exists(fileInCache)) { - if (retries > 0 && Chapter.ParentManga.CoverUrl is not null) + if (retries > 0) { - Chapter.ParentManga.SaveCoverImageToCache(); - CopyCoverFromCacheToDownloadLocation(--retries); + manga.SaveCoverImageToCache(); + CopyCoverFromCacheToDownloadLocation(manga, --retries); } return; diff --git a/API/Schema/Jobs/Job.cs b/API/Schema/Jobs/Job.cs index 9861800..16553d2 100644 --- a/API/Schema/Jobs/Job.cs +++ b/API/Schema/Jobs/Job.cs @@ -12,28 +12,35 @@ public abstract class Job public string JobId { get; init; } [MaxLength(64)] - public string? ParentJobId { get; internal set; } - internal virtual Job ParentJob { get; } + public string? ParentJobId { get; init; } + public Job? ParentJob { get; init; } [MaxLength(64)] - public string[]? DependsOnJobIds { get; init; } - public virtual Job[] DependsOnJobs { get; init; } + public ICollection? DependsOnJobsIds { get; init; } + public ICollection? DependsOnJobs { get; init; } public JobType JobType { get; init; } public ulong RecurrenceMs { get; set; } public DateTime LastExecution { get; internal set; } = DateTime.UnixEpoch; - public DateTime NextExecution { get; internal set; } + + [NotMapped] + public DateTime NextExecution => LastExecution.AddMilliseconds(RecurrenceMs); public JobState state { get; internal set; } = JobState.Waiting; - public Job(string jobId, JobType jobType, ulong recurrenceMs, string? parentJobId = null, - string[]? dependsOnJobIds = null) + public Job(string jobId, JobType jobType, ulong recurrenceMs, Job? parentJob = null, ICollection? dependsOnJobs = null) + : this(jobId, jobType, recurrenceMs, parentJob?.JobId, dependsOnJobs?.Select(j => j.JobId).ToList()) + { + this.ParentJob = parentJob; + this.DependsOnJobs = dependsOnJobs; + } + + public Job(string jobId, JobType jobType, ulong recurrenceMs, string? parentJobId = null, ICollection? dependsOnJobsIds = null) { JobId = jobId; ParentJobId = parentJobId; - DependsOnJobIds = dependsOnJobIds; + DependsOnJobsIds = dependsOnJobsIds; JobType = jobType; RecurrenceMs = recurrenceMs; - NextExecution = LastExecution.AddMilliseconds(RecurrenceMs); } public IEnumerable Run(PgsqlContext context) diff --git a/API/Schema/Jobs/JobType.cs b/API/Schema/Jobs/JobType.cs index ab32639..bf206f3 100644 --- a/API/Schema/Jobs/JobType.cs +++ b/API/Schema/Jobs/JobType.cs @@ -6,5 +6,6 @@ public enum JobType : byte DownloadSingleChapterJob = 0, DownloadNewChaptersJob = 1, UpdateMetaDataJob = 2, - MoveFileOrFolderJob = 3 + MoveFileOrFolderJob = 3, + DownloadMangaCoverJob = 4 } \ No newline at end of file diff --git a/API/Schema/Jobs/MoveFileOrFolderJob.cs b/API/Schema/Jobs/MoveFileOrFolderJob.cs index 816613b..a29d7b1 100644 --- a/API/Schema/Jobs/MoveFileOrFolderJob.cs +++ b/API/Schema/Jobs/MoveFileOrFolderJob.cs @@ -1,7 +1,7 @@ namespace API.Schema.Jobs; -public class MoveFileOrFolderJob(string fromLocation, string toLocation, string? parentJobId = null, string[]? dependsOnJobIds = null) - : Job(TokenGen.CreateToken(typeof(MoveFileOrFolderJob), 64), JobType.MoveFileOrFolderJob, 0, parentJobId, dependsOnJobIds) +public class MoveFileOrFolderJob(string fromLocation, string toLocation, string? parentJobId = null, ICollection? dependsOnJobsIds = null) + : Job(TokenGen.CreateToken(typeof(MoveFileOrFolderJob), 64), JobType.MoveFileOrFolderJob, 0, parentJobId, dependsOnJobsIds) { public string FromLocation { get; init; } = fromLocation; public string ToLocation { get; init; } = toLocation; diff --git a/API/Schema/Jobs/UpdateMetadataJob.cs b/API/Schema/Jobs/UpdateMetadataJob.cs index c181c82..c8d76cd 100644 --- a/API/Schema/Jobs/UpdateMetadataJob.cs +++ b/API/Schema/Jobs/UpdateMetadataJob.cs @@ -2,8 +2,8 @@ namespace API.Schema.Jobs; -public class UpdateMetadataJob(ulong recurrenceMs, string mangaId, string? parentJobId = null, string[]? dependsOnJobIds = null) - : Job(TokenGen.CreateToken(typeof(UpdateMetadataJob), 64), JobType.UpdateMetaDataJob, recurrenceMs, parentJobId, dependsOnJobIds) +public class UpdateMetadataJob(ulong recurrenceMs, string mangaId, string? parentJobId = null, ICollection? dependsOnJobsIds = null) + : Job(TokenGen.CreateToken(typeof(UpdateMetadataJob), 64), JobType.UpdateMetaDataJob, recurrenceMs, parentJobId, dependsOnJobsIds) { [MaxLength(64)] public string MangaId { get; init; } = mangaId; diff --git a/API/Schema/Manga.cs b/API/Schema/Manga.cs index 9cab486..fe510a8 100644 --- a/API/Schema/Manga.cs +++ b/API/Schema/Manga.cs @@ -11,58 +11,64 @@ using static System.IO.UnixFileMode; namespace API.Schema; [PrimaryKey("MangaId")] -public class Manga( - string connectorId, - string name, - string description, - string websiteUrl, - string coverUrl, - string? coverFileNameInCache, - uint year, - string? originalLanguage, - MangaReleaseStatus releaseStatus, - float ignoreChapterBefore, - MangaConnector mangaConnector, - ICollection authors, - ICollection tags, - ICollection links, - ICollection altTitles) +public class Manga { [MaxLength(64)] public string MangaId { get; init; } = TokenGen.CreateToken(typeof(Manga), 64); [MaxLength(64)] - public string ConnectorId { get; init; } = connectorId; + public string ConnectorId { get; init; } - public string Name { get; internal set; } = name; - public string Description { get; internal set; } = description; - public string WebsiteUrl { get; internal set; } = websiteUrl; - public string CoverUrl { get; internal set; } = coverUrl; - public string? CoverFileNameInCache { get; internal set; } = coverFileNameInCache; - public uint year { get; internal set; } = year; - public string? OriginalLanguage { get; internal set; } = originalLanguage; - public MangaReleaseStatus ReleaseStatus { get; internal set; } = releaseStatus; - public string FolderName { get; private set; } = BuildFolderName(name); - public float IgnoreChapterBefore { get; internal set; } = ignoreChapterBefore; + public string Name { get; internal set; } + public string Description { get; internal set; } + public string WebsiteUrl { get; internal set; } + public string CoverUrl { get; internal set; } + public string? CoverFileNameInCache { get; internal set; } + public uint Year { get; internal set; } + public string? OriginalLanguage { get; internal set; } + public MangaReleaseStatus ReleaseStatus { get; internal set; } + public string FolderName { get; private set; } + public float IgnoreChapterBefore { get; internal set; } - [ForeignKey("MangaConnectorId")] - public MangaConnector MangaConnector { get; private set; } = mangaConnector; + public string MangaConnectorId { get; private set; } + public MangaConnector? MangaConnector { get; private set; } - public ICollection Authors { get; internal set; } = authors; + public ICollection? Authors { get; internal set; } - public ICollection Tags { get; internal set; } = tags; + public ICollection? Tags { get; internal set; } - public ICollection Links { get; internal set; } = links; + public ICollection? Links { get; internal set; } - public ICollection AltTitles { get; internal set; } = altTitles; + public ICollection? AltTitles { get; internal set; } public Manga(string connectorId, string name, string description, string websiteUrl, string coverUrl, - string? coverFileNameInCache, - uint year, string? originalLanguage, MangaReleaseStatus releaseStatus, float ignoreChapterBefore) + string? coverFileNameInCache, uint year, string? originalLanguage, MangaReleaseStatus releaseStatus, + float ignoreChapterBefore, MangaConnector mangaConnector, ICollection authors, + ICollection tags, ICollection links, ICollection altTitles) : this(connectorId, name, description, websiteUrl, coverUrl, coverFileNameInCache, year, originalLanguage, - releaseStatus, - ignoreChapterBefore, null, null, null, null, null) + releaseStatus, ignoreChapterBefore, mangaConnector.Name) { - + this.Authors = authors; + this.Tags = tags; + this.Links = links; + this.AltTitles = altTitles; + } + + public Manga(string connectorId, string name, string description, string websiteUrl, string coverUrl, + string? coverFileNameInCache, uint year, string? originalLanguage, MangaReleaseStatus releaseStatus, + float ignoreChapterBefore, string mangaConnectorId) + { + ConnectorId = connectorId; + Name = name; + Description = description; + WebsiteUrl = websiteUrl; + CoverUrl = coverUrl; + CoverFileNameInCache = coverFileNameInCache; + Year = year; + OriginalLanguage = originalLanguage; + ReleaseStatus = releaseStatus; + IgnoreChapterBefore = ignoreChapterBefore; + MangaConnectorId = mangaConnectorId; + FolderName = BuildFolderName(name); } public MoveFileOrFolderJob UpdateFolderName(string downloadLocation, string newName) @@ -75,7 +81,7 @@ public class Manga( internal void UpdateWithInfo(Manga other) { this.Name = other.Name; - this.year = other.year; + this.Year = other.Year; this.Description = other.Description; this.CoverUrl = other.CoverUrl; this.OriginalLanguage = other.OriginalLanguage; diff --git a/API/Schema/PgsqlContext.cs b/API/Schema/PgsqlContext.cs index 9697152..8c66a5a 100644 --- a/API/Schema/PgsqlContext.cs +++ b/API/Schema/PgsqlContext.cs @@ -51,6 +51,12 @@ public class PgsqlContext(DbContextOptions options) : DbContext(op .HasValue(JobType.DownloadNewChaptersJob) .HasValue(JobType.DownloadSingleChapterJob) .HasValue(JobType.UpdateMetaDataJob); + modelBuilder.Entity() + .HasOne(j => j.ParentJob) + .WithMany() + .HasForeignKey(j => j.ParentJobId); + modelBuilder.Entity() + .HasMany(j => j.DependsOnJobs); modelBuilder.Entity() .Navigation(dncj => dncj.Manga) .AutoInclude(); @@ -62,7 +68,9 @@ public class PgsqlContext(DbContextOptions options) : DbContext(op .AutoInclude(); modelBuilder.Entity() - .HasOne(m => m.MangaConnector); + .HasOne(m => m.MangaConnector) + .WithMany() + .HasForeignKey(m => m.MangaConnectorId); modelBuilder.Entity() .Navigation(m => m.MangaConnector) .AutoInclude(); @@ -92,7 +100,8 @@ public class PgsqlContext(DbContextOptions options) : DbContext(op .AutoInclude(); modelBuilder.Entity() .HasOne(c => c.ParentManga) - .WithMany(); + .WithMany() + .HasForeignKey(c => c.ParentMangaId); modelBuilder.Entity() .Navigation(c => c.ParentManga) .AutoInclude(); diff --git a/API/Tranga.cs b/API/Tranga.cs index 8d6259c..e6d89a3 100644 --- a/API/Tranga.cs +++ b/API/Tranga.cs @@ -67,7 +67,7 @@ public static class Tranga { List completedJobs = context.Jobs.Where(j => j.state == JobState.Completed).ToList(); foreach (Job job in completedJobs) - if(job.RecurrenceMs < 1) + if(job.RecurrenceMs <= 0) context.Jobs.Remove(job); else { @@ -76,7 +76,7 @@ public static class Tranga context.Jobs.Update(job); } - List runJobs = context.Jobs.Where(j => j.state <= JobState.Running && j.NextExecution < DateTime.UtcNow).ToList(); + List runJobs = context.Jobs.Where(j => j.state <= JobState.Running).ToList().Where(j => j.NextExecution < DateTime.UtcNow).ToList(); foreach (Job job in runJobs) { Thread t = new (() =>