Compare commits

...

23 Commits

Author SHA1 Message Date
02ab3d8cae UpdatecoverJob Migrations
Some checks failed
Docker Image CI / build (push) Has been cancelled
2025-05-18 18:17:41 +02:00
7974c58fd5 Fix PgsqlContext Discriminator UpdateCoverJob 2025-05-18 18:16:17 +02:00
503d9dfb5f Fix Name UpdateCoverJob 2025-05-18 17:55:37 +02:00
62b035f6c5 GET Mangaconnector endpoint 2025-05-18 17:20:53 +02:00
40c70fbf19 Update readme 2025-05-18 17:15:53 +02:00
49bd66ccab Add UpdateCoverJob.cs
Covers get updated on every pull
If a Manga has no DownloadAvailableChaptersJob, Cover is removed
Add endpoint POST Settings/CleanupCovers that removed covers not associated to any Manga
2025-05-18 17:05:01 +02:00
9b251169a5 Remove old covers from ImageCache 2025-05-18 16:54:53 +02:00
aa29c45094 Do not regenerate JobIds in EF Constructor
(and pass down recurrenceTime regardless of usage)
2025-05-18 16:53:42 +02:00
bd60fda05a Chapters now have IdOnConnector-Site 2025-05-18 16:30:03 +02:00
8ecbdb91b2 Let Job update itself in its own context 2025-05-18 16:06:52 +02:00
cb1c68f295 Remove Job.DependenciesFulfilled 2025-05-18 16:06:39 +02:00
421a25ec31 Delete duplicate IsRequired Statements 2025-05-18 16:06:16 +02:00
2d122a918f Create a Context per cycle
Load each Job in a separate context per Job.
2025-05-18 16:06:00 +02:00
100cb06ba0 SearchController.cs Local-Search endpoint 2025-05-18 15:32:58 +02:00
6125b036bf SearchController.cs Local-Search endpoint 2025-05-18 15:31:11 +02:00
3fe3fc09b0 JobContext per Job 2025-05-18 15:21:59 +02:00
96d5b09391 Ony load necessary References and Collections 2025-05-18 15:16:55 +02:00
84aecda916 NoTrackingWithIdentityResolution 2025-05-18 15:00:33 +02:00
0803a92a66 UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking) 2025-05-18 14:52:06 +02:00
7f55aaf85d test 2025-05-18 14:41:43 +02:00
3853e2daa2 Chapter.cs remove comparison again and instead check chapterids in RetrieveChaptersJob.cs 2025-05-18 14:28:07 +02:00
852fbf5ae8 Chapter.cs Compare Ids for Collection-Comparisons 2025-05-18 14:05:03 +02:00
4e7a725fee Load entry references and collections 2025-05-18 13:53:23 +02:00
27 changed files with 1830 additions and 109 deletions

View File

@ -137,9 +137,10 @@ public class JobController(PgsqlContext context, ILog Log) : Controller
Job updateFilesDownloaded = Job updateFilesDownloaded =
new UpdateChaptersDownloadedJob(m, record.recurrenceTimeMs, dependsOnJobs: [retrieveChapters]); new UpdateChaptersDownloadedJob(m, record.recurrenceTimeMs, dependsOnJobs: [retrieveChapters]);
Job downloadChapters = new DownloadAvailableChaptersJob(m, record.recurrenceTimeMs, dependsOnJobs: [retrieveChapters, updateFilesDownloaded]); Job downloadChapters = new DownloadAvailableChaptersJob(m, record.recurrenceTimeMs, dependsOnJobs: [retrieveChapters, updateFilesDownloaded]);
Job UpdateCover = new UpdateCoverJob(m, record.recurrenceTimeMs, downloadChapters);
retrieveChapters.ParentJob = downloadChapters; retrieveChapters.ParentJob = downloadChapters;
updateFilesDownloaded.ParentJob = retrieveChapters; updateFilesDownloaded.ParentJob = retrieveChapters;
return AddJobs([retrieveChapters, downloadChapters, updateFilesDownloaded]); return AddJobs([retrieveChapters, downloadChapters, updateFilesDownloaded, UpdateCover]);
} }
/// <summary> /// <summary>

View File

@ -23,6 +23,31 @@ public class MangaConnectorController(PgsqlContext context, ILog Log) : Controll
MangaConnector[] connectors = context.MangaConnectors.ToArray(); MangaConnector[] connectors = context.MangaConnectors.ToArray();
return Ok(connectors); return Ok(connectors);
} }
/// <summary>
/// Returns the MangaConnector with the requested Name
/// </summary>
/// <param name="MangaConnectorName"></param>
/// <response code="200"></response>
/// <response code="404">Connector with ID not found.</response>
/// <response code="500">Error during Database Operation</response>
[HttpGet("{MangaConnectorName}")]
[ProducesResponseType<MangaConnector>(Status200OK, "application/json")]
public IActionResult GetConnector(string MangaConnectorName)
{
try
{
if(context.MangaConnectors.Find(MangaConnectorName) is not { } connector)
return NotFound();
return Ok(connector);
}
catch (Exception e)
{
Log.Error(e);
return StatusCode(500, e.Message);
}
}
/// <summary> /// <summary>
/// Get all enabled Connectors (Scanlation-Sites) /// Get all enabled Connectors (Scanlation-Sites)

View File

@ -6,6 +6,7 @@ using Asp.Versioning;
using log4net; using log4net;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Soenneker.Utils.String.NeedlemanWunsch;
using static Microsoft.AspNetCore.Http.StatusCodes; using static Microsoft.AspNetCore.Http.StatusCodes;
// ReSharper disable InconsistentNaming // ReSharper disable InconsistentNaming
@ -55,6 +56,21 @@ public class SearchController(PgsqlContext context, ILog Log) : Controller
return Ok(retMangas.ToArray()); return Ok(retMangas.ToArray());
} }
/// <summary>
/// Search for a known Manga
/// </summary>
/// <param name="Query"></param>
/// <response code="200"></response>
[HttpGet("Local/{Query}")]
[ProducesResponseType<Manga[]>(Status200OK, "application/json")]
public IActionResult SearchMangaLocally(string Query)
{
Dictionary<Manga, double> distance = context.Mangas
.ToArray()
.ToDictionary(m => m, m => NeedlemanWunschStringUtil.CalculateSimilarityPercentage(Query, m.Name));
return Ok(distance.Where(kv => kv.Value > 50).OrderByDescending(kv => kv.Value).Select(kv => kv.Key).ToArray());
}
/// <summary> /// <summary>
/// Returns Manga from MangaConnector associated with URL /// Returns Manga from MangaConnector associated with URL

View File

