Add NewtonsoftJson to Swagger

Add RetrieveChaptersJob.cs
Add UpdateFilesDownloadedJob.cs
Remove DownloadNewChaptersJob.cs and instead use DownloadAvailableChaptersJob.cs
This commit is contained in:
Glax 2025-03-08 18:09:41 +01:00
parent ecfc8f349b
commit ffc0e7555a
14 changed files with 274 additions and 94 deletions

View File

@ -27,6 +27,7 @@
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.7" /> <PackageReference Include="SixLabors.ImageSharp" Version="3.1.7" />
<PackageReference Include="Soenneker.Utils.String.NeedlemanWunsch" Version="3.0.920" /> <PackageReference Include="Soenneker.Utils.String.NeedlemanWunsch" Version="3.0.920" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="7.3.1" /> <PackageReference Include="Swashbuckle.AspNetCore" Version="7.3.1" />
<PackageReference Include="Swashbuckle.AspNetCore.Newtonsoft" Version="7.3.1" />
<PackageReference Include="System.Drawing.Common" Version="9.0.2" /> <PackageReference Include="System.Drawing.Common" Version="9.0.2" />
</ItemGroup> </ItemGroup>

View File

@ -83,19 +83,24 @@ public class JobController(PgsqlContext context) : Controller
} }
/// <summary> /// <summary>
/// Create a new CreateNewDownloadChapterJob /// Create a new DownloadAvailableChaptersJob
/// </summary> /// </summary>
/// <param name="MangaId">ID of Manga</param> /// <param name="MangaId">ID of Manga</param>
/// <param name="recurrenceTime">How often should we check for new chapters</param> /// <param name="recurrenceTime">How often should we check for new chapters</param>
/// <response code="201">Created new Job</response> /// <response code="201">Created new Job</response>
/// <response code="404">Could not find Manga with ID</response>
/// <response code="500">Error during Database Operation</response> /// <response code="500">Error during Database Operation</response>
[HttpPut("NewDownloadChapterJob/{MangaId}")] [HttpPut("DownloadAvailableChaptersJob/{MangaId}")]
[ProducesResponseType(Status201Created)] [ProducesResponseType<string[]>(Status201Created, "application/json")]
[ProducesResponseType(Status404NotFound)]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")] [ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public IActionResult CreateNewDownloadChapterJob(string MangaId, [FromBody]ulong recurrenceTime) public IActionResult CreateNewDownloadChapterJob(string MangaId, [FromBody]ulong recurrenceTime)
{ {
Job job = new DownloadNewChaptersJob(recurrenceTime, MangaId); if (context.Manga.Find(MangaId) is null)
return AddJob(job); return NotFound();
Job dep = new RetrieveChaptersJob(recurrenceTime, MangaId);
Job job = new DownloadAvailableChaptersJob(recurrenceTime, MangaId, null, [dep.JobId]);
return AddJobs([dep, job]);
} }
/// <summary> /// <summary>
@ -103,29 +108,37 @@ public class JobController(PgsqlContext context) : Controller
/// </summary> /// </summary>
/// <param name="ChapterId">ID of the Chapter</param> /// <param name="ChapterId">ID of the Chapter</param>
/// <response code="201">Created new Job</response> /// <response code="201">Created new Job</response>
/// <response code="404">Could not find Chapter with ID</response>
/// <response code="500">Error during Database Operation</response> /// <response code="500">Error during Database Operation</response>
[HttpPut("DownloadSingleChapterJob/{ChapterId}")] [HttpPut("DownloadSingleChapterJob/{ChapterId}")]
[ProducesResponseType(Status201Created)] [ProducesResponseType<string[]>(Status201Created, "application/json")]
[ProducesResponseType(Status404NotFound)]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")] [ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public IActionResult CreateNewDownloadChapterJob(string ChapterId) public IActionResult CreateNewDownloadChapterJob(string ChapterId)
{ {
if(context.Chapters.Find(ChapterId) is null)
return NotFound();
Job job = new DownloadSingleChapterJob(ChapterId); Job job = new DownloadSingleChapterJob(ChapterId);
return AddJob(job); return AddJobs([job]);
} }
/// <summary> /// <summary>
/// Create a new UpdateMetadataJob /// Create a new UpdateFilesDownloadedJob
/// </summary> /// </summary>
/// <param name="MangaId">ID of the Manga</param> /// <param name="MangaId">ID of the Manga</param>
/// <response code="201">Created new Job</response> /// <response code="201">Created new Job</response>
/// <response code="201">Could not find Manga with ID</response>
/// <response code="500">Error during Database Operation</response> /// <response code="500">Error during Database Operation</response>
[HttpPut("UpdateMetadataJob/{MangaId}")] [HttpPut("UpdateFilesJob/{MangaId}")]
[ProducesResponseType(Status201Created)] [ProducesResponseType<string[]>(Status201Created, "application/json")]
[ProducesResponseType(Status404NotFound)]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")] [ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public IActionResult CreateUpdateMetadataJob(string MangaId) public IActionResult CreateUpdateFilesDownloadedJob(string MangaId)
{ {
Job job = new UpdateMetadataJob(0, MangaId); if(context.Manga.Find(MangaId) is null)
return AddJob(job); return NotFound();
Job job = new UpdateFilesDownloadedJob(0, MangaId);
return AddJobs([job]);
} }
/// <summary> /// <summary>
@ -133,7 +146,50 @@ public class JobController(PgsqlContext context) : Controller
/// </summary> /// </summary>
/// <response code="201">Created new Job</response> /// <response code="201">Created new Job</response>
/// <response code="500">Error during Database Operation</response> /// <response code="500">Error during Database Operation</response>
[HttpPut("UpdateMetadataJob")] [HttpPut("UpdateAllFilesJob")]
[ProducesResponseType(Status201Created)]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public IActionResult CreateUpdateAllFilesDownloadedJob()
{
List<string> ids = context.Manga.Select(m => m.MangaId).ToList();
List<UpdateFilesDownloadedJob> jobs = ids.Select(id => new UpdateFilesDownloadedJob(0, id)).ToList();
try
{
context.Jobs.AddRange(jobs);
context.SaveChanges();
return Created();
}
catch (Exception e)
{
return StatusCode(500, e.Message);
}
}
/// <summary>
/// Create a new UpdateMetadataJob
/// </summary>
/// <param name="MangaId">ID of the Manga</param>
/// <response code="201">Created new Job</response>
/// <response code="404">Could not find Manga with ID</response>
/// <response code="500">Error during Database Operation</response>
[HttpPut("UpdateMetadataJob/{MangaId}")]
[ProducesResponseType<string[]>(Status201Created, "application/json")]
[ProducesResponseType(Status404NotFound)]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public IActionResult CreateUpdateMetadataJob(string MangaId)
{
if(context.Manga.Find(MangaId) is null)
return NotFound();
Job job = new UpdateMetadataJob(0, MangaId);
return AddJobs([job]);
}
/// <summary>
/// Create a new UpdateMetadataJob for all Manga
/// </summary>
/// <response code="201">Created new Job</response>
/// <response code="500">Error during Database Operation</response>
[HttpPut("UpdateAllMetadataJob")]
[ProducesResponseType(Status201Created)] [ProducesResponseType(Status201Created)]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")] [ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public IActionResult CreateUpdateAllMetadataJob() public IActionResult CreateUpdateAllMetadataJob()
@ -152,13 +208,13 @@ public class JobController(PgsqlContext context) : Controller
} }
} }
private IActionResult AddJob(Job job) private IActionResult AddJobs(Job[] jobs)
{ {
try try
{ {
context.Jobs.Add(job); context.Jobs.AddRange(jobs);
context.SaveChanges(); context.SaveChanges();
return new CreatedResult(job.JobId, job); return new CreatedResult((string?)null, jobs.Select(j => j.JobId).ToArray());
} }
catch (Exception e) catch (Exception e)
{ {

View File

@ -142,8 +142,8 @@ public class SearchController(PgsqlContext context) : Controller
MangaTag? inDb = context.Tags.FirstOrDefault(t => t.Equals(mt)); MangaTag? inDb = context.Tags.FirstOrDefault(t => t.Equals(mt));
return inDb ?? mt; return inDb ?? mt;
}); });
manga.Tags = mergedTags.ToList(); manga.MangaTags = mergedTags.ToList();
IEnumerable<MangaTag> newTags = manga.Tags.Where(mt => !context.Tags.Any(t => t.Tag.Equals(mt.Tag))); IEnumerable<MangaTag> newTags = manga.MangaTags.Where(mt => !context.Tags.Any(t => t.Tag.Equals(mt.Tag)));
context.Tags.AddRange(newTags); context.Tags.AddRange(newTags);
} }
@ -195,6 +195,7 @@ public class SearchController(PgsqlContext context) : Controller
context.Manga.Add(manga); context.Manga.Add(manga);
context.Jobs.Add(new DownloadMangaCoverJob(manga.MangaId)); context.Jobs.Add(new DownloadMangaCoverJob(manga.MangaId));
context.Jobs.Add(new RetrieveChaptersJob(0, manga.MangaId));
context.SaveChanges(); context.SaveChanges();
return existing ?? manga; return existing ?? manga;

