Compare commits

...

22 Commits

Author SHA1 Message Date
3283dd7290 DownloadAvailableChaptersJob.cs only create DownloadSingleChapterJobs for Chapters that have not been downloaded 2025-05-16 22:00:17 +02:00
937c5cb7a7 Create a UpdateChaptersDownloadedJob with creation of DownloadAvailableChaptersJob 2025-05-16 21:59:53 +02:00
225b7f02ad Lazy Load Jobs.DependsOnJobs, Manga.Chapters 2025-05-16 21:53:59 +02:00
6258e07f20 Remove unnecessary Attachments 2025-05-16 21:36:24 +02:00
622198a09e Changes to Job.cs:
- Nest try-catch for DBUpdateException and other Exceptions
- More Logging for Jobs
2025-05-16 21:32:42 +02:00
49b382fe1f Logging for Job-Cycle 2025-05-16 21:27:02 +02:00
5a6dc5a5b2 Logging for Jobs-Filtering 2025-05-16 21:25:08 +02:00
4bc70eca68 Distinct Jobs 2025-05-16 21:23:47 +02:00
63fee081e6 Catch all Exceptions in Job 2025-05-16 21:18:46 +02:00
e45b72dcf9 Include Spaces in Directory-Path 2025-05-16 21:18:37 +02:00
021ad5e804 Include FullArchivePath in Chapter-Response 2025-05-16 21:18:19 +02:00
8e0c964883 Update Jobs on each cycle (since it is super fast now) 2025-05-16 21:09:43 +02:00
d6e945741a Fix Manga-Chapter loading 2025-05-16 21:09:32 +02:00
3a3306240f Use LazyLoading 2025-05-16 21:05:55 +02:00
110dd36166 Do not update context.Jobs on every cycle 2025-05-16 20:16:11 +02:00
065cac62af Move TRANGA message 2025-05-16 20:16:02 +02:00
563afa1e6f Split UpdateFilesDownloadedJob.cs to UpdateChaptersDownloadedJob.cs and split into UpdateSingleChapterDownloadedJob.cs 2025-05-16 20:13:39 +02:00
be2adff57d DownloadSingleChapterJob.cs load Jobs 2025-05-16 20:01:50 +02:00
adc7ee606e RetrieveChaptersJob.cs do not use context to access Chapters 2025-05-16 20:00:53 +02:00
a764f381c9 Logging for DBContexts 2025-05-16 19:46:14 +02:00
590ccdd09a Use GlobalConnector for Url Search requests 2025-05-16 19:27:34 +02:00
0f0a49f74f Change Search with name to GET Request 2025-05-16 19:27:22 +02:00
30 changed files with 1177 additions and 169 deletions

View File

@ -19,6 +19,7 @@
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Proxies" Version="9.0.5" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="Npgsql" Version="9.0.3" /> <PackageReference Include="Npgsql" Version="9.0.3" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4" /> <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4" />

View File