@ -267,4 +267,28 @@ public class SettingsController(PgsqlContext context, ILog Log) : Controller
return StatusCode(500, e); return StatusCode(500, e);
} }
} }
/// <summary>
/// Creates a UpdateCoverJob for all Manga
/// </summary>
/// <response code="200">Array of JobIds</response>
/// <response code="500">Error during Database Operation</response>
[HttpPost("CleanupCovers")]
[ProducesResponseType<string[]>(Status200OK)]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public IActionResult CleanupCovers()
{
try
{
Tranga.RemoveStaleFiles(context);
List<UpdateCoverJob> newJobs = context.Mangas.ToList().Select(m => new UpdateCoverJob(m, 0)).ToList();
context.Jobs.AddRange(newJobs);
return Ok(newJobs.Select(j => j.JobId));
}
catch (Exception e)
{
Log.Error(e);
return StatusCode(500, e);
}
}
} }

View File

@ -0,0 +1,724 @@
// <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("20250518142903_Chapter-IdOnConnectorSite")]
partial class ChapterIdOnConnectorSite
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "9.0.5")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("API.Schema.Author", b =>
{
b.Property<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>("IdOnConnectorSite")
.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,29 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace API.Migrations.pgsql
{
/// <inheritdoc />
public partial class ChapterIdOnConnectorSite : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "IdOnConnectorSite",
table: "Chapters",
type: "character varying(256)",
maxLength: 256,
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "IdOnConnectorSite",
table: "Chapters");
}
}
}

View File

@ -0,0 +1,755 @@
// <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("20250518161710_UpdateCoverJob")]
partial class UpdateCoverJob
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "9.0.5")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("API.Schema.Author", b =>
{
b.Property<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>("IdOnConnectorSite")
.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.UpdateCoverJob", 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("UpdateCoverJob_MangaId");
});
b.HasDiscriminator().HasValue((byte)9);
});
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.UpdateCoverJob", 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,50 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace API.Migrations.pgsql
{
/// <inheritdoc />
public partial class UpdateCoverJob : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "UpdateCoverJob_MangaId",
table: "Jobs",
type: "character varying(64)",
maxLength: 64,
nullable: true);
migrationBuilder.CreateIndex(
name: "IX_Jobs_UpdateCoverJob_MangaId",
table: "Jobs",
column: "UpdateCoverJob_MangaId");
migrationBuilder.AddForeignKey(
name: "FK_Jobs_Mangas_UpdateCoverJob_MangaId",
table: "Jobs",
column: "UpdateCoverJob_MangaId",
principalTable: "Mangas",
principalColumn: "MangaId",
onDelete: ReferentialAction.Cascade);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_Jobs_Mangas_UpdateCoverJob_MangaId",
table: "Jobs");
migrationBuilder.DropIndex(
name: "IX_Jobs_UpdateCoverJob_MangaId",
table: "Jobs");
migrationBuilder.DropColumn(
name: "UpdateCoverJob_MangaId",
table: "Jobs");
}
}
}

View File