View File

@ -367,17 +367,17 @@ namespace API.Migrations
b.Property<string>("MangaId") b.Property<string>("MangaId")
.HasColumnType("character varying(64)"); .HasColumnType("character varying(64)");
b.Property<string>("TagsTag") b.Property<string>("MangaTagsTag")
.HasColumnType("text"); .HasColumnType("text");
b.HasKey("MangaId", "TagsTag"); b.HasKey("MangaId", "MangaTagsTag");
b.HasIndex("TagsTag"); b.HasIndex("MangaTagsTag");
b.ToTable("MangaMangaTag"); b.ToTable("MangaMangaTag");
}); });
modelBuilder.Entity("API.Schema.Jobs.DownloadMangaCoverJob", b => modelBuilder.Entity("API.Schema.Jobs.DownloadAvailableChaptersJob", b =>
{ {
b.HasBaseType("API.Schema.Jobs.Job"); b.HasBaseType("API.Schema.Jobs.Job");
@ -391,13 +391,13 @@ namespace API.Migrations
b.ToTable("Jobs", t => b.ToTable("Jobs", t =>
{ {
t.Property("MangaId") t.Property("MangaId")
.HasColumnName("DownloadMangaCoverJob_MangaId"); .HasColumnName("DownloadAvailableChaptersJob_MangaId");
}); });
b.HasDiscriminator().HasValue((byte)4); b.HasDiscriminator().HasValue((byte)1);
}); });
modelBuilder.Entity("API.Schema.Jobs.DownloadNewChaptersJob", b => modelBuilder.Entity("API.Schema.Jobs.DownloadMangaCoverJob", b =>
{ {
b.HasBaseType("API.Schema.Jobs.Job"); b.HasBaseType("API.Schema.Jobs.Job");
@ -408,7 +408,7 @@ namespace API.Migrations
b.HasIndex("MangaId"); b.HasIndex("MangaId");
b.HasDiscriminator().HasValue((byte)1); b.HasDiscriminator().HasValue((byte)4);
}); });
modelBuilder.Entity("API.Schema.Jobs.DownloadSingleChapterJob", b => modelBuilder.Entity("API.Schema.Jobs.DownloadSingleChapterJob", b =>
@ -440,6 +440,46 @@ namespace API.Migrations
b.HasDiscriminator().HasValue((byte)3); b.HasDiscriminator().HasValue((byte)3);
}); });
modelBuilder.Entity("API.Schema.Jobs.RetrieveChaptersJob", 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("RetrieveChaptersJob_MangaId");
});
b.HasDiscriminator().HasValue((byte)5);
});
modelBuilder.Entity("API.Schema.Jobs.UpdateFilesDownloadedJob", 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("UpdateFilesDownloadedJob_MangaId");
});
b.HasDiscriminator().HasValue((byte)6);
});
modelBuilder.Entity("API.Schema.Jobs.UpdateMetadataJob", b => modelBuilder.Entity("API.Schema.Jobs.UpdateMetadataJob", b =>
{ {
b.HasBaseType("API.Schema.Jobs.Job"); b.HasBaseType("API.Schema.Jobs.Job");
@ -604,12 +644,12 @@ namespace API.Migrations
b.HasOne("API.Schema.MangaTag", null) b.HasOne("API.Schema.MangaTag", null)
.WithMany() .WithMany()
.HasForeignKey("TagsTag") .HasForeignKey("MangaTagsTag")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
}); });
modelBuilder.Entity("API.Schema.Jobs.DownloadMangaCoverJob", b => modelBuilder.Entity("API.Schema.Jobs.DownloadAvailableChaptersJob", b =>
{ {
b.HasOne("API.Schema.Manga", "Manga") b.HasOne("API.Schema.Manga", "Manga")
.WithMany() .WithMany()
@ -620,7 +660,7 @@ namespace API.Migrations
b.Navigation("Manga"); b.Navigation("Manga");
}); });
modelBuilder.Entity("API.Schema.Jobs.DownloadNewChaptersJob", b => modelBuilder.Entity("API.Schema.Jobs.DownloadMangaCoverJob", b =>
{ {
b.HasOne("API.Schema.Manga", "Manga") b.HasOne("API.Schema.Manga", "Manga")
.WithMany() .WithMany()
@ -642,6 +682,28 @@ namespace API.Migrations
b.Navigation("Chapter"); b.Navigation("Chapter");
}); });
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.UpdateFilesDownloadedJob", b =>
{
b.HasOne("API.Schema.Manga", "Manga")
.WithMany()
.HasForeignKey("MangaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Manga");
});
modelBuilder.Entity("API.Schema.Jobs.UpdateMetadataJob", b => modelBuilder.Entity("API.Schema.Jobs.UpdateMetadataJob", b =>
{ {
b.HasOne("API.Schema.Manga", "Manga") b.HasOne("API.Schema.Manga", "Manga")

View File

@ -8,6 +8,7 @@ using Asp.Versioning;
using Asp.Versioning.Builder; using Asp.Versioning.Builder;
using Asp.Versioning.Conventions; using Asp.Versioning.Conventions;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters; using Newtonsoft.Json.Converters;
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
@ -25,25 +26,27 @@ builder.Services.AddCors(options =>
}); });
builder.Services.AddApiVersioning(option => builder.Services.AddApiVersioning(option =>
{ {
option.AssumeDefaultVersionWhenUnspecified = true; option.AssumeDefaultVersionWhenUnspecified = true;
option.DefaultApiVersion = new ApiVersion(2); option.DefaultApiVersion = new ApiVersion(2);
option.ReportApiVersions = true; option.ReportApiVersions = true;
option.ApiVersionReader = ApiVersionReader.Combine( option.ApiVersionReader = ApiVersionReader.Combine(
new UrlSegmentApiVersionReader(), new UrlSegmentApiVersionReader(),
new QueryStringApiVersionReader("api-version"), new QueryStringApiVersionReader("api-version"),
new HeaderApiVersionReader("X-Version"), new HeaderApiVersionReader("X-Version"),
new MediaTypeApiVersionReader("x-version")); new MediaTypeApiVersionReader("x-version"));
}) })
.AddMvc(options => .AddMvc(options =>
{ {
options.Conventions.Add(new VersionByNamespaceConvention()); options.Conventions.Add(new VersionByNamespaceConvention());
}) })
.AddApiExplorer(options => { .AddApiExplorer(options =>
options.GroupNameFormat = "'v'V"; {
options.SubstituteApiVersionInUrl = true; options.GroupNameFormat = "'v'V";
}); options.SubstituteApiVersionInUrl = true;
});
builder.Services.AddEndpointsApiExplorer(); builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGenNewtonsoftSupport();
builder.Services.AddSwaggerGen(opt => builder.Services.AddSwaggerGen(opt =>
{ {
var xmlFilename = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml"; var xmlFilename = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
@ -58,12 +61,13 @@ builder.Services.AddDbContext<PgsqlContext>(options =>
$"Password={Environment.GetEnvironmentVariable("POSTGRES_PASSWORD")??"postgres"}")); $"Password={Environment.GetEnvironmentVariable("POSTGRES_PASSWORD")??"postgres"}"));
builder.Services.AddControllers(options => builder.Services.AddControllers(options =>
{ {
options.AllowEmptyInputInBodyModelBinding = true; options.AllowEmptyInputInBodyModelBinding = true;
}) });
.AddNewtonsoftJson(opts => builder.Services.AddControllers().AddNewtonsoftJson(opts =>
{ {
opts.SerializerSettings.Converters.Add(new StringEnumConverter()); opts.SerializerSettings.Converters.Add(new StringEnumConverter());
opts.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore;
}); });
builder.WebHost.UseUrls("http://*:6531"); builder.WebHost.UseUrls("http://*:6531");
@ -115,9 +119,7 @@ using (var scope = app.Services.CreateScope())
MangaConnector[] newConnectors = connectors.Where(c => !context.MangaConnectors.Contains(c)).ToArray(); MangaConnector[] newConnectors = connectors.Where(c => !context.MangaConnectors.Contains(c)).ToArray();
context.MangaConnectors.AddRange(newConnectors); context.MangaConnectors.AddRange(newConnectors);
IQueryable<string> updateMetadataJobMangaIds = context.Jobs.Where(j => j.JobType == JobType.UpdateMetaDataJob).Select(j => ((UpdateMetadataJob)j).MangaId); context.Jobs.AddRange(context.Manga.AsEnumerable().Select(m => new UpdateFilesDownloadedJob(0, m.MangaId)));
Job[] newUpdateMetadataJobs = context.Manga.Where(m => !updateMetadataJobMangaIds.Contains(m.MangaId)).ToList().Select(m => new UpdateMetadataJob(0, m.MangaId)).ToArray();
context.Jobs.AddRange(newUpdateMetadataJobs);
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));