@ -134,8 +134,10 @@ public class JobController(PgsqlContext context, ILog Log) : Controller
} }
} }
Job retrieveChapters = new RetrieveChaptersJob(m, record.language, record.recurrenceTimeMs); Job retrieveChapters = new RetrieveChaptersJob(m, record.language, record.recurrenceTimeMs);
Job downloadChapters = new DownloadAvailableChaptersJob(m, record.recurrenceTimeMs, dependsOnJobs: [retrieveChapters]); Job updateFilesDownloaded =
return AddJobs([retrieveChapters, downloadChapters]); new UpdateChaptersDownloadedJob(m, record.recurrenceTimeMs, dependsOnJobs: [retrieveChapters]);
Job downloadChapters = new DownloadAvailableChaptersJob(m, record.recurrenceTimeMs, dependsOnJobs: [retrieveChapters, updateFilesDownloaded]);
return AddJobs([retrieveChapters, downloadChapters, updateFilesDownloaded]);
} }
/// <summary> /// <summary>
@ -158,7 +160,7 @@ public class JobController(PgsqlContext context, ILog Log) : Controller
} }
/// <summary> /// <summary>
/// Create a new UpdateFilesDownloadedJob /// Create a new UpdateChaptersDownloadedJob
/// </summary> /// </summary>
/// <param name="MangaId">ID of the Manga</param> /// <param name="MangaId">ID of the Manga</param>
/// <response code="201">Job-IDs</response> /// <response code="201">Job-IDs</response>
@ -172,7 +174,7 @@ public class JobController(PgsqlContext context, ILog Log) : Controller
{ {
if(context.Mangas.Find(MangaId) is not { } m) if(context.Mangas.Find(MangaId) is not { } m)
return NotFound(); return NotFound();
Job job = new UpdateFilesDownloadedJob(m, 0); Job job = new UpdateChaptersDownloadedJob(m, 0);
return AddJobs([job]); return AddJobs([job]);
} }
@ -186,7 +188,7 @@ public class JobController(PgsqlContext context, ILog Log) : Controller
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")] [ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public IActionResult CreateUpdateAllFilesDownloadedJob() public IActionResult CreateUpdateAllFilesDownloadedJob()
{ {
List<UpdateFilesDownloadedJob> jobs = context.Mangas.Select(m => new UpdateFilesDownloadedJob(m, 0, null, null)).ToList(); List<UpdateChaptersDownloadedJob> jobs = context.Mangas.Select(m => new UpdateChaptersDownloadedJob(m, 0, null, null)).ToList();
try try
{ {
context.Jobs.AddRange(jobs); context.Jobs.AddRange(jobs);

View File

@ -343,7 +343,7 @@ public class MangaController(PgsqlContext context, ILog Log) : Controller
return NotFound(); return NotFound();
MoveMangaLibraryJob moveLibrary = new(manga, library); MoveMangaLibraryJob moveLibrary = new(manga, library);
UpdateFilesDownloadedJob updateDownloadedFiles = new(manga, 0, dependsOnJobs: [moveLibrary]); UpdateChaptersDownloadedJob updateDownloadedFiles = new(manga, 0, dependsOnJobs: [moveLibrary]);
try try
{ {

View File

@ -25,7 +25,7 @@ public class SearchController(PgsqlContext context, ILog Log) : Controller
/// <response code="404">MangaConnector with ID not found</response> /// <response code="404">MangaConnector with ID not found</response>
/// <response code="406">MangaConnector with ID is disabled</response> /// <response code="406">MangaConnector with ID is disabled</response>
/// <response code="500">Error during Database Operation</response> /// <response code="500">Error during Database Operation</response>
[HttpPost("{MangaConnectorName}/{Query}")] [HttpGet("{MangaConnectorName}/{Query}")]
[ProducesResponseType<Manga[]>(Status200OK, "application/json")] [ProducesResponseType<Manga[]>(Status200OK, "application/json")]
[ProducesResponseType(Status404NotFound)] [ProducesResponseType(Status404NotFound)]
[ProducesResponseType(Status406NotAcceptable)] [ProducesResponseType(Status406NotAcceptable)]
@ -67,30 +67,25 @@ public class SearchController(PgsqlContext context, ILog Log) : Controller
/// <response code="500">Error during Database Operation</response> /// <response code="500">Error during Database Operation</response>
[HttpPost("Url")] [HttpPost("Url")]
[ProducesResponseType<Manga>(Status200OK, "application/json")] [ProducesResponseType<Manga>(Status200OK, "application/json")]
[ProducesResponseType(Status300MultipleChoices)]
[ProducesResponseType(Status400BadRequest)] [ProducesResponseType(Status400BadRequest)]
[ProducesResponseType(Status404NotFound)]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")] [ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public IActionResult GetMangaFromUrl([FromBody]string url) public IActionResult GetMangaFromUrl([FromBody]string url)
{ {
List<MangaConnector> connectors = context.MangaConnectors.AsEnumerable().Where(c => c.UrlMatchesConnector(url)).ToList(); if (context.MangaConnectors.Find("Global") is not { } connector)
if (connectors.Count == 0) return StatusCode(Status500InternalServerError, "Could not find Global Connector.");
return NotFound();
else if (connectors.Count > 1)
return StatusCode(Status300MultipleChoices);
if(connectors.First().GetMangaFromUrl(url) is not { } manga) if(connector.GetMangaFromUrl(url) is not { } manga)
return BadRequest(); return BadRequest();
try try
{ {
if(AddMangaToContext(manga) is { } add) if(AddMangaToContext(manga) is { } add)
return Ok(add); return Ok(add);
return StatusCode(500); return StatusCode(Status500InternalServerError);
} }
catch (DbUpdateException e) catch (DbUpdateException e)
{ {
Log.Error(e); Log.Error(e);
return StatusCode(500, e.Message); return StatusCode(Status500InternalServerError, e.Message);
} }
} }

View File

@ -416,7 +416,7 @@ namespace API.Migrations.pgsql
b.HasDiscriminator().HasValue((byte)5); b.HasDiscriminator().HasValue((byte)5);
}); });
modelBuilder.Entity("API.Schema.Jobs.UpdateFilesDownloadedJob", b => modelBuilder.Entity("API.Schema.Jobs.UpdateChaptersDownloadedJob", b =>
{ {
b.HasBaseType("API.Schema.Jobs.Job"); b.HasBaseType("API.Schema.Jobs.Job");
@ -661,7 +661,7 @@ namespace API.Migrations.pgsql
b.Navigation("Manga"); b.Navigation("Manga");
}); });
modelBuilder.Entity("API.Schema.Jobs.UpdateFilesDownloadedJob", b => modelBuilder.Entity("API.Schema.Jobs.UpdateChaptersDownloadedJob", b =>
{ {
b.HasOne("API.Schema.Manga", "Manga") b.HasOne("API.Schema.Manga", "Manga")
.WithMany() .WithMany()

View File

@ -416,7 +416,7 @@ namespace API.Migrations.pgsql
b.HasDiscriminator().HasValue((byte)5); b.HasDiscriminator().HasValue((byte)5);
}); });
modelBuilder.Entity("API.Schema.Jobs.UpdateFilesDownloadedJob", b => modelBuilder.Entity("API.Schema.Jobs.UpdateChaptersDownloadedJob", b =>
{ {
b.HasBaseType("API.Schema.Jobs.Job"); b.HasBaseType("API.Schema.Jobs.Job");
@ -667,7 +667,7 @@ namespace API.Migrations.pgsql
b.Navigation("Manga"); b.Navigation("Manga");
}); });
modelBuilder.Entity("API.Schema.Jobs.UpdateFilesDownloadedJob", b => modelBuilder.Entity("API.Schema.Jobs.UpdateChaptersDownloadedJob", b =>
{ {
b.HasOne("API.Schema.Manga", "Manga") b.HasOne("API.Schema.Manga", "Manga")
.WithMany() .WithMany()

View File

@ -416,7 +416,7 @@ namespace API.Migrations.pgsql
b.HasDiscriminator().HasValue((byte)5); b.HasDiscriminator().HasValue((byte)5);
}); });
modelBuilder.Entity("API.Schema.Jobs.UpdateFilesDownloadedJob", b => modelBuilder.Entity("API.Schema.Jobs.UpdateChaptersDownloadedJob", b =>
{ {
b.HasBaseType("API.Schema.Jobs.Job"); b.HasBaseType("API.Schema.Jobs.Job");
@ -667,7 +667,7 @@ namespace API.Migrations.pgsql
b.Navigation("Manga"); b.Navigation("Manga");
}); });
modelBuilder.Entity("API.Schema.Jobs.UpdateFilesDownloadedJob", b => modelBuilder.Entity("API.Schema.Jobs.UpdateChaptersDownloadedJob", b =>
{ {
b.HasOne("API.Schema.Manga", "Manga") b.HasOne("API.Schema.Manga", "Manga")
.WithMany() .WithMany()

View File

@ -416,7 +416,7 @@ namespace API.Migrations.pgsql
b.HasDiscriminator().HasValue((byte)5); b.HasDiscriminator().HasValue((byte)5);
}); });
modelBuilder.Entity("API.Schema.Jobs.UpdateFilesDownloadedJob", b => modelBuilder.Entity("API.Schema.Jobs.UpdateChaptersDownloadedJob", b =>
{ {
b.HasBaseType("API.Schema.Jobs.Job"); b.HasBaseType("API.Schema.Jobs.Job");
@ -668,7 +668,7 @@ namespace API.Migrations.pgsql
b.Navigation("Manga"); b.Navigation("Manga");
}); });
modelBuilder.Entity("API.Schema.Jobs.UpdateFilesDownloadedJob", b => modelBuilder.Entity("API.Schema.Jobs.UpdateChaptersDownloadedJob", b =>
{ {
b.HasOne("API.Schema.Manga", "Manga") b.HasOne("API.Schema.Manga", "Manga")
.WithMany() .WithMany()

View File

@ -0,0 +1,720 @@
// <auto-generated />
using System;
using API.Schema.Contexts;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace API.Migrations.pgsql
{
[DbContext(typeof(PgsqlContext))]
[Migration("20250516180953_Split-UpdateChaptersDownloadedJob-Into-UpdateSingleChapterDownloadedJob")]
partial class SplitUpdateChaptersDownloadedJobIntoUpdateSingleChapterDownloadedJob
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "9.0.3")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("API.Schema.Author", b =>
{
b.Property<string>("AuthorId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("AuthorName")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("character varying(128)");
b.HasKey("AuthorId");
b.ToTable("Authors");
});
modelBuilder.Entity("API.Schema.Chapter", b =>
{
b.Property<string>("ChapterId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("ChapterNumber")
.IsRequired()
.HasMaxLength(10)
.HasColumnType("character varying(10)");
b.Property<bool>("Downloaded")
.HasColumnType("boolean");
b.Property<string>("FileName")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("ParentMangaId")
.IsRequired()
.HasColumnType("character varying(64)");
b.Property<string>("Title")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("Url")
.IsRequired()
.HasMaxLength(2048)
.HasColumnType("character varying(2048)");
b.Property<int?>("VolumeNumber")
.HasColumnType("integer");
b.HasKey("ChapterId");
b.HasIndex("ParentMangaId");
b.ToTable("Chapters");
});
modelBuilder.Entity("API.Schema.Jobs.Job", b =>
{
b.Property<string>("JobId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<bool>("Enabled")
.HasColumnType("boolean");
b.Property<byte>("JobType")
.HasColumnType("smallint");
b.Property<DateTime>("LastExecution")
.HasColumnType("timestamp with time zone");
b.Property<string>("ParentJobId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<decimal>("RecurrenceMs")
.HasColumnType("numeric(20,0)");
b.Property<byte>("state")
.HasColumnType("smallint");
b.HasKey("JobId");
b.HasIndex("ParentJobId");
b.ToTable("Jobs");
b.HasDiscriminator<byte>("JobType");
b.UseTphMappingStrategy();
});
modelBuilder.Entity("API.Schema.LocalLibrary", b =>
{
b.Property<string>("LocalLibraryId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("BasePath")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("LibraryName")
.IsRequired()
.HasMaxLength(512)
.HasColumnType("character varying(512)");
b.HasKey("LocalLibraryId");
b.ToTable("LocalLibraries");
});
modelBuilder.Entity("API.Schema.Manga", b =>
{
b.Property<string>("MangaId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("CoverFileNameInCache")
.HasMaxLength(512)
.HasColumnType("character varying(512)");
b.Property<string>("CoverUrl")
.IsRequired()
.HasMaxLength(512)
.HasColumnType("character varying(512)");
b.Property<string>("Description")
.IsRequired()
.HasColumnType("text");
b.Property<string>("DirectoryName")
.IsRequired()
.HasMaxLength(1024)
.HasColumnType("character varying(1024)");
b.Property<string>("IdOnConnectorSite")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<float>("IgnoreChaptersBefore")
.HasColumnType("real");
b.Property<string>("LibraryId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("MangaConnectorName")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("character varying(32)");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(512)
.HasColumnType("character varying(512)");
b.Property<string>("OriginalLanguage")
.HasMaxLength(8)
.HasColumnType("character varying(8)");
b.Property<byte>("ReleaseStatus")
.HasColumnType("smallint");
b.Property<string>("WebsiteUrl")
.IsRequired()
.HasMaxLength(512)
.HasColumnType("character varying(512)");
b.Property<long?>("Year")
.HasColumnType("bigint");
b.HasKey("MangaId");
b.HasIndex("LibraryId");
b.HasIndex("MangaConnectorName");
b.ToTable("Mangas");
});
modelBuilder.Entity("API.Schema.MangaConnectors.MangaConnector", b =>
{
b.Property<string>("Name")
.HasMaxLength(32)
.HasColumnType("character varying(32)");
b.PrimitiveCollection<string[]>("BaseUris")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("text[]");
b.Property<bool>("Enabled")
.HasColumnType("boolean");
b.Property<string>("IconUrl")
.IsRequired()
.HasMaxLength(2048)
.HasColumnType("character varying(2048)");
b.PrimitiveCollection<string[]>("SupportedLanguages")
.IsRequired()
.HasMaxLength(8)
.HasColumnType("text[]");
b.HasKey("Name");
b.ToTable("MangaConnectors");
b.HasDiscriminator<string>("Name").HasValue("MangaConnector");
b.UseTphMappingStrategy();
});
modelBuilder.Entity("API.Schema.MangaTag", b =>
{
b.Property<string>("Tag")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.HasKey("Tag");
b.ToTable("Tags");
});
modelBuilder.Entity("AuthorToManga", b =>
{
b.Property<string>("AuthorIds")
.HasColumnType("character varying(64)");
b.Property<string>("MangaIds")
.HasColumnType("character varying(64)");
b.HasKey("AuthorIds", "MangaIds");
b.HasIndex("MangaIds");
b.ToTable("AuthorToManga");
});
modelBuilder.Entity("JobJob", b =>
{
b.Property<string>("DependsOnJobsJobId")
.HasColumnType("character varying(64)");
b.Property<string>("JobId")
.HasColumnType("character varying(64)");
b.HasKey("DependsOnJobsJobId", "JobId");
b.HasIndex("JobId");
b.ToTable("JobJob");
});
modelBuilder.Entity("MangaTagToManga", b =>
{
b.Property<string>("MangaTagIds")
.HasColumnType("character varying(64)");
b.Property<string>("MangaIds")
.HasColumnType("character varying(64)");
b.HasKey("MangaTagIds", "MangaIds");
b.HasIndex("MangaIds");
b.ToTable("MangaTagToManga");
});
modelBuilder.Entity("API.Schema.Jobs.DownloadAvailableChaptersJob", b =>
{
b.HasBaseType("API.Schema.Jobs.Job");
b.Property<string>("MangaId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.HasIndex("MangaId");
b.ToTable("Jobs", t =>
{
t.Property("MangaId")
.HasColumnName("DownloadAvailableChaptersJob_MangaId");
});
b.HasDiscriminator().HasValue((byte)1);
});
modelBuilder.Entity("API.Schema.Jobs.DownloadMangaCoverJob", b =>
{
b.HasBaseType("API.Schema.Jobs.Job");
b.Property<string>("MangaId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.HasIndex("MangaId");
b.HasDiscriminator().HasValue((byte)4);
});
modelBuilder.Entity("API.Schema.Jobs.DownloadSingleChapterJob", b =>
{
b.HasBaseType("API.Schema.Jobs.Job");
b.Property<string>("ChapterId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.HasIndex("ChapterId");
b.HasDiscriminator().HasValue((byte)0);
});
modelBuilder.Entity("API.Schema.Jobs.MoveFileOrFolderJob", b =>
{
b.HasBaseType("API.Schema.Jobs.Job");
b.Property<string>("FromLocation")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("ToLocation")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.HasDiscriminator().HasValue((byte)3);
});
modelBuilder.Entity("API.Schema.Jobs.MoveMangaLibraryJob", b =>
{
b.HasBaseType("API.Schema.Jobs.Job");
b.Property<string>("MangaId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("ToLibraryId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.HasIndex("MangaId");
b.HasIndex("ToLibraryId");
b.ToTable("Jobs", t =>
{
t.Property("MangaId")
.HasColumnName("MoveMangaLibraryJob_MangaId");
});
b.HasDiscriminator().HasValue((byte)7);
});
modelBuilder.Entity("API.Schema.Jobs.RetrieveChaptersJob", b =>
{
b.HasBaseType("API.Schema.Jobs.Job");
b.Property<string>("Language")
.IsRequired()
.HasMaxLength(8)
.HasColumnType("character varying(8)");
b.Property<string>("MangaId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.HasIndex("MangaId");
b.ToTable("Jobs", t =>
{
t.Property("MangaId")
.HasColumnName("RetrieveChaptersJob_MangaId");
});
b.HasDiscriminator().HasValue((byte)5);
});
modelBuilder.Entity("API.Schema.Jobs.UpdateChaptersDownloadedJob", b =>
{
b.HasBaseType("API.Schema.Jobs.Job");
b.Property<string>("MangaId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.HasIndex("MangaId");
b.ToTable("Jobs", t =>
{
t.Property("MangaId")
.HasColumnName("UpdateChaptersDownloadedJob_MangaId");
});
b.HasDiscriminator().HasValue((byte)6);
});
modelBuilder.Entity("API.Schema.Jobs.UpdateSingleChapterDownloadedJob", b =>
{
b.HasBaseType("API.Schema.Jobs.Job");
b.Property<string>("ChapterId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.HasIndex("ChapterId");
b.ToTable("Jobs", t =>
{
t.Property("ChapterId")
.HasColumnName("UpdateSingleChapterDownloadedJob_ChapterId");
});
b.HasDiscriminator().HasValue((byte)8);
});
modelBuilder.Entity("API.Schema.MangaConnectors.ComickIo", b =>
{
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
b.HasDiscriminator().HasValue("ComickIo");
});
modelBuilder.Entity("API.Schema.MangaConnectors.Global", b =>
{
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
b.HasDiscriminator().HasValue("Global");
});
modelBuilder.Entity("API.Schema.MangaConnectors.MangaDex", b =>
{
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
b.HasDiscriminator().HasValue("MangaDex");
});
modelBuilder.Entity("API.Schema.Chapter", b =>
{
b.HasOne("API.Schema.Manga", "ParentManga")
.WithMany("Chapters")
.HasForeignKey("ParentMangaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("ParentManga");
});
modelBuilder.Entity("API.Schema.Jobs.Job", b =>
{
b.HasOne("API.Schema.Jobs.Job", "ParentJob")
.WithMany()
.HasForeignKey("ParentJobId")
.OnDelete(DeleteBehavior.Cascade);
b.Navigation("ParentJob");
});
modelBuilder.Entity("API.Schema.Manga", b =>
{
b.HasOne("API.Schema.LocalLibrary", "Library")
.WithMany()
.HasForeignKey("LibraryId")
.OnDelete(DeleteBehavior.SetNull);
b.HasOne("API.Schema.MangaConnectors.MangaConnector", "MangaConnector")
.WithMany()
.HasForeignKey("MangaConnectorName")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.OwnsMany("API.Schema.Link", "Links", b1 =>
{
b1.Property<string>("LinkId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b1.Property<string>("LinkProvider")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b1.Property<string>("LinkUrl")
.IsRequired()
.HasMaxLength(2048)
.HasColumnType("character varying(2048)");
b1.Property<string>("MangaId")
.IsRequired()
.HasColumnType("character varying(64)");
b1.HasKey("LinkId");
b1.HasIndex("MangaId");
b1.ToTable("Link");
b1.WithOwner()
.HasForeignKey("MangaId");
});
b.OwnsMany("API.Schema.MangaAltTitle", "AltTitles", b1 =>
{
b1.Property<string>("AltTitleId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b1.Property<string>("Language")
.IsRequired()
.HasMaxLength(8)
.HasColumnType("character varying(8)");
b1.Property<string>("MangaId")
.IsRequired()
.HasColumnType("character varying(64)");
b1.Property<string>("Title")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b1.HasKey("AltTitleId");
b1.HasIndex("MangaId");
b1.ToTable("MangaAltTitle");
b1.WithOwner()
.HasForeignKey("MangaId");
});
b.Navigation("AltTitles");
b.Navigation("Library");
b.Navigation("Links");
b.Navigation("MangaConnector");
});
modelBuilder.Entity("AuthorToManga", b =>
{
b.HasOne("API.Schema.Author", null)
.WithMany()
.HasForeignKey("AuthorIds")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("API.Schema.Manga", null)
.WithMany()
.HasForeignKey("MangaIds")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("JobJob", b =>
{
b.HasOne("API.Schema.Jobs.Job", null)
.WithMany()
.HasForeignKey("DependsOnJobsJobId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("API.Schema.Jobs.Job", null)
.WithMany()
.HasForeignKey("JobId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("MangaTagToManga", b =>
{
b.HasOne("API.Schema.Manga", null)
.WithMany()
.HasForeignKey("MangaIds")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("API.Schema.MangaTag", null)
.WithMany()
.HasForeignKey("MangaTagIds")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("API.Schema.Jobs.DownloadAvailableChaptersJob", b =>
{
b.HasOne("API.Schema.Manga", "Manga")
.WithMany()
.HasForeignKey("MangaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Manga");
});
modelBuilder.Entity("API.Schema.Jobs.DownloadMangaCoverJob", b =>
{
b.HasOne("API.Schema.Manga", "Manga")
.WithMany()
.HasForeignKey("MangaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Manga");
});
modelBuilder.Entity("API.Schema.Jobs.DownloadSingleChapterJob", b =>
{
b.HasOne("API.Schema.Chapter", "Chapter")
.WithMany()
.HasForeignKey("ChapterId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Chapter");
});
modelBuilder.Entity("API.Schema.Jobs.MoveMangaLibraryJob", b =>
{
b.HasOne("API.Schema.Manga", "Manga")
.WithMany()
.HasForeignKey("MangaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("API.Schema.LocalLibrary", "ToLibrary")
.WithMany()
.HasForeignKey("ToLibraryId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Manga");
b.Navigation("ToLibrary");
});
modelBuilder.Entity("API.Schema.Jobs.RetrieveChaptersJob", b =>
{
b.HasOne("API.Schema.Manga", "Manga")
.WithMany()
.HasForeignKey("MangaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Manga");
});
modelBuilder.Entity("API.Schema.Jobs.UpdateChaptersDownloadedJob", b =>
{
b.HasOne("API.Schema.Manga", "Manga")
.WithMany()
.HasForeignKey("MangaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Manga");
});
modelBuilder.Entity("API.Schema.Jobs.UpdateSingleChapterDownloadedJob", b =>
{
b.HasOne("API.Schema.Chapter", "Chapter")
.WithMany()
.HasForeignKey("ChapterId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Chapter");
});
modelBuilder.Entity("API.Schema.Manga", b =>
{
b.Navigation("Chapters");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,94 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace API.Migrations.pgsql
{
/// <inheritdoc />
public partial class SplitUpdateChaptersDownloadedJobIntoUpdateSingleChapterDownloadedJob : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_Jobs_Mangas_UpdateFilesDownloadedJob_MangaId",
table: "Jobs");
migrationBuilder.RenameColumn(
name: "UpdateFilesDownloadedJob_MangaId",
table: "Jobs",
newName: "UpdateChaptersDownloadedJob_MangaId");
migrationBuilder.RenameIndex(
name: "IX_Jobs_UpdateFilesDownloadedJob_MangaId",
table: "Jobs",
newName: "IX_Jobs_UpdateChaptersDownloadedJob_MangaId");
migrationBuilder.AddColumn<string>(
name: "UpdateSingleChapterDownloadedJob_ChapterId",
table: "Jobs",
type: "character varying(64)",
maxLength: 64,
nullable: true);
migrationBuilder.CreateIndex(
name: "IX_Jobs_UpdateSingleChapterDownloadedJob_ChapterId",
table: "Jobs",
column: "UpdateSingleChapterDownloadedJob_ChapterId");
migrationBuilder.AddForeignKey(
name: "FK_Jobs_Chapters_UpdateSingleChapterDownloadedJob_ChapterId",
table: "Jobs",
column: "UpdateSingleChapterDownloadedJob_ChapterId",
principalTable: "Chapters",
principalColumn: "ChapterId",
onDelete: ReferentialAction.Cascade);
migrationBuilder.AddForeignKey(
name: "FK_Jobs_Mangas_UpdateChaptersDownloadedJob_MangaId",
table: "Jobs",
column: "UpdateChaptersDownloadedJob_MangaId",
principalTable: "Mangas",
principalColumn: "MangaId",
onDelete: ReferentialAction.Cascade);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_Jobs_Chapters_UpdateSingleChapterDownloadedJob_ChapterId",
table: "Jobs");
migrationBuilder.DropForeignKey(
name: "FK_Jobs_Mangas_UpdateChaptersDownloadedJob_MangaId",
table: "Jobs");
migrationBuilder.DropIndex(
name: "IX_Jobs_UpdateSingleChapterDownloadedJob_ChapterId",
table: "Jobs");
migrationBuilder.DropColumn(
name: "UpdateSingleChapterDownloadedJob_ChapterId",
table: "Jobs");
migrationBuilder.RenameColumn(
name: "UpdateChaptersDownloadedJob_MangaId",
table: "Jobs",
newName: "UpdateFilesDownloadedJob_MangaId");
migrationBuilder.RenameIndex(
name: "IX_Jobs_UpdateChaptersDownloadedJob_MangaId",
table: "Jobs",
newName: "IX_Jobs_UpdateFilesDownloadedJob_MangaId");
migrationBuilder.AddForeignKey(
name: "FK_Jobs_Mangas_UpdateFilesDownloadedJob_MangaId",
table: "Jobs",
column: "UpdateFilesDownloadedJob_MangaId",
principalTable: "Mangas",
principalColumn: "MangaId",
onDelete: ReferentialAction.Cascade);
}
}
}

View File

@ -413,7 +413,7 @@ namespace API.Migrations.pgsql
b.HasDiscriminator().HasValue((byte)5); b.HasDiscriminator().HasValue((byte)5);
}); });
modelBuilder.Entity("API.Schema.Jobs.UpdateFilesDownloadedJob", b => modelBuilder.Entity("API.Schema.Jobs.UpdateChaptersDownloadedJob", b =>
{ {
b.HasBaseType("API.Schema.Jobs.Job"); b.HasBaseType("API.Schema.Jobs.Job");
@ -427,12 +427,32 @@ namespace API.Migrations.pgsql
b.ToTable("Jobs", t => b.ToTable("Jobs", t =>
{ {
t.Property("MangaId") t.Property("MangaId")
.HasColumnName("UpdateFilesDownloadedJob_MangaId"); .HasColumnName("UpdateChaptersDownloadedJob_MangaId");
}); });
b.HasDiscriminator().HasValue((byte)6); b.HasDiscriminator().HasValue((byte)6);
}); });
modelBuilder.Entity("API.Schema.Jobs.UpdateSingleChapterDownloadedJob", b =>
{
b.HasBaseType("API.Schema.Jobs.Job");
b.Property<string>("ChapterId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.HasIndex("ChapterId");
b.ToTable("Jobs", t =>
{
t.Property("ChapterId")
.HasColumnName("UpdateSingleChapterDownloadedJob_ChapterId");
});
b.HasDiscriminator().HasValue((byte)8);
});
modelBuilder.Entity("API.Schema.MangaConnectors.ComickIo", b => modelBuilder.Entity("API.Schema.MangaConnectors.ComickIo", b =>
{ {
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector"); b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
@ -665,7 +685,7 @@ namespace API.Migrations.pgsql
b.Navigation("Manga"); b.Navigation("Manga");
}); });
modelBuilder.Entity("API.Schema.Jobs.UpdateFilesDownloadedJob", b => modelBuilder.Entity("API.Schema.Jobs.UpdateChaptersDownloadedJob", b =>
{ {
b.HasOne("API.Schema.Manga", "Manga") b.HasOne("API.Schema.Manga", "Manga")
.WithMany() .WithMany()
@ -676,6 +696,17 @@ namespace API.Migrations.pgsql
b.Navigation("Manga"); b.Navigation("Manga");
}); });
modelBuilder.Entity("API.Schema.Jobs.UpdateSingleChapterDownloadedJob", b =>
{
b.HasOne("API.Schema.Chapter", "Chapter")
.WithMany()
.HasForeignKey("ChapterId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Chapter");
});
modelBuilder.Entity("API.Schema.Manga", b => modelBuilder.Entity("API.Schema.Manga", b =>
{ {
b.Navigation("Chapters"); b.Navigation("Chapters");

View File

@ -122,12 +122,9 @@ using (IServiceScope scope = app.Services.CreateScope())
context.LocalLibraries.Add(new LocalLibrary(TrangaSettings.downloadLocation, "Default Library")); context.LocalLibraries.Add(new LocalLibrary(TrangaSettings.downloadLocation, "Default Library"));
context.Jobs.AddRange(context.Jobs.Where(j => j.JobType == JobType.DownloadAvailableChaptersJob) context.Jobs.AddRange(context.Jobs.Where(j => j.JobType == JobType.DownloadAvailableChaptersJob)
.AsEnumerable() .Include(downloadAvailableChaptersJob => ((DownloadAvailableChaptersJob)downloadAvailableChaptersJob).Manga)
.Select(dacj => .ToList()
{ .Select(dacj => new UpdateChaptersDownloadedJob(((DownloadAvailableChaptersJob)dacj).Manga, 0)));
DownloadAvailableChaptersJob? j = dacj as DownloadAvailableChaptersJob;
return new UpdateFilesDownloadedJob(j!.Manga, 0);
}));
context.Jobs.RemoveRange(context.Jobs.Where(j => j.state == JobState.Completed && j.RecurrenceMs < 1)); context.Jobs.RemoveRange(context.Jobs.Where(j => j.state == JobState.Completed && j.RecurrenceMs < 1));
foreach (Job job in context.Jobs.Where(j => j.state == JobState.Running)) foreach (Job job in context.Jobs.Where(j => j.state == JobState.Running))
{ {
@ -153,7 +150,7 @@ using (IServiceScope scope = app.Services.CreateScope())
TrangaSettings.Load(); TrangaSettings.Load();
Tranga.StartLogger(); Tranga.StartLogger();
Tranga.JobStarterThread.Start(app.Services); Tranga.JobStarterThread.Start(app.Services);
Tranga.NotificationSenderThread.Start(app.Services); //Tranga.NotificationSenderThread.Start(app.Services); //TODO RE-ENABLE
app.UseCors("AllowAll"); app.UseCors("AllowAll");

View File

@ -26,7 +26,7 @@ public class Chapter : IComparable<Chapter>
[StringLength(256)] [Required] public string FileName { get; private set; } [StringLength(256)] [Required] public string FileName { get; private set; }
[Required] public bool Downloaded { get; internal set; } [Required] public bool Downloaded { get; internal set; }
[JsonIgnore] [NotMapped] public string FullArchiveFilePath => Path.Join(ParentManga.FullDirectoryPath, FileName); [NotMapped] public string FullArchiveFilePath => Path.Join(ParentManga.FullDirectoryPath, FileName);
public Chapter(Manga parentManga, string url, string chapterNumber, int? volumeNumber = null, string? title = null) public Chapter(Manga parentManga, string url, string chapterNumber, int? volumeNumber = null, string? title = null)
{ {

View File

@ -1,12 +1,26 @@
using API.Schema.LibraryConnectors; using API.Schema.LibraryConnectors;
using log4net;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Diagnostics;
namespace API.Schema.Contexts; namespace API.Schema.Contexts;
public class LibraryContext(DbContextOptions<LibraryContext> options) : DbContext(options) public class LibraryContext(DbContextOptions<LibraryContext> options) : DbContext(options)
{ {
public DbSet<LibraryConnector> LibraryConnectors { get; set; } public DbSet<LibraryConnector> LibraryConnectors { get; set; }
private ILog Log => LogManager.GetLogger(GetType());
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
base.OnConfiguring(optionsBuilder);
optionsBuilder.EnableSensitiveDataLogging();
optionsBuilder.LogTo(s =>
{
Log.Debug(s);
}, [DbLoggerCategory.Query.Name], LogLevel.Trace, DbContextLoggerOptions.Level | DbContextLoggerOptions.Category);
}
protected override void OnModelCreating(ModelBuilder modelBuilder) protected override void OnModelCreating(ModelBuilder modelBuilder)
{ {
//LibraryConnector Types //LibraryConnector Types

View File

@ -1,5 +1,7 @@
using API.Schema.NotificationConnectors; using API.Schema.NotificationConnectors;
using log4net;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Diagnostics;
namespace API.Schema.Contexts; namespace API.Schema.Contexts;
@ -7,4 +9,15 @@ public class NotificationsContext(DbContextOptions<NotificationsContext> options
{ {
public DbSet<NotificationConnector> NotificationConnectors { get; set; } public DbSet<NotificationConnector> NotificationConnectors { get; set; }
public DbSet<Notification> Notifications { get; set; } public DbSet<Notification> Notifications { get; set; }
private ILog Log => LogManager.GetLogger(GetType());
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
base.OnConfiguring(optionsBuilder);
optionsBuilder.EnableSensitiveDataLogging();
optionsBuilder.LogTo(s =>
{
Log.Debug(s);
}, [DbLoggerCategory.Query.Name], LogLevel.Trace, DbContextLoggerOptions.Level | DbContextLoggerOptions.Category);
}
} }

View File

@ -1,6 +1,8 @@
using API.Schema.Jobs; using API.Schema.Jobs;
using API.Schema.MangaConnectors; using API.Schema.MangaConnectors;
using log4net;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Diagnostics;
namespace API.Schema.Contexts; namespace API.Schema.Contexts;
@ -13,7 +15,18 @@ public class PgsqlContext(DbContextOptions<PgsqlContext> options) : DbContext(op
public DbSet<Chapter> Chapters { get; set; } public DbSet<Chapter> Chapters { get; set; }
public DbSet<Author> Authors { get; set; } public DbSet<Author> Authors { get; set; }
public DbSet<MangaTag> Tags { get; set; } public DbSet<MangaTag> Tags { get; set; }
private ILog Log => LogManager.GetLogger(GetType());
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
base.OnConfiguring(optionsBuilder);
optionsBuilder.EnableSensitiveDataLogging();
optionsBuilder.LogTo(s =>
{
Log.Debug(s);
}, [DbLoggerCategory.Query.Name], LogLevel.Trace, DbContextLoggerOptions.Level | DbContextLoggerOptions.Category);
}
protected override void OnModelCreating(ModelBuilder modelBuilder) protected override void OnModelCreating(ModelBuilder modelBuilder)
{ {
//Job Types //Job Types
@ -25,7 +38,8 @@ public class PgsqlContext(DbContextOptions<PgsqlContext> options) : DbContext(op
.HasValue<DownloadSingleChapterJob>(JobType.DownloadSingleChapterJob) .HasValue<DownloadSingleChapterJob>(JobType.DownloadSingleChapterJob)
.HasValue<DownloadMangaCoverJob>(JobType.DownloadMangaCoverJob) .HasValue<DownloadMangaCoverJob>(JobType.DownloadMangaCoverJob)
.HasValue<RetrieveChaptersJob>(JobType.RetrieveChaptersJob) .HasValue<RetrieveChaptersJob>(JobType.RetrieveChaptersJob)
.HasValue<UpdateFilesDownloadedJob>(JobType.UpdateFilesDownloadedJob); .HasValue<UpdateChaptersDownloadedJob>(JobType.UpdateChaptersDownloadedJob)
.HasValue<UpdateSingleChapterDownloadedJob>(JobType.UpdateSingleChapterDownloadedJob);
//Job specification //Job specification
modelBuilder.Entity<DownloadAvailableChaptersJob>() modelBuilder.Entity<DownloadAvailableChaptersJob>()
@ -36,7 +50,7 @@ public class PgsqlContext(DbContextOptions<PgsqlContext> options) : DbContext(op
.OnDelete(DeleteBehavior.Cascade); .OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<DownloadAvailableChaptersJob>() modelBuilder.Entity<DownloadAvailableChaptersJob>()
.Navigation(j => j.Manga) .Navigation(j => j.Manga)
.AutoInclude(); .EnableLazyLoading();
modelBuilder.Entity<DownloadMangaCoverJob>() modelBuilder.Entity<DownloadMangaCoverJob>()
.HasOne<Manga>(j => j.Manga) .HasOne<Manga>(j => j.Manga)
.WithMany() .WithMany()
@ -45,7 +59,7 @@ public class PgsqlContext(DbContextOptions<PgsqlContext> options) : DbContext(op
.OnDelete(DeleteBehavior.Cascade); .OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<DownloadMangaCoverJob>() modelBuilder.Entity<DownloadMangaCoverJob>()
.Navigation(j => j.Manga) .Navigation(j => j.Manga)
.AutoInclude(); .EnableLazyLoading();
modelBuilder.Entity<DownloadSingleChapterJob>() modelBuilder.Entity<DownloadSingleChapterJob>()
.HasOne<Chapter>(j => j.Chapter) .HasOne<Chapter>(j => j.Chapter)
.WithMany() .WithMany()
@ -54,7 +68,7 @@ public class PgsqlContext(DbContextOptions<PgsqlContext> options) : DbContext(op
.OnDelete(DeleteBehavior.Cascade); .OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<DownloadSingleChapterJob>() modelBuilder.Entity<DownloadSingleChapterJob>()
.Navigation(j => j.Chapter) .Navigation(j => j.Chapter)
.AutoInclude(); .EnableLazyLoading();
modelBuilder.Entity<MoveMangaLibraryJob>() modelBuilder.Entity<MoveMangaLibraryJob>()
.HasOne<Manga>(j => j.Manga) .HasOne<Manga>(j => j.Manga)
.WithMany() .WithMany()
@ -63,7 +77,7 @@ public class PgsqlContext(DbContextOptions<PgsqlContext> options) : DbContext(op
.OnDelete(DeleteBehavior.Cascade); .OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<MoveMangaLibraryJob>() modelBuilder.Entity<MoveMangaLibraryJob>()
.Navigation(j => j.Manga) .Navigation(j => j.Manga)
.AutoInclude(); .EnableLazyLoading();
modelBuilder.Entity<MoveMangaLibraryJob>() modelBuilder.Entity<MoveMangaLibraryJob>()
.HasOne<LocalLibrary>(j => j.ToLibrary) .HasOne<LocalLibrary>(j => j.ToLibrary)
.WithMany() .WithMany()
@ -72,7 +86,7 @@ public class PgsqlContext(DbContextOptions<PgsqlContext> options) : DbContext(op
.OnDelete(DeleteBehavior.Cascade); .OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<MoveMangaLibraryJob>() modelBuilder.Entity<MoveMangaLibraryJob>()
.Navigation(j => j.ToLibrary) .Navigation(j => j.ToLibrary)
.AutoInclude(); .EnableLazyLoading();
modelBuilder.Entity<RetrieveChaptersJob>() modelBuilder.Entity<RetrieveChaptersJob>()
.HasOne<Manga>(j => j.Manga) .HasOne<Manga>(j => j.Manga)
.WithMany() .WithMany()
@ -81,23 +95,22 @@ public class PgsqlContext(DbContextOptions<PgsqlContext> options) : DbContext(op
.OnDelete(DeleteBehavior.Cascade); .OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<RetrieveChaptersJob>() modelBuilder.Entity<RetrieveChaptersJob>()
.Navigation(j => j.Manga) .Navigation(j => j.Manga)
.AutoInclude(); .EnableLazyLoading();
modelBuilder.Entity<UpdateFilesDownloadedJob>() modelBuilder.Entity<UpdateChaptersDownloadedJob>()
.HasOne<Manga>(j => j.Manga) .HasOne<Manga>(j => j.Manga)
.WithMany() .WithMany()
.HasForeignKey(j => j.MangaId) .HasForeignKey(j => j.MangaId)
.IsRequired() .IsRequired()
.OnDelete(DeleteBehavior.Cascade); .OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<UpdateFilesDownloadedJob>() modelBuilder.Entity<UpdateChaptersDownloadedJob>()
.Navigation(j => j.Manga) .Navigation(j => j.Manga)
.AutoInclude(); .EnableLazyLoading();
//Job has possible ParentJob //Job has possible ParentJob
modelBuilder.Entity<Job>() modelBuilder.Entity<Job>()
.HasMany<Job>() .HasOne<Job>(childJob => childJob.ParentJob)
.WithOne(childJob => childJob.ParentJob) .WithMany()
.HasForeignKey(childjob => childjob.ParentJobId) .HasForeignKey(childjob => childjob.ParentJobId)
.IsRequired(false)
.OnDelete(DeleteBehavior.Cascade); .OnDelete(DeleteBehavior.Cascade);
//Job might be dependent on other Jobs //Job might be dependent on other Jobs
modelBuilder.Entity<Job>() modelBuilder.Entity<Job>()
@ -105,7 +118,8 @@ public class PgsqlContext(DbContextOptions<PgsqlContext> options) : DbContext(op
.WithMany(); .WithMany();
modelBuilder.Entity<Job>() modelBuilder.Entity<Job>()
.Navigation(j => j.DependsOnJobs) .Navigation(j => j.DependsOnJobs)
.AutoInclude(false); .AutoInclude(false)
.EnableLazyLoading();
//MangaConnector Types //MangaConnector Types
modelBuilder.Entity<MangaConnector>() modelBuilder.Entity<MangaConnector>()
@ -136,7 +150,8 @@ public class PgsqlContext(DbContextOptions<PgsqlContext> options) : DbContext(op
.AutoInclude(); .AutoInclude();
modelBuilder.Entity<Manga>() modelBuilder.Entity<Manga>()
.Navigation(m => m.Chapters) .Navigation(m => m.Chapters)
.AutoInclude(false); .AutoInclude(false)
.EnableLazyLoading();
//Manga owns MangaAltTitles //Manga owns MangaAltTitles
modelBuilder.Entity<Manga>() modelBuilder.Entity<Manga>()
.OwnsMany<MangaAltTitle>(m => m.AltTitles) .OwnsMany<MangaAltTitle>(m => m.AltTitles)

View File

@ -1,5 +1,6 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using API.Schema.Contexts; using API.Schema.Contexts;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Newtonsoft.Json; using Newtonsoft.Json;
namespace API.Schema.Jobs; namespace API.Schema.Jobs;
@ -7,7 +8,15 @@ namespace API.Schema.Jobs;
public class DownloadAvailableChaptersJob : Job public class DownloadAvailableChaptersJob : Job
{ {
[StringLength(64)] [Required] public string MangaId { get; init; } [StringLength(64)] [Required] public string MangaId { get; init; }
[JsonIgnore] public Manga Manga { get; init; } = null!;
private Manga _manga = null!;
[JsonIgnore]
public Manga Manga
{
get => LazyLoader.Load(this, ref _manga);
init => _manga = value;
}
public DownloadAvailableChaptersJob(Manga manga, ulong recurrenceMs, Job? parentJob = null, ICollection<Job>? dependsOnJobs = null) public DownloadAvailableChaptersJob(Manga manga, ulong recurrenceMs, Job? parentJob = null, ICollection<Job>? dependsOnJobs = null)
: base(TokenGen.CreateToken(typeof(DownloadAvailableChaptersJob)), JobType.DownloadAvailableChaptersJob, recurrenceMs, parentJob, dependsOnJobs) : base(TokenGen.CreateToken(typeof(DownloadAvailableChaptersJob)), JobType.DownloadAvailableChaptersJob, recurrenceMs, parentJob, dependsOnJobs)
@ -19,15 +28,14 @@ public class DownloadAvailableChaptersJob : Job
/// <summary> /// <summary>
/// EF ONLY!!! /// EF ONLY!!!
/// </summary> /// </summary>
internal DownloadAvailableChaptersJob(string mangaId, ulong recurrenceMs, string? parentJobId) internal DownloadAvailableChaptersJob(ILazyLoader lazyLoader, string mangaId, ulong recurrenceMs, string? parentJobId)
: base(TokenGen.CreateToken(typeof(DownloadAvailableChaptersJob)), JobType.DownloadAvailableChaptersJob, recurrenceMs, parentJobId) : base(lazyLoader, TokenGen.CreateToken(typeof(DownloadAvailableChaptersJob)), JobType.DownloadAvailableChaptersJob, recurrenceMs, parentJobId)
{ {
this.MangaId = mangaId; this.MangaId = mangaId;
} }
protected override IEnumerable<Job> RunInternal(PgsqlContext context) protected override IEnumerable<Job> RunInternal(PgsqlContext context)
{ {
context.Attach(Manga); return Manga.Chapters.Where(c => c.Downloaded == false).Select(chapter => new DownloadSingleChapterJob(chapter, this));
return Manga.Chapters.Select(chapter => new DownloadSingleChapterJob(chapter, this));
} }
} }

View File

@ -1,6 +1,7 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using API.Schema.Contexts; using API.Schema.Contexts;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Newtonsoft.Json; using Newtonsoft.Json;
namespace API.Schema.Jobs; namespace API.Schema.Jobs;
@ -8,7 +9,15 @@ namespace API.Schema.Jobs;
public class DownloadMangaCoverJob : Job public class DownloadMangaCoverJob : Job
{ {
[StringLength(64)] [Required] public string MangaId { get; init; } [StringLength(64)] [Required] public string MangaId { get; init; }
[JsonIgnore] public Manga Manga { get; init; } = null!;
private Manga _manga = null!;
[JsonIgnore]
public Manga Manga
{
get => LazyLoader.Load(this, ref _manga);
init => _manga = value;
}
public DownloadMangaCoverJob(Manga manga, Job? parentJob = null, ICollection<Job>? dependsOnJobs = null) public DownloadMangaCoverJob(Manga manga, Job? parentJob = null, ICollection<Job>? dependsOnJobs = null)
: base(TokenGen.CreateToken(typeof(DownloadMangaCoverJob)), JobType.DownloadMangaCoverJob, 0, parentJob, dependsOnJobs) : base(TokenGen.CreateToken(typeof(DownloadMangaCoverJob)), JobType.DownloadMangaCoverJob, 0, parentJob, dependsOnJobs)
@ -20,15 +29,14 @@ public class DownloadMangaCoverJob : Job
/// <summary> /// <summary>
/// EF ONLY!!! /// EF ONLY!!!
/// </summary> /// </summary>
internal DownloadMangaCoverJob(string mangaId, string? parentJobId) internal DownloadMangaCoverJob(ILazyLoader lazyLoader, string mangaId, string? parentJobId)
: base(TokenGen.CreateToken(typeof(DownloadMangaCoverJob)), JobType.DownloadMangaCoverJob, 0, parentJobId) : base(lazyLoader, TokenGen.CreateToken(typeof(DownloadMangaCoverJob)), JobType.DownloadMangaCoverJob, 0, parentJobId)
{ {
this.MangaId = mangaId; this.MangaId = mangaId;
} }
protected override IEnumerable<Job> RunInternal(PgsqlContext context) protected override IEnumerable<Job> RunInternal(PgsqlContext context)
{ {
context.Attach(Manga);
try try
{ {
Manga.CoverFileNameInCache = Manga.MangaConnector.SaveCoverImageToCache(Manga); Manga.CoverFileNameInCache = Manga.MangaConnector.SaveCoverImageToCache(Manga);

View File

@ -3,6 +3,8 @@ using System.IO.Compression;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using API.MangaDownloadClients; using API.MangaDownloadClients;
using API.Schema.Contexts; using API.Schema.Contexts;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Newtonsoft.Json; using Newtonsoft.Json;
using SixLabors.ImageSharp; using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Formats.Jpeg; using SixLabors.ImageSharp.Formats.Jpeg;
@ -16,7 +18,14 @@ public class DownloadSingleChapterJob : Job
{ {
[StringLength(64)] [Required] public string ChapterId { get; init; } [StringLength(64)] [Required] public string ChapterId { get; init; }
[JsonIgnore] public Chapter Chapter { get; init; } = null!; private Chapter _chapter = null!;
[JsonIgnore]
public Chapter Chapter
{
get => LazyLoader.Load(this, ref _chapter);
init => _chapter = value;
}
public DownloadSingleChapterJob(Chapter chapter, Job? parentJob = null, ICollection<Job>? dependsOnJobs = null) public DownloadSingleChapterJob(Chapter chapter, Job? parentJob = null, ICollection<Job>? dependsOnJobs = null)
: base(TokenGen.CreateToken(typeof(DownloadSingleChapterJob)), JobType.DownloadSingleChapterJob, 0, parentJob, dependsOnJobs) : base(TokenGen.CreateToken(typeof(DownloadSingleChapterJob)), JobType.DownloadSingleChapterJob, 0, parentJob, dependsOnJobs)
@ -28,15 +37,14 @@ public class DownloadSingleChapterJob : Job
/// <summary> /// <summary>
/// EF ONLY!!! /// EF ONLY!!!
/// </summary> /// </summary>
internal DownloadSingleChapterJob(string chapterId, string? parentJobId) internal DownloadSingleChapterJob(ILazyLoader lazyLoader, string chapterId, string? parentJobId)
: base(TokenGen.CreateToken(typeof(DownloadSingleChapterJob)), JobType.DownloadSingleChapterJob, 0, parentJobId) : base(lazyLoader, TokenGen.CreateToken(typeof(DownloadSingleChapterJob)), JobType.DownloadSingleChapterJob, 0, parentJobId)
{ {
this.ChapterId = chapterId; this.ChapterId = chapterId;
} }
protected override IEnumerable<Job> RunInternal(PgsqlContext context) protected override IEnumerable<Job> RunInternal(PgsqlContext context)
{ {
context.Attach(Chapter);
string[] imageUrls = Chapter.ParentManga.MangaConnector.GetChapterImageUrls(Chapter); string[] imageUrls = Chapter.ParentManga.MangaConnector.GetChapterImageUrls(Chapter);
if (imageUrls.Length < 1) if (imageUrls.Length < 1)
{ {
@ -97,16 +105,16 @@ public class DownloadSingleChapterJob : Job
Chapter.Downloaded = true; Chapter.Downloaded = true;
context.SaveChanges(); context.SaveChanges();
if (context.Jobs.AsEnumerable().Any(j => if (context.Jobs.ToList().Any(j =>
{ {
if (j.JobType != JobType.UpdateFilesDownloadedJob) if (j.JobType != JobType.UpdateChaptersDownloadedJob)
return false; return false;
UpdateFilesDownloadedJob job = (UpdateFilesDownloadedJob)j; UpdateChaptersDownloadedJob job = (UpdateChaptersDownloadedJob)j;
return job.MangaId == this.Chapter.ParentMangaId; return job.MangaId == this.Chapter.ParentMangaId;
})) }))
return []; return [];
return [new UpdateFilesDownloadedJob(Chapter.ParentManga, 0, this.ParentJob)]; return [new UpdateChaptersDownloadedJob(Chapter.ParentManga, 0, this.ParentJob)];
} }
private void ProcessImage(string imagePath) private void ProcessImage(string imagePath)

View File

@ -3,6 +3,7 @@ using System.ComponentModel.DataAnnotations.Schema;
using API.Schema.Contexts; using API.Schema.Contexts;
using log4net; using log4net;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Newtonsoft.Json; using Newtonsoft.Json;
namespace API.Schema.Jobs; namespace API.Schema.Jobs;
@ -16,7 +17,12 @@ public abstract class Job
[StringLength(64)] public string? ParentJobId { get; init; } [StringLength(64)] public string? ParentJobId { get; init; }
[JsonIgnore] public Job? ParentJob { get; init; } [JsonIgnore] public Job? ParentJob { get; init; }
[JsonIgnore] public ICollection<Job> DependsOnJobs { get; init; } private ICollection<Job> _dependsOnJobs = null!;
[JsonIgnore] public ICollection<Job> DependsOnJobs
{
get => LazyLoader.Load(this, ref _dependsOnJobs);
init => _dependsOnJobs = value;
}
[Required] public JobType JobType { get; init; } [Required] public JobType JobType { get; init; }
@ -32,6 +38,7 @@ public abstract class Job
[JsonIgnore] [NotMapped] internal bool DependenciesFulfilled => DependsOnJobs.All(j => j.IsCompleted); [JsonIgnore] [NotMapped] internal bool DependenciesFulfilled => DependsOnJobs.All(j => j.IsCompleted);
[NotMapped] [JsonIgnore] protected ILog Log { get; init; } [NotMapped] [JsonIgnore] protected ILog Log { get; init; }
[NotMapped] [JsonIgnore] protected ILazyLoader LazyLoader { get; init; }
protected Job(string jobId, JobType jobType, ulong recurrenceMs, Job? parentJob = null, ICollection<Job>? dependsOnJobs = null) protected Job(string jobId, JobType jobType, ulong recurrenceMs, Job? parentJob = null, ICollection<Job>? dependsOnJobs = null)
{ {
@ -48,8 +55,9 @@ public abstract class Job
/// <summary> /// <summary>
/// EF ONLY!!! /// EF ONLY!!!
/// </summary> /// </summary>
protected internal Job(string jobId, JobType jobType, ulong recurrenceMs, string? parentJobId) protected internal Job(ILazyLoader lazyLoader, string jobId, JobType jobType, ulong recurrenceMs, string? parentJobId)
{ {
this.LazyLoader = lazyLoader;
this.JobId = jobId; this.JobId = jobId;
this.JobType = jobType; this.JobType = jobType;
this.RecurrenceMs = recurrenceMs; this.RecurrenceMs = recurrenceMs;
@ -61,29 +69,39 @@ public abstract class Job
public IEnumerable<Job> Run(IServiceProvider serviceProvider) public IEnumerable<Job> Run(IServiceProvider serviceProvider)
{ {
Log.Debug($"Running job {JobId}"); Log.Info($"Running job {JobId}");
using IServiceScope scope = serviceProvider.CreateScope(); DateTime jobStart = DateTime.UtcNow;
Job[]? ret = null;
using IServiceScope scope = serviceProvider.CreateScope();
PgsqlContext context = scope.ServiceProvider.GetRequiredService<PgsqlContext>();
try try
{ {
PgsqlContext context = scope.ServiceProvider.GetRequiredService<PgsqlContext>();
context.Attach(this); context.Attach(this);
this.state = JobState.Running; this.state = JobState.Running;
context.SaveChanges(); context.SaveChanges();
Job[] newJobs = RunInternal(context).ToArray(); ret = RunInternal(context).ToArray();
this.state = JobState.Completed; this.state = JobState.Completed;
context.Jobs.AddRange(ret);
Log.Info($"Job {JobId} completed. Generated {ret.Length} new jobs.");
context.SaveChanges(); context.SaveChanges();
context.Jobs.AddRange(newJobs);
context.SaveChanges();
Log.Info($"Job {JobId} completed. Generated {newJobs.Length} new jobs.");
return newJobs;
} }
catch (DbUpdateException e) catch (Exception e)
{ {
this.state = JobState.Failed; if (e is not DbUpdateException)
Log.Error($"Failed to run job {JobId}", e); {
return []; this.state = JobState.Failed;
Log.Error($"Failed to run job {JobId}", e);
context.SaveChanges();
}
else
{
Log.Error($"Failed to update Database {JobId}", e);
}
} }
Log.Info($"Finished Job {JobId}! (took {DateTime.UtcNow.Subtract(jobStart).TotalMilliseconds}ms)");
return ret ?? [];
} }
protected abstract IEnumerable<Job> RunInternal(PgsqlContext context); protected abstract IEnumerable<Job> RunInternal(PgsqlContext context);

View File

@ -9,6 +9,7 @@ public enum JobType : byte
MoveFileOrFolderJob = 3, MoveFileOrFolderJob = 3,
DownloadMangaCoverJob = 4, DownloadMangaCoverJob = 4,
RetrieveChaptersJob = 5, RetrieveChaptersJob = 5,
UpdateFilesDownloadedJob = 6, UpdateChaptersDownloadedJob = 6,
MoveMangaLibraryJob = 7 MoveMangaLibraryJob = 7,
UpdateSingleChapterDownloadedJob = 8,
} }

View File

@ -1,5 +1,6 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using API.Schema.Contexts; using API.Schema.Contexts;
using Microsoft.EntityFrameworkCore.Infrastructure;
namespace API.Schema.Jobs; namespace API.Schema.Jobs;
@ -22,8 +23,8 @@ public class MoveFileOrFolderJob : Job
/// <summary> /// <summary>
/// EF ONLY!!! /// EF ONLY!!!
/// </summary> /// </summary>
internal MoveFileOrFolderJob(string jobId, string fromLocation, string toLocation, string? parentJobId) internal MoveFileOrFolderJob(ILazyLoader lazyLoader, string jobId, string fromLocation, string toLocation, string? parentJobId)
: base(jobId, JobType.MoveFileOrFolderJob, 0, parentJobId) : base(lazyLoader, jobId, JobType.MoveFileOrFolderJob, 0, parentJobId)
{ {
this.FromLocation = fromLocation; this.FromLocation = fromLocation;
this.ToLocation = toLocation; this.ToLocation = toLocation;

View File

@ -1,6 +1,7 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using API.Schema.Contexts; using API.Schema.Contexts;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Newtonsoft.Json; using Newtonsoft.Json;
namespace API.Schema.Jobs; namespace API.Schema.Jobs;
@ -8,7 +9,15 @@ namespace API.Schema.Jobs;
public class MoveMangaLibraryJob : Job public class MoveMangaLibraryJob : Job
{ {
[StringLength(64)] [Required] public string MangaId { get; init; } [StringLength(64)] [Required] public string MangaId { get; init; }
[JsonIgnore] public Manga Manga { get; init; } = null!;
private Manga _manga = null!;
[JsonIgnore]
public Manga Manga
{
get => LazyLoader.Load(this, ref _manga);
init => _manga = value;
}
[StringLength(64)] [Required] public string ToLibraryId { get; init; } [StringLength(64)] [Required] public string ToLibraryId { get; init; }
public LocalLibrary ToLibrary { get; init; } = null!; public LocalLibrary ToLibrary { get; init; } = null!;
@ -24,8 +33,8 @@ public class MoveMangaLibraryJob : Job
/// <summary> /// <summary>
/// EF ONLY!!! /// EF ONLY!!!
/// </summary> /// </summary>
internal MoveMangaLibraryJob(string mangaId, string toLibraryId, string? parentJobId) internal MoveMangaLibraryJob(ILazyLoader lazyLoader, string mangaId, string toLibraryId, string? parentJobId)
: base(TokenGen.CreateToken(typeof(MoveMangaLibraryJob)), JobType.MoveMangaLibraryJob, 0, parentJobId) : base(lazyLoader, TokenGen.CreateToken(typeof(MoveMangaLibraryJob)), JobType.MoveMangaLibraryJob, 0, parentJobId)
{ {
this.MangaId = mangaId; this.MangaId = mangaId;
this.ToLibraryId = toLibraryId; this.ToLibraryId = toLibraryId;
@ -33,8 +42,6 @@ public class MoveMangaLibraryJob : Job
protected override IEnumerable<Job> RunInternal(PgsqlContext context) protected override IEnumerable<Job> RunInternal(PgsqlContext context)
{ {
context.Attach(Manga);
context.Entry(Manga).Collection<Chapter>(m => m.Chapters).Load();
Dictionary<Chapter, string> oldPath = Manga.Chapters.ToDictionary(c => c, c => c.FullArchiveFilePath); Dictionary<Chapter, string> oldPath = Manga.Chapters.ToDictionary(c => c, c => c.FullArchiveFilePath);
Manga.Library = ToLibrary; Manga.Library = ToLibrary;
try try

View File

@ -1,6 +1,7 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using API.Schema.Contexts; using API.Schema.Contexts;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Newtonsoft.Json; using Newtonsoft.Json;
namespace API.Schema.Jobs; namespace API.Schema.Jobs;
@ -8,7 +9,15 @@ namespace API.Schema.Jobs;
public class RetrieveChaptersJob : Job public class RetrieveChaptersJob : Job
{ {
[StringLength(64)] [Required] public string MangaId { get; init; } [StringLength(64)] [Required] public string MangaId { get; init; }
[JsonIgnore] public Manga Manga { get; init; } = null!;
private Manga _manga = null!;
[JsonIgnore]
public Manga Manga
{
get => LazyLoader.Load(this, ref _manga);
init => _manga = value;
}
[StringLength(8)] [Required] public string Language { get; private set; } [StringLength(8)] [Required] public string Language { get; private set; }
public RetrieveChaptersJob(Manga manga, string language, ulong recurrenceMs, Job? parentJob = null, ICollection<Job>? dependsOnJobs = null) public RetrieveChaptersJob(Manga manga, string language, ulong recurrenceMs, Job? parentJob = null, ICollection<Job>? dependsOnJobs = null)
@ -22,8 +31,8 @@ public class RetrieveChaptersJob : Job
/// <summary> /// <summary>
/// EF ONLY!!! /// EF ONLY!!!
/// </summary> /// </summary>
internal RetrieveChaptersJob(string mangaId, string language, ulong recurrenceMs, string? parentJobId) internal RetrieveChaptersJob(ILazyLoader lazyLoader, string mangaId, string language, ulong recurrenceMs, string? parentJobId)
: base(TokenGen.CreateToken(typeof(RetrieveChaptersJob)), JobType.RetrieveChaptersJob, recurrenceMs, parentJobId) : base(lazyLoader, TokenGen.CreateToken(typeof(RetrieveChaptersJob)), JobType.RetrieveChaptersJob, recurrenceMs, parentJobId)
{ {
this.MangaId = mangaId; this.MangaId = mangaId;
this.Language = language; this.Language = language;
@ -31,10 +40,9 @@ public class RetrieveChaptersJob : Job
protected override IEnumerable<Job> RunInternal(PgsqlContext context) protected override IEnumerable<Job> RunInternal(PgsqlContext context)
{ {
context.Attach(Manga);
// This gets all chapters that are not downloaded // This gets all chapters that are not downloaded
Chapter[] allChapters = Manga.MangaConnector.GetChapters(Manga, Language); Chapter[] allChapters = Manga.MangaConnector.GetChapters(Manga, Language);
Chapter[] newChapters = allChapters.Where(chapter => context.Chapters.Contains(chapter) == false).ToArray(); Chapter[] newChapters = allChapters.Where(chapter => Manga.Chapters.Contains(chapter) == false).ToArray();
Log.Info($"{newChapters.Length} new chapters."); Log.Info($"{newChapters.Length} new chapters.");
try try

View File

@ -0,0 +1,41 @@
using System.ComponentModel.DataAnnotations;
using API.Schema.Contexts;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Newtonsoft.Json;
namespace API.Schema.Jobs;
public class UpdateChaptersDownloadedJob : Job
{
[StringLength(64)] [Required] public string MangaId { get; init; }
private Manga _manga = null!;
[JsonIgnore]
public Manga Manga
{
get => LazyLoader.Load(this, ref _manga);
init => _manga = value;
}
public UpdateChaptersDownloadedJob(Manga manga, ulong recurrenceMs, Job? parentJob = null, ICollection<Job>? dependsOnJobs = null)
: base(TokenGen.CreateToken(typeof(UpdateChaptersDownloadedJob)), JobType.UpdateChaptersDownloadedJob, recurrenceMs, parentJob, dependsOnJobs)
{
this.MangaId = manga.MangaId;
this.Manga = manga;
}
/// <summary>
/// EF ONLY!!!
/// </summary>
internal UpdateChaptersDownloadedJob(ILazyLoader lazyLoader, string mangaId, ulong recurrenceMs, string? parentJobId)
: base(lazyLoader, TokenGen.CreateToken(typeof(UpdateChaptersDownloadedJob)), JobType.UpdateChaptersDownloadedJob, recurrenceMs, parentJobId)
{
this.MangaId = mangaId;
}
protected override IEnumerable<Job> RunInternal(PgsqlContext context)
{
return Manga.Chapters.Select(c => new UpdateSingleChapterDownloadedJob(c, this));
}
}

View File

@ -1,46 +0,0 @@
using System.ComponentModel.DataAnnotations;
using API.Schema.Contexts;
using Microsoft.EntityFrameworkCore;
using Newtonsoft.Json;
namespace API.Schema.Jobs;
public class UpdateFilesDownloadedJob : Job
{
[StringLength(64)] [Required] public string MangaId { get; init; }
[JsonIgnore] public Manga Manga { get; init; } = null!;
public UpdateFilesDownloadedJob(Manga manga, ulong recurrenceMs, Job? parentJob = null, ICollection<Job>? dependsOnJobs = null)
: base(TokenGen.CreateToken(typeof(UpdateFilesDownloadedJob)), JobType.UpdateFilesDownloadedJob, recurrenceMs, parentJob, dependsOnJobs)
{
this.MangaId = manga.MangaId;
this.Manga = manga;
}
/// <summary>
/// EF ONLY!!!
/// </summary>
internal UpdateFilesDownloadedJob(string mangaId, ulong recurrenceMs, string? parentJobId)
: base(TokenGen.CreateToken(typeof(UpdateFilesDownloadedJob)), JobType.UpdateFilesDownloadedJob, recurrenceMs, parentJobId)
{
this.MangaId = mangaId;
}
protected override IEnumerable<Job> RunInternal(PgsqlContext context)
{
context.Attach(Manga);
context.Entry(Manga).Collection<Chapter>(m => m.Chapters).Load();
foreach (Chapter chapter in Manga.Chapters)
chapter.Downloaded = chapter.CheckDownloaded();
try
{
context.SaveChanges();
}
catch (DbUpdateException e)
{
Log.Error(e);
}
return [];
}
}

View File

@ -0,0 +1,52 @@
using System.ComponentModel.DataAnnotations;
using API.Schema.Contexts;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Newtonsoft.Json;
namespace API.Schema.Jobs;
public class UpdateSingleChapterDownloadedJob : Job
{
[StringLength(64)] [Required] public string ChapterId { get; init; }
private Chapter _chapter = null!;
[JsonIgnore]
public Chapter Chapter
{
get => LazyLoader.Load(this, ref _chapter);
init => _chapter = value;
}
public UpdateSingleChapterDownloadedJob(Chapter chapter, Job? parentJob = null, ICollection<Job>? dependsOnJobs = null)
: base(TokenGen.CreateToken(typeof(UpdateSingleChapterDownloadedJob)), JobType.UpdateSingleChapterDownloadedJob, 0, parentJob, dependsOnJobs)
{
this.ChapterId = chapter.ChapterId;
this.Chapter = chapter;
}
/// <summary>
/// EF ONLY!!!
/// </summary>
internal UpdateSingleChapterDownloadedJob(ILazyLoader lazyLoader, string chapterId, string? parentJobId)
: base(lazyLoader, TokenGen.CreateToken(typeof(UpdateSingleChapterDownloadedJob)), JobType.UpdateSingleChapterDownloadedJob, 0, parentJobId)
{
this.ChapterId = chapterId;
}
protected override IEnumerable<Job> RunInternal(PgsqlContext context)
{
Chapter.Downloaded = Chapter.CheckDownloaded();
try
{
context.SaveChanges();
}
catch (DbUpdateException e)
{
Log.Error(e);
}
return [];
}
}

View File

@ -4,6 +4,7 @@ using System.Runtime.InteropServices;
using System.Text; using System.Text;
using API.Schema.MangaConnectors; using API.Schema.MangaConnectors;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Newtonsoft.Json; using Newtonsoft.Json;
using static System.IO.UnixFileMode; using static System.IO.UnixFileMode;
@ -45,8 +46,16 @@ public class Manga
[JsonIgnore] [JsonIgnore]
[NotMapped] [NotMapped]
public string? FullDirectoryPath => Library is not null ? Path.Join(Library.BasePath, DirectoryName) : null; public string? FullDirectoryPath => Library is not null ? Path.Join(Library.BasePath, DirectoryName) : null;
[JsonIgnore] public ICollection<Chapter> Chapters { get; internal set; } = []; [NotMapped] public ICollection<string> ChapterIds => Chapters.Select(c => c.ChapterId).ToList();
private readonly ILazyLoader _lazyLoader = null!;
private ICollection<Chapter> _chapters = null!;
[JsonIgnore]
public ICollection<Chapter> Chapters
{
get => _lazyLoader.Load(this, ref _chapters);
init => _chapters = value;
}
public Manga(string idOnConnector, string name, string description, string websiteUrl, string coverUrl, MangaReleaseStatus releaseStatus, public Manga(string idOnConnector, string name, string description, string websiteUrl, string coverUrl, MangaReleaseStatus releaseStatus,
MangaConnector mangaConnector, ICollection<Author> authors, ICollection<MangaTag> mangaTags, ICollection<Link> links, ICollection<MangaAltTitle> altTitles, MangaConnector mangaConnector, ICollection<Author> authors, ICollection<MangaTag> mangaTags, ICollection<Link> links, ICollection<MangaAltTitle> altTitles,
@ -71,14 +80,16 @@ public class Manga
this.DirectoryName = CleanDirectoryName(name); this.DirectoryName = CleanDirectoryName(name);
this.Year = year; this.Year = year;
this.OriginalLanguage = originalLanguage; this.OriginalLanguage = originalLanguage;
this.Chapters = [];
} }
/// <summary> /// <summary>
/// EF ONLY!!! /// EF ONLY!!!
/// </summary> /// </summary>
public Manga(string mangaId, string idOnConnectorSite, string name, string description, string websiteUrl, string coverUrl, MangaReleaseStatus releaseStatus, public Manga(ILazyLoader lazyLoader, string mangaId, string idOnConnectorSite, string name, string description, string websiteUrl, string coverUrl, MangaReleaseStatus releaseStatus,
string mangaConnectorName, string directoryName, float ignoreChaptersBefore, string? libraryId, uint? year, string? originalLanguage) string mangaConnectorName, string directoryName, float ignoreChaptersBefore, string? libraryId, uint? year, string? originalLanguage)
{ {
this._lazyLoader = lazyLoader;
this.MangaId = mangaId; this.MangaId = mangaId;
this.IdOnConnectorSite = idOnConnectorSite; this.IdOnConnectorSite = idOnConnectorSite;
this.Name = name; this.Name = name;
@ -97,7 +108,9 @@ public class Manga
public string CreatePublicationFolder() public string CreatePublicationFolder()
{ {
string publicationFolder = FullDirectoryPath; string? publicationFolder = FullDirectoryPath;
if (publicationFolder is null)
throw new DirectoryNotFoundException("Publication folder not found");
if(!Directory.Exists(publicationFolder)) if(!Directory.Exists(publicationFolder))
Directory.CreateDirectory(publicationFolder); Directory.CreateDirectory(publicationFolder);
if(RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) if(RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
@ -131,7 +144,7 @@ public class Manga
StringBuilder sb = new (); StringBuilder sb = new ();
foreach (char c in name) foreach (char c in name)
{ {
if (c > 32 && c < 127 && ForbiddenCharsBelow127.Contains(c) == false) if (c >= 32 && c < 127 && ForbiddenCharsBelow127.Contains(c) == false)
sb.Append(c); sb.Append(c);
else if (c > 127 && c < 152 && IncludeCharsAbove127.Contains(c)) else if (c > 127 && c < 152 && IncludeCharsAbove127.Contains(c))
sb.Append(c); sb.Append(c);

View File

@ -5,7 +5,7 @@ namespace API;
public static class TokenGen public static class TokenGen
{ {
private const int MinimumLength = 32; private const int MinimumLength = 16;
private const int MaximumLength = 64; private const int MaximumLength = 64;
private const string Chars = "abcdefghijklmnopqrstuvwxyz0123456789"; private const string Chars = "abcdefghijklmnopqrstuvwxyz0123456789";

View File

@ -12,6 +12,16 @@ namespace API;
public static class Tranga public static class Tranga
{ {
// ReSharper disable once InconsistentNaming
private const string TRANGA =
"\n\n" +
" _______ v2\n" +
"|_ _|.----..---.-..-----..-----..---.-.\n" +
" | | | _|| _ || || _ || _ |\n" +
" |___| |__| |___._||__|__||___ ||___._|\n" +
" |_____| \n\n";
public static Thread NotificationSenderThread { get; } = new (NotificationSender); public static Thread NotificationSenderThread { get; } = new (NotificationSender);
public static Thread JobStarterThread { get; } = new (JobStarter); public static Thread JobStarterThread { get; } = new (JobStarter);
private static readonly ILog Log = LogManager.GetLogger(typeof(Tranga)); private static readonly ILog Log = LogManager.GetLogger(typeof(Tranga));
@ -20,6 +30,7 @@ public static class Tranga
{ {
BasicConfigurator.Configure(); BasicConfigurator.Configure();
Log.Info("Logger Configured."); Log.Info("Logger Configured.");
Log.Info(TRANGA);
} }
private static void NotificationSender(object? serviceProviderObj) private static void NotificationSender(object? serviceProviderObj)
@ -82,17 +93,10 @@ public static class Tranga
Log.Error("Error sending notifications.", e); Log.Error("Error sending notifications.", e);
} }
} }
private const string TRANGA =
"\n\n" +
" _______ \n" +
"|_ _|.----..---.-..-----..-----..---.-.\n" +
" | | | _|| _ || || _ || _ |\n" +
" |___| |__| |___._||__|__||___ ||___._|\n" +
" |_____| \n\n";
private static readonly Dictionary<Thread, Job> RunningJobs = new(); private static readonly Dictionary<Thread, Job> RunningJobs = new();
private static void JobStarter(object? serviceProviderObj) private static void JobStarter(object? serviceProviderObj)
{ {
Log.Info("JobStarter Thread running.");
if (serviceProviderObj is null) if (serviceProviderObj is null)
{ {
Log.Error("serviceProviderObj is null"); Log.Error("serviceProviderObj is null");
@ -100,23 +104,20 @@ public static class Tranga
} }
IServiceProvider serviceProvider = (IServiceProvider)serviceProviderObj; IServiceProvider serviceProvider = (IServiceProvider)serviceProviderObj;
using IServiceScope scope = serviceProvider.CreateScope(); using IServiceScope scope = serviceProvider.CreateScope();
PgsqlContext? context = scope.ServiceProvider.GetService<PgsqlContext>(); PgsqlContext context = scope.ServiceProvider.GetRequiredService<PgsqlContext>();
if (context is null)
{
Log.Error("PgsqlContext is null");
return;
}
Log.Info(TRANGA);
Log.Info("Loading Jobs");
context.Jobs.Load();
Log.Info("JobStarter Thread running.");
while (true) while (true)
{ {
Log.Debug("Starting Job-Cycle...");
DateTime cycleStart = DateTime.UtcNow;
Log.Debug("Loading Jobs...");
DateTime loadStart = DateTime.UtcNow;
context.Jobs.Load();
Log.Debug("Updating Entries...");
foreach (EntityEntry entityEntry in context.ChangeTracker.Entries().ToArray()) foreach (EntityEntry entityEntry in context.ChangeTracker.Entries().ToArray())
entityEntry.Reload(); entityEntry.Reload();
Log.Debug($"Jobs Loaded! (took {DateTime.UtcNow.Subtract(loadStart).TotalMilliseconds}ms)");
//Update finished Jobs to new states //Update finished Jobs to new states
context.Jobs.Load();
List<Job> completedJobs = context.Jobs.Local.Where(j => j.state == JobState.Completed).ToList(); List<Job> completedJobs = context.Jobs.Local.Where(j => j.state == JobState.Completed).ToList();
foreach (Job completedJob in completedJobs) foreach (Job completedJob in completedJobs)
if (completedJob.RecurrenceMs <= 0) if (completedJob.RecurrenceMs <= 0)
@ -136,6 +137,8 @@ public static class Tranga
//Retrieve waiting and due Jobs //Retrieve waiting and due Jobs
List<Job> runningJobs = context.Jobs.Local.Where(j => j.state == JobState.Running).ToList(); List<Job> runningJobs = context.Jobs.Local.Where(j => j.state == JobState.Running).ToList();
DateTime filterStart = DateTime.UtcNow;
Log.Debug("Filtering Jobs...");
List<MangaConnector> busyConnectors = GetBusyConnectors(runningJobs); List<MangaConnector> busyConnectors = GetBusyConnectors(runningJobs);
List<Job> waitingJobs = GetWaitingJobs(context.Jobs.Local.ToList()); List<Job> waitingJobs = GetWaitingJobs(context.Jobs.Local.ToList());
@ -146,6 +149,7 @@ public static class Tranga
List<Job> jobsWithoutDownloading = List<Job> jobsWithoutDownloading =
jobsWithoutMissingDependencies jobsWithoutMissingDependencies
.Where(j => j.JobType != JobType.DownloadSingleChapterJob) .Where(j => j.JobType != JobType.DownloadSingleChapterJob)
.DistinctBy(j => j.JobType)
.ToList(); .ToList();
List<Job> firstChapterPerConnector = List<Job> firstChapterPerConnector =
jobsWithoutMissingDependencies jobsWithoutMissingDependencies
@ -163,6 +167,8 @@ public static class Tranga
.ToList(); .ToList();
List<Job> startJobs = jobsWithoutDownloading.Concat(firstChapterPerConnector).ToList(); List<Job> startJobs = jobsWithoutDownloading.Concat(firstChapterPerConnector).ToList();
Log.Debug($"Jobs Filtered! (took {DateTime.UtcNow.Subtract(filterStart).TotalMilliseconds}ms)");
//Start Jobs that are allowed to run (preconditions match) //Start Jobs that are allowed to run (preconditions match)
foreach (Job job in startJobs) foreach (Job job in startJobs)
@ -195,6 +201,7 @@ public static class Tranga
{ {
Log.Error("Failed saving Job changes.", e); Log.Error("Failed saving Job changes.", e);
} }
Log.Debug($"Job-Cycle over! (took {DateTime.UtcNow.Subtract(cycleStart).TotalMilliseconds}ms)");
Thread.Sleep(TrangaSettings.startNewJobTimeoutMs); Thread.Sleep(TrangaSettings.startNewJobTimeoutMs);
} }
} }