@ -17,7 +17,7 @@ namespace API.Migrations.pgsql
{ {
#pragma warning disable 612, 618 #pragma warning disable 612, 618
modelBuilder modelBuilder
.HasAnnotation("ProductVersion", "9.0.3") .HasAnnotation("ProductVersion", "9.0.5")
.HasAnnotation("Relational:MaxIdentifierLength", 63); .HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
@ -57,6 +57,10 @@ namespace API.Migrations.pgsql
.HasMaxLength(256) .HasMaxLength(256)
.HasColumnType("character varying(256)"); .HasColumnType("character varying(256)");
b.Property<string>("IdOnConnectorSite")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("ParentMangaId") b.Property<string>("ParentMangaId")
.IsRequired() .IsRequired()
.HasColumnType("character varying(64)"); .HasColumnType("character varying(64)");
@ -433,6 +437,26 @@ namespace API.Migrations.pgsql
b.HasDiscriminator().HasValue((byte)6); b.HasDiscriminator().HasValue((byte)6);
}); });
modelBuilder.Entity("API.Schema.Jobs.UpdateCoverJob", 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("UpdateCoverJob_MangaId");
});
b.HasDiscriminator().HasValue((byte)9);
});
modelBuilder.Entity("API.Schema.Jobs.UpdateSingleChapterDownloadedJob", b => modelBuilder.Entity("API.Schema.Jobs.UpdateSingleChapterDownloadedJob", b =>
{ {
b.HasBaseType("API.Schema.Jobs.Job"); b.HasBaseType("API.Schema.Jobs.Job");
@ -696,6 +720,17 @@ namespace API.Migrations.pgsql
b.Navigation("Manga"); b.Navigation("Manga");
}); });
modelBuilder.Entity("API.Schema.Jobs.UpdateCoverJob", b =>
{
b.HasOne("API.Schema.Manga", "Manga")
.WithMany()
.HasForeignKey("MangaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Manga");
});
modelBuilder.Entity("API.Schema.Jobs.UpdateSingleChapterDownloadedJob", b => modelBuilder.Entity("API.Schema.Jobs.UpdateSingleChapterDownloadedJob", b =>
{ {
b.HasOne("API.Schema.Chapter", "Chapter") b.HasOne("API.Schema.Chapter", "Chapter")

View File

@ -149,6 +149,12 @@ using (IServiceScope scope = app.Services.CreateScope())
TrangaSettings.Load(); TrangaSettings.Load();
Tranga.StartLogger(); Tranga.StartLogger();
using (IServiceScope scope = app.Services.CreateScope())
{
PgsqlContext context = scope.ServiceProvider.GetRequiredService<PgsqlContext>();
Tranga.RemoveStaleFiles(context);
}
Tranga.JobStarterThread.Start(app.Services); Tranga.JobStarterThread.Start(app.Services);
//Tranga.NotificationSenderThread.Start(app.Services); //TODO RE-ENABLE //Tranga.NotificationSenderThread.Start(app.Services); //TODO RE-ENABLE

View File

@ -13,6 +13,7 @@ public class Chapter : IComparable<Chapter>
{ {
[StringLength(64)] [Required] public string ChapterId { get; init; } [StringLength(64)] [Required] public string ChapterId { get; init; }
[StringLength(256)]public string? IdOnConnectorSite { get; init; }
public string ParentMangaId { get; init; } public string ParentMangaId { get; init; }
[JsonIgnore] public Manga ParentManga { get; init; } = null!; [JsonIgnore] public Manga ParentManga { get; init; } = null!;
@ -28,9 +29,10 @@ public class Chapter : IComparable<Chapter>
[Required] public bool Downloaded { get; internal set; } [Required] public bool Downloaded { get; internal set; }
[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? idOnConnectorSite = null, string? title = null)
{ {
this.ChapterId = TokenGen.CreateToken(typeof(Chapter), parentManga.MangaId, chapterNumber); this.ChapterId = TokenGen.CreateToken(typeof(Chapter), parentManga.MangaId, chapterNumber);
this.IdOnConnectorSite = idOnConnectorSite;
this.ParentMangaId = parentManga.MangaId; this.ParentMangaId = parentManga.MangaId;
this.ParentManga = parentManga; this.ParentManga = parentManga;
this.VolumeNumber = volumeNumber; this.VolumeNumber = volumeNumber;
@ -44,9 +46,10 @@ public class Chapter : IComparable<Chapter>
/// <summary> /// <summary>
/// EF ONLY!!! /// EF ONLY!!!
/// </summary> /// </summary>
internal Chapter(string chapterId, string parentMangaId, int? volumeNumber, string chapterNumber, string url, string? title, string fileName, bool downloaded) internal Chapter(string chapterId, string parentMangaId, int? volumeNumber, string chapterNumber, string url, string? idOnConnectorSite, string? title, string fileName, bool downloaded)
{ {
this.ChapterId = chapterId; this.ChapterId = chapterId;
this.IdOnConnectorSite = idOnConnectorSite;
this.ParentMangaId = parentMangaId; this.ParentMangaId = parentMangaId;
this.VolumeNumber = volumeNumber; this.VolumeNumber = volumeNumber;
this.ChapterNumber = chapterNumber; this.ChapterNumber = chapterNumber;

View File

@ -38,6 +38,7 @@ 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<UpdateCoverJob>(JobType.UpdateCoverJob)
.HasValue<UpdateChaptersDownloadedJob>(JobType.UpdateChaptersDownloadedJob) .HasValue<UpdateChaptersDownloadedJob>(JobType.UpdateChaptersDownloadedJob)
.HasValue<UpdateSingleChapterDownloadedJob>(JobType.UpdateSingleChapterDownloadedJob); .HasValue<UpdateSingleChapterDownloadedJob>(JobType.UpdateSingleChapterDownloadedJob);
@ -46,7 +47,6 @@ public class PgsqlContext(DbContextOptions<PgsqlContext> options) : DbContext(op
.HasOne<Manga>(j => j.Manga) .HasOne<Manga>(j => j.Manga)
.WithMany() .WithMany()
.HasForeignKey(j => j.MangaId) .HasForeignKey(j => j.MangaId)
.IsRequired()
.OnDelete(DeleteBehavior.Cascade); .OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<DownloadAvailableChaptersJob>() modelBuilder.Entity<DownloadAvailableChaptersJob>()
.Navigation(j => j.Manga) .Navigation(j => j.Manga)
@ -55,7 +55,6 @@ public class PgsqlContext(DbContextOptions<PgsqlContext> options) : DbContext(op
.HasOne<Manga>(j => j.Manga) .HasOne<Manga>(j => j.Manga)
.WithMany() .WithMany()
.HasForeignKey(j => j.MangaId) .HasForeignKey(j => j.MangaId)
.IsRequired()
.OnDelete(DeleteBehavior.Cascade); .OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<DownloadMangaCoverJob>() modelBuilder.Entity<DownloadMangaCoverJob>()
.Navigation(j => j.Manga) .Navigation(j => j.Manga)
@ -64,7 +63,6 @@ public class PgsqlContext(DbContextOptions<PgsqlContext> options) : DbContext(op
.HasOne<Chapter>(j => j.Chapter) .HasOne<Chapter>(j => j.Chapter)
.WithMany() .WithMany()
.HasForeignKey(j => j.ChapterId) .HasForeignKey(j => j.ChapterId)
.IsRequired()
.OnDelete(DeleteBehavior.Cascade); .OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<DownloadSingleChapterJob>() modelBuilder.Entity<DownloadSingleChapterJob>()
.Navigation(j => j.Chapter) .Navigation(j => j.Chapter)
@ -73,7 +71,6 @@ public class PgsqlContext(DbContextOptions<PgsqlContext> options) : DbContext(op
.HasOne<Manga>(j => j.Manga) .HasOne<Manga>(j => j.Manga)
.WithMany() .WithMany()
.HasForeignKey(j => j.MangaId) .HasForeignKey(j => j.MangaId)
.IsRequired()
.OnDelete(DeleteBehavior.Cascade); .OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<MoveMangaLibraryJob>() modelBuilder.Entity<MoveMangaLibraryJob>()
.Navigation(j => j.Manga) .Navigation(j => j.Manga)
@ -82,7 +79,6 @@ public class PgsqlContext(DbContextOptions<PgsqlContext> options) : DbContext(op
.HasOne<LocalLibrary>(j => j.ToLibrary) .HasOne<LocalLibrary>(j => j.ToLibrary)
.WithMany() .WithMany()
.HasForeignKey(j => j.ToLibraryId) .HasForeignKey(j => j.ToLibraryId)
.IsRequired()
.OnDelete(DeleteBehavior.Cascade); .OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<MoveMangaLibraryJob>() modelBuilder.Entity<MoveMangaLibraryJob>()
.Navigation(j => j.ToLibrary) .Navigation(j => j.ToLibrary)
@ -91,7 +87,6 @@ public class PgsqlContext(DbContextOptions<PgsqlContext> options) : DbContext(op
.HasOne<Manga>(j => j.Manga) .HasOne<Manga>(j => j.Manga)
.WithMany() .WithMany()
.HasForeignKey(j => j.MangaId) .HasForeignKey(j => j.MangaId)
.IsRequired()
.OnDelete(DeleteBehavior.Cascade); .OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<RetrieveChaptersJob>() modelBuilder.Entity<RetrieveChaptersJob>()
.Navigation(j => j.Manga) .Navigation(j => j.Manga)
@ -100,7 +95,6 @@ public class PgsqlContext(DbContextOptions<PgsqlContext> options) : DbContext(op
.HasOne<Manga>(j => j.Manga) .HasOne<Manga>(j => j.Manga)
.WithMany() .WithMany()
.HasForeignKey(j => j.MangaId) .HasForeignKey(j => j.MangaId)
.IsRequired()
.OnDelete(DeleteBehavior.Cascade); .OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<UpdateChaptersDownloadedJob>() modelBuilder.Entity<UpdateChaptersDownloadedJob>()
.Navigation(j => j.Manga) .Navigation(j => j.Manga)
@ -132,7 +126,6 @@ public class PgsqlContext(DbContextOptions<PgsqlContext> options) : DbContext(op
.HasMany<Manga>() .HasMany<Manga>()
.WithOne(m => m.MangaConnector) .WithOne(m => m.MangaConnector)
.HasForeignKey(m => m.MangaConnectorName) .HasForeignKey(m => m.MangaConnectorName)
.IsRequired()
.OnDelete(DeleteBehavior.Cascade); .OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<Manga>() modelBuilder.Entity<Manga>()
.Navigation(m => m.MangaConnector) .Navigation(m => m.MangaConnector)
@ -143,7 +136,6 @@ public class PgsqlContext(DbContextOptions<PgsqlContext> options) : DbContext(op
.HasMany<Chapter>(m => m.Chapters) .HasMany<Chapter>(m => m.Chapters)
.WithOne(c => c.ParentManga) .WithOne(c => c.ParentManga)
.HasForeignKey(c => c.ParentMangaId) .HasForeignKey(c => c.ParentMangaId)
.IsRequired()
.OnDelete(DeleteBehavior.Cascade); .OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<Chapter>() modelBuilder.Entity<Chapter>()
.Navigation(c => c.ParentManga) .Navigation(c => c.ParentManga)

View File

@ -28,14 +28,15 @@ public class DownloadAvailableChaptersJob : Job
/// <summary> /// <summary>
/// EF ONLY!!! /// EF ONLY!!!
/// </summary> /// </summary>
internal DownloadAvailableChaptersJob(ILazyLoader lazyLoader, string mangaId, ulong recurrenceMs, string? parentJobId) internal DownloadAvailableChaptersJob(ILazyLoader lazyLoader, string jobId, ulong recurrenceMs, string mangaId, string? parentJobId)
: base(lazyLoader, TokenGen.CreateToken(typeof(DownloadAvailableChaptersJob)), JobType.DownloadAvailableChaptersJob, recurrenceMs, parentJobId) : base(lazyLoader, jobId, 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.Entry(Manga).Reference<LocalLibrary>(m => m.Library).Load();
return Manga.Chapters.Where(c => c.Downloaded == false).Select(chapter => new DownloadSingleChapterJob(chapter, this)); return Manga.Chapters.Where(c => c.Downloaded == false).Select(chapter => new DownloadSingleChapterJob(chapter, this));
} }
} }

View File

@ -29,8 +29,8 @@ public class DownloadMangaCoverJob : Job
/// <summary> /// <summary>
/// EF ONLY!!! /// EF ONLY!!!
/// </summary> /// </summary>
internal DownloadMangaCoverJob(ILazyLoader lazyLoader, string mangaId, string? parentJobId) internal DownloadMangaCoverJob(ILazyLoader lazyLoader, string jobId, ulong recurrenceMs, string mangaId, string? parentJobId)
: base(lazyLoader, TokenGen.CreateToken(typeof(DownloadMangaCoverJob)), JobType.DownloadMangaCoverJob, 0, parentJobId) : base(lazyLoader, jobId, JobType.DownloadMangaCoverJob, recurrenceMs, parentJobId)
{ {
this.MangaId = mangaId; this.MangaId = mangaId;
} }

View File

@ -37,8 +37,8 @@ public class DownloadSingleChapterJob : Job
/// <summary> /// <summary>
/// EF ONLY!!! /// EF ONLY!!!
/// </summary> /// </summary>
internal DownloadSingleChapterJob(ILazyLoader lazyLoader, string chapterId, string? parentJobId) internal DownloadSingleChapterJob(ILazyLoader lazyLoader, string jobId, ulong recurrenceMs, string chapterId, string? parentJobId)
: base(lazyLoader, TokenGen.CreateToken(typeof(DownloadSingleChapterJob)), JobType.DownloadSingleChapterJob, 0, parentJobId) : base(lazyLoader, jobId, JobType.DownloadSingleChapterJob, recurrenceMs, parentJobId)
{ {
this.ChapterId = chapterId; this.ChapterId = chapterId;
} }

View File

@ -35,7 +35,6 @@ public abstract class Job
[Required] public bool Enabled { get; internal set; } = true; [Required] public bool Enabled { get; internal set; } = true;
[JsonIgnore] [NotMapped] internal bool IsCompleted => state is >= (JobState)128 and < (JobState)192; [JsonIgnore] [NotMapped] internal bool IsCompleted => state is >= (JobState)128 and < (JobState)192;
[JsonIgnore] [NotMapped] internal bool DependenciesFulfilled => DependsOnJobs.All(j => j.IsCompleted);
[NotMapped] [JsonIgnore] protected ILog Log { get; init; } [NotMapped] [JsonIgnore] protected ILog Log { get; init; }
[NotMapped] [JsonIgnore] protected ILazyLoader LazyLoader { get; init; } [NotMapped] [JsonIgnore] protected ILazyLoader LazyLoader { get; init; }
@ -67,21 +66,20 @@ public abstract class Job
this.Log = LogManager.GetLogger(this.GetType()); this.Log = LogManager.GetLogger(this.GetType());
} }
public IEnumerable<Job> Run(IServiceProvider serviceProvider) public IEnumerable<Job> Run(PgsqlContext context)
{ {
Log.Info($"Running job {JobId}"); Log.Info($"Running job {JobId}");
DateTime jobStart = DateTime.UtcNow; DateTime jobStart = DateTime.UtcNow;
context.Attach(this);
Job[]? ret = null; Job[]? ret = null;
using IServiceScope scope = serviceProvider.CreateScope();
PgsqlContext context = scope.ServiceProvider.GetRequiredService<PgsqlContext>();
try try
{ {
context.Attach(this);
this.state = JobState.Running; this.state = JobState.Running;
context.SaveChanges(); context.SaveChanges();
ret = RunInternal(context).ToArray(); ret = RunInternal(context).ToArray();
this.state = JobState.Completed; this.state = this.RecurrenceMs > 0 ? JobState.CompletedWaiting : JobState.Completed;
this.LastExecution = DateTime.UtcNow;
context.Jobs.AddRange(ret); context.Jobs.AddRange(ret);
Log.Info($"Job {JobId} completed. Generated {ret.Length} new jobs."); Log.Info($"Job {JobId} completed. Generated {ret.Length} new jobs.");
context.SaveChanges(); context.SaveChanges();
@ -91,6 +89,8 @@ public abstract class Job
if (e is not DbUpdateException) if (e is not DbUpdateException)
{ {
this.state = JobState.Failed; this.state = JobState.Failed;
this.Enabled = false;
this.LastExecution = DateTime.UtcNow;
Log.Error($"Failed to run job {JobId}", e); Log.Error($"Failed to run job {JobId}", e);
context.SaveChanges(); context.SaveChanges();
} }

View File

@ -12,4 +12,5 @@ public enum JobType : byte
UpdateChaptersDownloadedJob = 6, UpdateChaptersDownloadedJob = 6,
MoveMangaLibraryJob = 7, MoveMangaLibraryJob = 7,
UpdateSingleChapterDownloadedJob = 8, UpdateSingleChapterDownloadedJob = 8,
UpdateCoverJob = 9,
} }

View File

@ -23,8 +23,8 @@ public class MoveFileOrFolderJob : Job
/// <summary> /// <summary>
/// EF ONLY!!! /// EF ONLY!!!
/// </summary> /// </summary>
internal MoveFileOrFolderJob(ILazyLoader lazyLoader, string jobId, string fromLocation, string toLocation, string? parentJobId) internal MoveFileOrFolderJob(ILazyLoader lazyLoader, string jobId, ulong recurrenceMs, string fromLocation, string toLocation, string? parentJobId)
: base(lazyLoader, jobId, JobType.MoveFileOrFolderJob, 0, parentJobId) : base(lazyLoader, jobId, JobType.MoveFileOrFolderJob, recurrenceMs, parentJobId)
{ {
this.FromLocation = fromLocation; this.FromLocation = fromLocation;
this.ToLocation = toLocation; this.ToLocation = toLocation;

View File

@ -33,8 +33,8 @@ public class MoveMangaLibraryJob : Job
/// <summary> /// <summary>
/// EF ONLY!!! /// EF ONLY!!!
/// </summary> /// </summary>
internal MoveMangaLibraryJob(ILazyLoader lazyLoader, string mangaId, string toLibraryId, string? parentJobId) internal MoveMangaLibraryJob(ILazyLoader lazyLoader, string jobId, ulong recurrenceMs, string mangaId, string toLibraryId, string? parentJobId)
: base(lazyLoader, TokenGen.CreateToken(typeof(MoveMangaLibraryJob)), JobType.MoveMangaLibraryJob, 0, parentJobId) : base(lazyLoader, jobId, JobType.MoveMangaLibraryJob, recurrenceMs, parentJobId)
{ {
this.MangaId = mangaId; this.MangaId = mangaId;
this.ToLibraryId = toLibraryId; this.ToLibraryId = toLibraryId;
@ -42,6 +42,7 @@ public class MoveMangaLibraryJob : Job
protected override IEnumerable<Job> RunInternal(PgsqlContext context) protected override IEnumerable<Job> RunInternal(PgsqlContext context)
{ {
context.Entry(Manga).Reference<LocalLibrary>(m => m.Library).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

@ -31,8 +31,8 @@ public class RetrieveChaptersJob : Job
/// <summary> /// <summary>
/// EF ONLY!!! /// EF ONLY!!!
/// </summary> /// </summary>
internal RetrieveChaptersJob(ILazyLoader lazyLoader, string mangaId, string language, ulong recurrenceMs, string? parentJobId) internal RetrieveChaptersJob(ILazyLoader lazyLoader, string jobId, ulong recurrenceMs, string mangaId, string language, string? parentJobId)
: base(lazyLoader, TokenGen.CreateToken(typeof(RetrieveChaptersJob)), JobType.RetrieveChaptersJob, recurrenceMs, parentJobId) : base(lazyLoader, jobId, JobType.RetrieveChaptersJob, recurrenceMs, parentJobId)
{ {
this.MangaId = mangaId; this.MangaId = mangaId;
this.Language = language; this.Language = language;
@ -42,12 +42,13 @@ public class RetrieveChaptersJob : Job
{ {
// This gets all chapters that are not downloaded // This gets all chapters that are not downloaded
Chapter[] allChapters = Manga.MangaConnector.GetChapters(Manga, Language).DistinctBy(c => c.ChapterId).ToArray(); Chapter[] allChapters = Manga.MangaConnector.GetChapters(Manga, Language).DistinctBy(c => c.ChapterId).ToArray();
Chapter[] newChapters = allChapters.Where(chapter => Manga.Chapters.Contains(chapter) == false).ToArray(); Chapter[] newChapters = allChapters.Where(chapter => Manga.Chapters.Select(c => c.ChapterId).Contains(chapter.ChapterId) == false).ToArray();
Log.Info($"{newChapters.Length} new chapters."); Log.Info($"{Manga.Chapters.Count} existing + {newChapters.Length} new chapters.");
try try
{ {
context.Chapters.AddRange(newChapters); foreach (Chapter newChapter in newChapters)
Manga.Chapters.Add(newChapter);
context.SaveChanges(); context.SaveChanges();
} }
catch (DbUpdateException e) catch (DbUpdateException e)

View File

@ -28,8 +28,8 @@ public class UpdateChaptersDownloadedJob : Job
/// <summary> /// <summary>
/// EF ONLY!!! /// EF ONLY!!!
/// </summary> /// </summary>
internal UpdateChaptersDownloadedJob(ILazyLoader lazyLoader, string mangaId, ulong recurrenceMs, string? parentJobId) internal UpdateChaptersDownloadedJob(ILazyLoader lazyLoader, string jobId, ulong recurrenceMs, string mangaId, string? parentJobId)
: base(lazyLoader, TokenGen.CreateToken(typeof(UpdateChaptersDownloadedJob)), JobType.UpdateChaptersDownloadedJob, recurrenceMs, parentJobId) : base(lazyLoader, jobId, JobType.UpdateChaptersDownloadedJob, recurrenceMs, parentJobId)
{ {
this.MangaId = mangaId; this.MangaId = mangaId;
} }

View File

@ -0,0 +1,65 @@
using System.ComponentModel.DataAnnotations;
using API.Schema.Contexts;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Newtonsoft.Json;
namespace API.Schema.Jobs;
public class UpdateCoverJob : 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 UpdateCoverJob(Manga manga, ulong recurrenceMs, Job? parentJob = null, ICollection<Job>? dependsOnJobs = null)
: base(TokenGen.CreateToken(typeof(UpdateCoverJob)), JobType.UpdateCoverJob, recurrenceMs, parentJob, dependsOnJobs)
{
this.MangaId = manga.MangaId;
this.Manga = manga;
}
/// <summary>
/// EF ONLY!!!
/// </summary>
internal UpdateCoverJob(ILazyLoader lazyLoader, string jobId, ulong recurrenceMs, string mangaId, string? parentJobId)
: base(lazyLoader, jobId, JobType.UpdateCoverJob, recurrenceMs, parentJobId)
{
this.MangaId = mangaId;
}
protected override IEnumerable<Job> RunInternal(PgsqlContext context)
{
bool keepCover = context.Jobs
.Any(job => job.JobType == JobType.DownloadAvailableChaptersJob
&& ((DownloadAvailableChaptersJob)job).MangaId == MangaId);
if (!keepCover)
{
if(File.Exists(Manga.CoverFileNameInCache))
File.Delete(Manga.CoverFileNameInCache);
try
{
Manga.CoverFileNameInCache = null;
context.Jobs.Remove(this);
context.SaveChanges();
}
catch (DbUpdateException e)
{
Log.Error(e);
}
}
else
{
return [new DownloadMangaCoverJob(Manga, this)];
}
return [];
}
}

View File

@ -29,14 +29,16 @@ public class UpdateSingleChapterDownloadedJob : Job
/// <summary> /// <summary>
/// EF ONLY!!! /// EF ONLY!!!
/// </summary> /// </summary>
internal UpdateSingleChapterDownloadedJob(ILazyLoader lazyLoader, string chapterId, string? parentJobId) internal UpdateSingleChapterDownloadedJob(ILazyLoader lazyLoader, string jobId, ulong recurrenceMs, string chapterId, string? parentJobId)
: base(lazyLoader, TokenGen.CreateToken(typeof(UpdateSingleChapterDownloadedJob)), JobType.UpdateSingleChapterDownloadedJob, 0, parentJobId) : base(lazyLoader, jobId, JobType.UpdateSingleChapterDownloadedJob, recurrenceMs, parentJobId)
{ {
this.ChapterId = chapterId; this.ChapterId = chapterId;
} }
protected override IEnumerable<Job> RunInternal(PgsqlContext context) protected override IEnumerable<Job> RunInternal(PgsqlContext context)
{ {
context.Entry(Chapter).Reference<Manga>(c => c.ParentManga).Load();
context.Entry(Chapter.ParentManga).Reference<LocalLibrary>(m => m.Library).Load();
Chapter.Downloaded = Chapter.CheckDownloaded(); Chapter.Downloaded = Chapter.CheckDownloaded();
try try

View File

@ -242,6 +242,6 @@ public class ComickIo : MangaConnector
throw new Exception("chapterNum is null"); throw new Exception("chapterNum is null");
string url = $"https://comick.io{canonical}"; string url = $"https://comick.io{canonical}";
return new Chapter(parentManga, url, chapterNum, volumeNum, title); return new Chapter(parentManga, url, chapterNum, volumeNum, hid, title);
} }
} }

View File

@ -330,6 +330,6 @@ public class MangaDex : MangaConnector
volume = int.Parse(volumeStr); volume = int.Parse(volumeStr);
string url = $"https://mangadex.org/chapter/{id}"; string url = $"https://mangadex.org/chapter/{id}";
return new Chapter(parentManga, url, chapter, volume, title); return new Chapter(parentManga, url, chapter, volume, id, title);
} }
} }

View File

@ -6,7 +6,6 @@ using API.Schema.NotificationConnectors;
using log4net; using log4net;
using log4net.Config; using log4net.Config;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.ChangeTracking;
namespace API; namespace API;
@ -33,6 +32,21 @@ public static class Tranga
Log.Info(TRANGA); Log.Info(TRANGA);
} }
internal static void RemoveStaleFiles(PgsqlContext context)
{
Log.Info($"Removing stale files...");
string[] usedFiles = context.Mangas.Select(m => m.CoverFileNameInCache).Where(s => s != null).ToArray()!;
string[] extraneousFiles = new DirectoryInfo(TrangaSettings.coverImageCache).GetFiles()
.Where(f => usedFiles.Contains(f.FullName) == false)
.Select(f => f.FullName)
.ToArray();
foreach (string path in extraneousFiles)
{
Log.Info($"Deleting {path}");
File.Delete(path);
}
}
private static void NotificationSender(object? serviceProviderObj) private static void NotificationSender(object? serviceProviderObj)
{ {
if (serviceProviderObj is null) if (serviceProviderObj is null)
@ -104,47 +118,34 @@ 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.GetRequiredService<PgsqlContext>();
while (true) while (true)
{ {
Log.Debug("Starting Job-Cycle..."); Log.Debug("Starting Job-Cycle...");
DateTime cycleStart = DateTime.UtcNow; DateTime cycleStart = DateTime.UtcNow;
PgsqlContext cycleContext = scope.ServiceProvider.GetRequiredService<PgsqlContext>();
Log.Debug("Loading Jobs..."); Log.Debug("Loading Jobs...");
DateTime loadStart = DateTime.UtcNow; DateTime loadStart = DateTime.UtcNow;
context.Jobs.Load();
Log.Debug("Updating Entries...");
foreach (EntityEntry entityEntry in context.ChangeTracker.Entries().ToArray())
entityEntry.Reload();
Log.Debug($"Jobs Loaded! (took {DateTime.UtcNow.Subtract(loadStart).TotalMilliseconds}ms)"); Log.Debug($"Jobs Loaded! (took {DateTime.UtcNow.Subtract(loadStart).TotalMilliseconds}ms)");
//Update finished Jobs to new states //Update finished Jobs to new states
List<Job> completedJobs = context.Jobs.Local.Where(j => j.state == JobState.Completed).ToList(); IQueryable<Job> completedJobs = cycleContext.Jobs.Where(j => j.state == JobState.Completed);
foreach (Job completedJob in completedJobs) foreach (Job completedJob in completedJobs)
if (completedJob.RecurrenceMs <= 0) if (completedJob.RecurrenceMs <= 0)
context.Jobs.Remove(completedJob);
else
{ {
completedJob.state = JobState.CompletedWaiting; cycleContext.Jobs.Remove(completedJob);
completedJob.LastExecution = DateTime.UtcNow;
} }
List<Job> failedJobs = context.Jobs.Local.Where(j => j.state == JobState.Failed).ToList();
foreach (Job failedJob in failedJobs)
{
failedJob.Enabled = false;
failedJob.LastExecution = DateTime.UtcNow;
}
//Retrieve waiting and due Jobs //Retrieve waiting and due Jobs
List<Job> runningJobs = context.Jobs.Local.Where(j => j.state == JobState.Running).ToList(); IQueryable<Job> runningJobs = cycleContext.Jobs.Where(j => j.state == JobState.Running);
DateTime filterStart = DateTime.UtcNow; DateTime filterStart = DateTime.UtcNow;
Log.Debug("Filtering Jobs..."); Log.Debug("Filtering Jobs...");
List<MangaConnector> busyConnectors = GetBusyConnectors(runningJobs); List<MangaConnector> busyConnectors = GetBusyConnectors(runningJobs);
List<Job> waitingJobs = GetWaitingJobs(context.Jobs.Local.ToList()); IQueryable<Job> waitingJobs = cycleContext.Jobs.Where(j => j.state == JobState.CompletedWaiting || j.state == JobState.FirstExecution);
List<Job> dueJobs = FilterDueJobs(waitingJobs); List<Job> dueJobs = FilterDueJobs(waitingJobs);
List<Job> jobsWithoutBusyConnectors = FilterJobWithBusyConnectors(dueJobs, busyConnectors); List<Job> jobsWithoutBusyConnectors = FilterJobWithBusyConnectors(dueJobs, busyConnectors);
List<Job> jobsWithoutMissingDependencies = FilterJobDependencies(context, jobsWithoutBusyConnectors); List<Job> jobsWithoutMissingDependencies = FilterJobDependencies(jobsWithoutBusyConnectors);
List<Job> jobsWithoutDownloading = List<Job> jobsWithoutDownloading =
jobsWithoutMissingDependencies jobsWithoutMissingDependencies
@ -154,6 +155,7 @@ public static class Tranga
List<Job> firstChapterPerConnector = List<Job> firstChapterPerConnector =
jobsWithoutMissingDependencies jobsWithoutMissingDependencies
.Where(j => j.JobType == JobType.DownloadSingleChapterJob) .Where(j => j.JobType == JobType.DownloadSingleChapterJob)
.AsEnumerable()
.OrderBy(j => .OrderBy(j =>
{ {
DownloadSingleChapterJob dscj = (DownloadSingleChapterJob)j; DownloadSingleChapterJob dscj = (DownloadSingleChapterJob)j;
@ -175,15 +177,18 @@ public static class Tranga
{ {
Thread t = new(() => Thread t = new(() =>
{ {
job.Run(serviceProvider); using IServiceScope jobScope = serviceProvider.CreateScope();
PgsqlContext jobContext = jobScope.ServiceProvider.GetRequiredService<PgsqlContext>();
jobContext.Jobs.Find(job.JobId)?.Run(jobContext); //FIND the job IN THE NEW CONTEXT!!!!!!! SO WE DON'T GET TRACKING PROBLEMS AND AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
}); });
RunningJobs.Add(t, job); RunningJobs.Add(t, job);
t.Start(); t.Start();
} }
Log.Debug($"Jobs Completed: {completedJobs.Count} Failed: {failedJobs.Count} Running: {runningJobs.Count}\n" + Log.Debug($"Jobs Completed: {completedJobs.Count()} Running: {runningJobs.Count()}\n" +
$"Waiting: {waitingJobs.Count}\n" + $"Waiting: {waitingJobs.Count()}\n" +
$"\tof which Due: {dueJobs.Count}\n" + $"\tof which Due: {dueJobs.Count()}\n" +
$"\t\tof which Started: {jobsWithoutMissingDependencies.Count}"); $"\t\tof which can be started: {jobsWithoutMissingDependencies.Count()}\n" +
$"\t\t\tof which started: {startJobs.Count()}");
(Thread, Job)[] removeFromThreadsList = RunningJobs.Where(t => !t.Key.IsAlive) (Thread, Job)[] removeFromThreadsList = RunningJobs.Where(t => !t.Key.IsAlive)
.Select(t => (t.Key, t.Value)).ToArray(); .Select(t => (t.Key, t.Value)).ToArray();
@ -195,7 +200,7 @@ public static class Tranga
try try
{ {
context.SaveChanges(); cycleContext.SaveChanges();
} }
catch (DbUpdateException e) catch (DbUpdateException e)
{ {
@ -206,7 +211,7 @@ public static class Tranga
} }
} }
private static List<MangaConnector> GetBusyConnectors(List<Job> runningJobs) private static List<MangaConnector> GetBusyConnectors(IQueryable<Job> runningJobs)
{ {
HashSet<MangaConnector> busyConnectors = new(); HashSet<MangaConnector> busyConnectors = new();
foreach (Job runningJob in runningJobs) foreach (Job runningJob in runningJobs)
@ -216,40 +221,25 @@ public static class Tranga
} }
return busyConnectors.ToList(); return busyConnectors.ToList();
} }
private static List<Job> GetWaitingJobs(List<Job> jobs) =>
jobs
.Where(j =>
j.Enabled &&
(j.state == JobState.FirstExecution || j.state == JobState.CompletedWaiting))
.ToList();
private static List<Job> FilterDueJobs(List<Job> jobs) => private static List<Job> FilterDueJobs(IQueryable<Job> jobs) =>
jobs jobs.ToList()
.Where(j => j.NextExecution < DateTime.UtcNow) .Where(j => j.NextExecution < DateTime.UtcNow)
.ToList(); .ToList();
private static List<Job> FilterJobDependencies(PgsqlContext context, List<Job> jobs) => private static List<Job> FilterJobDependencies(List<Job> jobs) =>
jobs jobs
.Where(j => .Where(job => job.DependsOnJobs.All(j => j.IsCompleted))
{
Log.Debug($"Loading Job Preconditions {j}...");
context.Entry(j).Collection(j => j.DependsOnJobs).Load();
Log.Debug($"Loaded Job Preconditions {j}!");
return j.DependenciesFulfilled;
})
.ToList(); .ToList();
private static List<Job> FilterJobWithBusyConnectors(List<Job> jobs, List<MangaConnector> busyConnectors) => private static List<Job> FilterJobWithBusyConnectors(List<Job> jobs, List<MangaConnector> busyConnectors) =>
jobs jobs.Where(j =>
.Where(j =>
{ {
//Filter jobs with busy connectors //Filter jobs with busy connectors
if (GetJobConnector(j) is { } mangaConnector) if (GetJobConnector(j) is { } mangaConnector)
return busyConnectors.Contains(mangaConnector) == false; return busyConnectors.Contains(mangaConnector) == false;
return true; return true;
}) }).ToList();
.ToList();
private static MangaConnector? GetJobConnector(Job job) private static MangaConnector? GetJobConnector(Job job)
{ {

View File

@ -31,14 +31,7 @@
Tranga can download Chapters and Metadata from "Scanlation" sites such as Tranga can download Chapters and Metadata from "Scanlation" sites such as
- [MangaDex.org](https://mangadex.org/) (Multilingual) - [MangaDex.org](https://mangadex.org/) (Multilingual)
- [Manganato.gg](https://manganato.com/) (en) (or natomanga.com, mangakakalot, nelomanga, ...) - [Comick.io](https://comick.io/)
- [MangaKatana.com](https://mangakatana.com) (en)
- [Mangaworld.bz](https://www.mangaworld.bz/) (it)
- [Bato.to](https://bato.to/v3x) (en)
- [ManhuaPlus](https://manhuaplus.org/) (en)
- [MangaHere](https://www.mangahere.cc/) (en)
- [Weebcentral](https://weebcentral.com) (en)
- [Webtoons](https://www.webtoons.com/en/) (en)
- ❓ Open an [issue](https://github.com/C9Glax/tranga/issues/new?assignees=&labels=New+Connector&projects=&template=new_connector.yml&title=%5BNew+Connector%5D%3A+) - ❓ Open an [issue](https://github.com/C9Glax/tranga/issues/new?assignees=&labels=New+Connector&projects=&template=new_connector.yml&title=%5BNew+Connector%5D%3A+)
and trigger a library-scan with [Komga](https://komga.org/) and [Kavita](https://www.kavitareader.com/). and trigger a library-scan with [Komga](https://komga.org/) and [Kavita](https://www.kavitareader.com/).
@ -47,25 +40,29 @@ Notifications can be sent to your devices using [Gotify](https://gotify.net/), [
## What this program does and does *not* do ## What this program does and does *not* do
Tranga (the program in this repository) is a REST-API and worker in one. Meaning it will open a network-port DOES: Download Images from a Website.<br />
to listen for requests, and then work through these. Requests include searches for Manga, starting "Jobs" such DOES: Create Archives.<br />
as downloading available chapters, creating a monitoring job (that will periodically do the aforementioned),
update metadata, and more. ### how:
Tranga (this repository) is a REST-API and worker in one. Tranga provides REST-Endpoints to configure workers (Jobs).
Requests include searches for Manga, creating and starting Jobs such as downloading available chapters.
For available endpoints check `<hostedInstance>/swagger`
This repository *does not* include a frontend. A frontend can take many forms, such as a website: This repository *does not* include a frontend. A frontend can take many forms, such as a website:
[tranga-website](https://github.com/C9Glax/tranga-website) [tranga-website](https://github.com/C9Glax/tranga-website)
When downloading a chapter (meaning the images that make-up the manga) from a Scanlation-Website, Tranga will When downloading a chapter (meaning the images that make-up the manga) from a Website, Tranga will
additionally try and scrape Metadata from the same website ~~or enhance it from third-party sources~~ additionally try and scrape Metadata from the same website ~~or enhance it from third-party sources~~
(tbd https://github.com/C9Glax/tranga/issues/280). ([tbd issue](https://github.com/C9Glax/tranga/issues/280)).
Downloaded images can be jpeg-compressed and/or made black and white to save on diskspace Downloaded images can be jpeg-compressed and/or made black and white to save on diskspace
(measured at least a 50% reduction in size, without a significant loss of quality). (measured at least a 50% reduction in size, without a significant loss of quality).
Tranga will then package the contents of each chapter in a `.cbz`-archive and place it in a common folder per Manga. Tranga will then package the contents of each chapter in a `.cbz`-archive and place it in a common folder per Manga.
If specified, Tranga will then notify library-Managers such as [Komga](https://komga.org/) and [Kavita](https://www.kavitareader.com/) to trigger a scan for new If specified, Tranga will then notify library-Managers such as [Komga](https://komga.org/) and [Kavita](https://www.kavitareader.com/) to trigger a scan for new
chapters. Tranga can also send notifications to your devices via third-party services such as [Gotify](https://gotify.net/), [LunaSea](https://www.lunasea.app/) or [Ntfy](https://ntfy.sh/ chapters. Tranga can also send notifications to your devices via third-party services such as [Gotify](https://gotify.net/), [Ntfy](https://ntfy.sh/),
). or any other REST Webhook.
## Screenshots ## Screenshots
@ -117,6 +114,8 @@ Endpoints are documented in Swagger. Just spin up an instance, and go to `http:/
### Docker ### Docker
Built for AMD64 (and ARM64, maybe, if it feels like it).
An example `docker-compose.yaml` is provided. Mount `/Manga` to wherever you want your chapters (`.cbz`-Archives) An example `docker-compose.yaml` is provided. Mount `/Manga` to wherever you want your chapters (`.cbz`-Archives)
downloaded (where Komga/Kavita can access them for example). downloaded (where Komga/Kavita can access them for example).
The file also includes [tranga-website](https://github.com/C9Glax/tranga-website) as frontend. For its configuration refer to the The file also includes [tranga-website](https://github.com/C9Glax/tranga-website) as frontend. For its configuration refer to the
@ -155,21 +154,22 @@ Manga[] zyx = Object.GetAnotherThing(); //I can now easily see that zyx is an Ar
**A broad overview of where is what:**<br /> **A broad overview of where is what:**<br />
- `Program.cs` Configuration for ASP.NET, Swagger (also in `NamedSwaggerGenOptions.cs`, Npgsql - `Program.cs` Configuration for ASP.NET, Swagger (also in `NamedSwaggerGenOptions.cs`)
- `Tranga.cs` Job(worker)-Logic - `Tranga.cs` Worker-Logic
- `Schema/` Entity-Framework - `Schema/` Entity-Framework
- `Schema/Jobs/` + Logic for Jobs - `Schema/Jobs/` + Logic for Jobs
- `Schema/**/` + Logic for ** - `Schema/**/` + Logic for **
- `Schema/PgsqlContext.cs` EF configuration - `Schema/Contexts/` EF configuration
- `MangaDownloadClients/` Networking-Clients for Scraping - `MangaDownloadClients/` Networking-Clients for Scraping
- `Controllers/` ASP.NET Controllers (Endpoints) - `Controllers/` ASP.NET Controllers (Endpoints)
- `APIEndpointRecords/` Records for API-Requests with specific Request-Types (Body) - `APIEndpointRecords/` Records for API-Requests with specific Request-Types (Body)
If you want to add a new Scanlationsite-Connector: <br /> If you want to add a new Website-Connector: <br />
1. Copy one of the existing connectors, or start from scratch and inherit from `API.Schema.MangaConnectors.MangaConnector`. 1. Copy one of the existing connectors, or start from scratch and inherit from `API.Schema.MangaConnectors.MangaConnector`.
2. Add the new Connector as Object-Instance in `Program.cs` to the MangaConnector-Array `connectors`. 2. Add the new Connector as Object-Instance in `Program.cs` to the MangaConnector-Array `connectors`.
3. In `Schema/PgsqlContext.cs` add the Discriminator for the Connector (the value is the name of the connector, as defined 3. In `PgsqlContext.cs` add the Discriminator for the Connector (the value is the name of the connector, as defined
in the constructor). in the constructor).
4. In `Program.cs` add a new Object to the Array.
<!-- LICENSE --> <!-- LICENSE -->
## License ## License