View File

@ -39,8 +39,7 @@ public class Chapter : IComparable<Chapter>
public bool Downloaded { get; internal set; } = false; public bool Downloaded { get; internal set; } = false;
public string ParentMangaId { get; internal set; } public string ParentMangaId { get; internal set; }
[JsonIgnore] [JsonIgnore] public Manga? ParentManga { get; init; }
public Manga? ParentManga { get; init; }
public int CompareTo(Chapter? other) public int CompareTo(Chapter? other)
{ {
@ -130,7 +129,7 @@ public class Chapter : IComparable<Chapter>
internal string GetComicInfoXmlString() internal string GetComicInfoXmlString()
{ {
XElement comicInfo = new("ComicInfo", XElement comicInfo = new("ComicInfo",
new XElement("Tags", string.Join(',', ParentManga.Tags.Select(tag => tag.Tag))), new XElement("Tags", string.Join(',', ParentManga.MangaTags.Select(tag => tag.Tag))),
new XElement("LanguageISO", ParentManga.OriginalLanguage), new XElement("LanguageISO", ParentManga.OriginalLanguage),
new XElement("Title", Title), new XElement("Title", Title),
new XElement("Writer", string.Join(',', ParentManga.Authors.Select(author => author.AuthorName))), new XElement("Writer", string.Join(',', ParentManga.Authors.Select(author => author.AuthorName))),

View File

@ -0,0 +1,21 @@
using System.ComponentModel.DataAnnotations;
using API.Schema.MangaConnectors;
using Newtonsoft.Json;
namespace API.Schema.Jobs;
public class DownloadAvailableChaptersJob(ulong recurrenceMs, string mangaId, string? parentJobId = null, ICollection<string>? dependsOnJobsIds = null)
: Job(TokenGen.CreateToken(typeof(DownloadAvailableChaptersJob)), JobType.DownloadAvailableChaptersJob, recurrenceMs, parentJobId, dependsOnJobsIds)
{
[MaxLength(64)]
public string MangaId { get; init; } = mangaId;
[JsonIgnore]
public Manga? Manga { get; init; }
protected override IEnumerable<Job> RunInternal(PgsqlContext context)
{
return context.Chapters.Where(c => c.ParentMangaId == MangaId).AsEnumerable()
.Select(chapter => new DownloadSingleChapterJob(chapter.ChapterId, this.JobId));
}
}

View File

@ -4,8 +4,10 @@
public enum JobType : byte public enum JobType : byte
{ {
DownloadSingleChapterJob = 0, DownloadSingleChapterJob = 0,
DownloadNewChaptersJob = 1, DownloadAvailableChaptersJob = 1,
UpdateMetaDataJob = 2, UpdateMetaDataJob = 2,
MoveFileOrFolderJob = 3, MoveFileOrFolderJob = 3,
DownloadMangaCoverJob = 4 DownloadMangaCoverJob = 4,
RetrieveChaptersJob = 5,
UpdateFilesDownloadedJob = 6
} }

View File

@ -1,11 +1,11 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using API.Schema.MangaConnectors; using API.Schema.MangaConnectors;
using Newtonsoft.Json; using Newtonsoft.Json;
namespace API.Schema.Jobs; namespace API.Schema.Jobs;
public class DownloadNewChaptersJob(ulong recurrenceMs, string mangaId, string? parentJobId = null, ICollection<string>? dependsOnJobsIds = null) public class RetrieveChaptersJob(ulong recurrenceMs, string mangaId, string? parentJobId = null, ICollection<string>? dependsOnJobsIds = null)
: Job(TokenGen.CreateToken(typeof(DownloadNewChaptersJob)), JobType.DownloadNewChaptersJob, recurrenceMs, parentJobId, dependsOnJobsIds) : Job(TokenGen.CreateToken(typeof(RetrieveChaptersJob)), JobType.RetrieveChaptersJob, recurrenceMs, parentJobId, dependsOnJobsIds)
{ {
[MaxLength(64)] [MaxLength(64)]
public string MangaId { get; init; } = mangaId; public string MangaId { get; init; } = mangaId;
@ -30,7 +30,7 @@ public class DownloadNewChaptersJob(ulong recurrenceMs, string mangaId, string?
Chapter[] newChapters = allNewChapters.Where(chapter => !chapterIds.Contains(chapter.ChapterId)).ToArray(); Chapter[] newChapters = allNewChapters.Where(chapter => !chapterIds.Contains(chapter.ChapterId)).ToArray();
context.Chapters.AddRangeAsync(newChapters).Wait(); context.Chapters.AddRangeAsync(newChapters).Wait();
context.SaveChangesAsync().Wait(); context.SaveChangesAsync().Wait();
return allNewChapters.Select(chapter => new DownloadSingleChapterJob(chapter.ChapterId, this.JobId)); return [];
} }
} }

View File

@ -0,0 +1,24 @@
using System.ComponentModel.DataAnnotations;
using Newtonsoft.Json;
namespace API.Schema.Jobs;
public class UpdateFilesDownloadedJob(ulong recurrenceMs, string mangaId, string? parentJobId = null, ICollection<string>? dependsOnJobsIds = null)
: Job(TokenGen.CreateToken(typeof(UpdateFilesDownloadedJob)), JobType.UpdateFilesDownloadedJob, recurrenceMs, parentJobId, dependsOnJobsIds)
{
[MaxLength(64)]
public string MangaId { get; init; } = mangaId;
[JsonIgnore]
public virtual Manga? Manga { get; init; }
protected override IEnumerable<Job> RunInternal(PgsqlContext context)
{
IQueryable<Chapter> chapters = context.Chapters.Where(c => c.ParentMangaId == MangaId);
foreach (Chapter chapter in chapters)
chapter.Downloaded = chapter.IsDownloaded();
context.SaveChanges();
return [];
}
}

View File

@ -21,14 +21,6 @@ public class UpdateMetadataJob(ulong recurrenceMs, string mangaId, string? paren
/// <param name="context"></param> /// <param name="context"></param>
protected override IEnumerable<Job> RunInternal(PgsqlContext context) protected override IEnumerable<Job> RunInternal(PgsqlContext context)
{ {
//Manga manga = Manga ?? context.Manga.Find(MangaId)!; throw new NotImplementedException();
IQueryable<Chapter> chapters = context.Chapters.Where(c => c.ParentMangaId == MangaId);
foreach (Chapter chapter in chapters)
chapter.Downloaded = chapter.IsDownloaded();
context.SaveChanges();
return [];
//TODO implement Metadata-Update from MangaConnector
} }
} }

View File

@ -1,4 +1,5 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Net; using System.Net;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
@ -6,6 +7,7 @@ using API.MangaDownloadClients;
using API.Schema.Jobs; using API.Schema.Jobs;
using API.Schema.MangaConnectors; using API.Schema.MangaConnectors;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Newtonsoft.Json;
using static System.IO.UnixFileMode; using static System.IO.UnixFileMode;
namespace API.Schema; namespace API.Schema;
@ -30,26 +32,30 @@ public class Manga
public float IgnoreChapterBefore { get; internal set; } public float IgnoreChapterBefore { get; internal set; }
public string MangaConnectorId { get; private set; } public string MangaConnectorId { get; private set; }
[JsonIgnore] public MangaConnector? MangaConnector { get; private set; }
public MangaConnector? MangaConnector { get; private set; } [JsonIgnore] public ICollection<Author>? Authors { get; internal set; }
[NotMapped] public IEnumerable<string> AuthorIds => Authors?.Select(a => a.AuthorId) ?? [];
public ICollection<Author>? Authors { get; internal set; } [JsonIgnore] public ICollection<MangaTag>? MangaTags { get; internal set; }
[NotMapped] public IEnumerable<string> Tags => MangaTags.Select(t => t.Tag);
public ICollection<MangaTag>? Tags { get; internal set; }
public ICollection<Link>? Links { get; internal set; } [JsonIgnore] public ICollection<Link>? Links { get; internal set; }
[NotMapped] public IEnumerable<string> LinkIds => Links?.Select(l => l.LinkId) ?? [];
public ICollection<MangaAltTitle>? AltTitles { get; internal set; } [JsonIgnore] public ICollection<MangaAltTitle>? AltTitles { get; internal set; }
[NotMapped] public IEnumerable<string> AltTitleIds => AltTitles?.Select(a => a.AltTitleId) ?? [];
public Manga(string connectorId, string name, string description, string websiteUrl, string coverUrl, public Manga(string connectorId, string name, string description, string websiteUrl, string coverUrl,
string? coverFileNameInCache, uint year, string? originalLanguage, MangaReleaseStatus releaseStatus, string? coverFileNameInCache, uint year, string? originalLanguage, MangaReleaseStatus releaseStatus,
float ignoreChapterBefore, MangaConnector mangaConnector, ICollection<Author> authors, float ignoreChapterBefore, MangaConnector mangaConnector, ICollection<Author> authors,
ICollection<MangaTag> tags, ICollection<Link> links, ICollection<MangaAltTitle> altTitles) ICollection<MangaTag> mangaTags, ICollection<Link> links, ICollection<MangaAltTitle> altTitles)
: this(connectorId, name, description, websiteUrl, coverUrl, coverFileNameInCache, year, originalLanguage, : this(connectorId, name, description, websiteUrl, coverUrl, coverFileNameInCache, year, originalLanguage,
releaseStatus, ignoreChapterBefore, mangaConnector.Name) releaseStatus, ignoreChapterBefore, mangaConnector.Name)
{ {
this.Authors = authors; this.Authors = authors;
this.Tags = tags; this.MangaTags = mangaTags;
this.Links = links; this.Links = links;
this.AltTitles = altTitles; this.AltTitles = altTitles;
} }
@ -89,7 +95,7 @@ public class Manga
this.OriginalLanguage = other.OriginalLanguage; this.OriginalLanguage = other.OriginalLanguage;
this.Authors = other.Authors; this.Authors = other.Authors;
this.Links = other.Links; this.Links = other.Links;
this.Tags = other.Tags; this.MangaTags = other.MangaTags;
this.AltTitles = other.AltTitles; this.AltTitles = other.AltTitles;
this.ReleaseStatus = other.ReleaseStatus; this.ReleaseStatus = other.ReleaseStatus;
} }

View File

@ -36,21 +36,23 @@ public class PgsqlContext(DbContextOptions<PgsqlContext> options) : DbContext(op
.HasDiscriminator<LibraryType>(l => l.LibraryType) .HasDiscriminator<LibraryType>(l => l.LibraryType)
.HasValue<Komga>(LibraryType.Komga) .HasValue<Komga>(LibraryType.Komga)
.HasValue<Kavita>(LibraryType.Kavita); .HasValue<Kavita>(LibraryType.Kavita);
modelBuilder.Entity<Job>() modelBuilder.Entity<Job>()
.HasDiscriminator<JobType>(j => j.JobType) .HasDiscriminator<JobType>(j => j.JobType)
.HasValue<MoveFileOrFolderJob>(JobType.MoveFileOrFolderJob) .HasValue<MoveFileOrFolderJob>(JobType.MoveFileOrFolderJob)
.HasValue<DownloadNewChaptersJob>(JobType.DownloadNewChaptersJob) .HasValue<DownloadAvailableChaptersJob>(JobType.DownloadAvailableChaptersJob)
.HasValue<DownloadSingleChapterJob>(JobType.DownloadSingleChapterJob) .HasValue<DownloadSingleChapterJob>(JobType.DownloadSingleChapterJob)
.HasValue<DownloadMangaCoverJob>(JobType.DownloadMangaCoverJob) .HasValue<DownloadMangaCoverJob>(JobType.DownloadMangaCoverJob)
.HasValue<UpdateMetadataJob>(JobType.UpdateMetaDataJob); .HasValue<UpdateMetadataJob>(JobType.UpdateMetaDataJob)
.HasValue<RetrieveChaptersJob>(JobType.RetrieveChaptersJob)
.HasValue<UpdateFilesDownloadedJob>(JobType.UpdateFilesDownloadedJob);
modelBuilder.Entity<Job>() modelBuilder.Entity<Job>()
.HasOne<Job>(j => j.ParentJob) .HasOne<Job>(j => j.ParentJob)
.WithMany() .WithMany()
.HasForeignKey(j => j.ParentJobId); .HasForeignKey(j => j.ParentJobId);
modelBuilder.Entity<Job>() modelBuilder.Entity<Job>()
.HasMany<Job>(j => j.DependsOnJobs); .HasMany<Job>(j => j.DependsOnJobs);
modelBuilder.Entity<DownloadNewChaptersJob>() modelBuilder.Entity<DownloadAvailableChaptersJob>()
.Navigation(dncj => dncj.Manga) .Navigation(dncj => dncj.Manga)
.AutoInclude(); .AutoInclude();
modelBuilder.Entity<DownloadSingleChapterJob>() modelBuilder.Entity<DownloadSingleChapterJob>()
@ -74,10 +76,10 @@ public class PgsqlContext(DbContextOptions<PgsqlContext> options) : DbContext(op
.Navigation(m => m.Authors) .Navigation(m => m.Authors)
.AutoInclude(); .AutoInclude();
modelBuilder.Entity<Manga>() modelBuilder.Entity<Manga>()
.HasMany<MangaTag>(m => m.Tags) .HasMany<MangaTag>(m => m.MangaTags)
.WithMany(); .WithMany();
modelBuilder.Entity<Manga>() modelBuilder.Entity<Manga>()
.Navigation(m => m.Tags) .Navigation(m => m.MangaTags)
.AutoInclude(); .AutoInclude();
modelBuilder.Entity<Manga>() modelBuilder.Entity<Manga>()
.HasMany<Link>(m => m.Links) .HasMany<Link>(m => m.Links)

View File

@ -91,10 +91,10 @@ public static class Tranga
// If the job is already running, skip it // If the job is already running, skip it
if (RunningJobs.Values.Any(j => j.JobId == job.JobId)) continue; if (RunningJobs.Values.Any(j => j.JobId == job.JobId)) continue;
if (job is DownloadNewChaptersJob dncj) if (job is DownloadAvailableChaptersJob dncj)
{ {
if (RunningJobs.Values.Any(j => if (RunningJobs.Values.Any(j =>
j is DownloadNewChaptersJob rdncj && j is DownloadAvailableChaptersJob rdncj &&
rdncj.Manga?.MangaConnector == dncj.Manga?.MangaConnector)) rdncj.Manga?.MangaConnector == dncj.Manga?.MangaConnector))
{ {
continue; continue;
@ -143,13 +143,15 @@ public static class Tranga
IEnumerable<Job> ret = new List<Job>(); IEnumerable<Job> ret = new List<Job>();
if(jobsByType.ContainsKey(JobType.MoveFileOrFolderJob)) if(jobsByType.ContainsKey(JobType.MoveFileOrFolderJob))
ret = ret.Concat(jobsByType[JobType.MoveFileOrFolderJob]); ret = ret.Concat(jobsByType[JobType.MoveFileOrFolderJob]);
if(jobsByType.ContainsKey(JobType.DownloadMangaCoverJob)) if(jobsByType.ContainsKey(JobType.DownloadMangaCoverJob))
ret = ret.Concat(jobsByType[JobType.DownloadMangaCoverJob]); ret = ret.Concat(jobsByType[JobType.DownloadMangaCoverJob]);
if(jobsByType.ContainsKey(JobType.UpdateFilesDownloadedJob))
ret = ret.Concat(jobsByType[JobType.UpdateFilesDownloadedJob]);
Dictionary<MangaConnector, List<Job>> metadataJobsByConnector = new(); Dictionary<MangaConnector, List<Job>> metadataJobsByConnector = new();
if (jobsByType.ContainsKey(JobType.DownloadNewChaptersJob)) if (jobsByType.ContainsKey(JobType.DownloadAvailableChaptersJob))
{ {
foreach (DownloadNewChaptersJob job in jobsByType[JobType.DownloadNewChaptersJob]) foreach (DownloadAvailableChaptersJob job in jobsByType[JobType.DownloadAvailableChaptersJob])
{ {
Manga manga = job.Manga ?? context.Manga.Find(job.MangaId)!; Manga manga = job.Manga ?? context.Manga.Find(job.MangaId)!;
MangaConnector connector = manga.MangaConnector ?? context.MangaConnectors.Find(manga.MangaConnectorId)!; MangaConnector connector = manga.MangaConnector ?? context.MangaConnectors.Find(manga.MangaConnectorId)!;
@ -167,6 +169,16 @@ public static class Tranga
metadataJobsByConnector[connector].Add(job); metadataJobsByConnector[connector].Add(job);
} }
} }
if (jobsByType.ContainsKey(JobType.RetrieveChaptersJob))
{
foreach (RetrieveChaptersJob job in jobsByType[JobType.RetrieveChaptersJob])
{
Manga manga = job.Manga ?? context.Manga.Find(job.MangaId)!;
MangaConnector connector = manga.MangaConnector ?? context.MangaConnectors.Find(manga.MangaConnectorId)!;
if(!metadataJobsByConnector.TryAdd(connector, [job]))
metadataJobsByConnector[connector].Add(job);
}
}
foreach (List<Job> metadataJobs in metadataJobsByConnector.Values) foreach (List<Job> metadataJobs in metadataJobsByConnector.Values)
ret = ret.Append(metadataJobs.MinBy(j => j.NextExecution))!; ret = ret.Append(metadataJobs.MinBy(j => j.NextExecution))!;