Compare commits

...

5 Commits

Author SHA1 Message Date
c696c38983 Fix Weebcentral
Some checks failed
Docker Image CI / build (push) Has been cancelled
2025-04-01 02:14:46 +02:00
dbbac1ad59 Fix Weebcentral 2025-04-01 02:14:13 +02:00
b955d41530 Fix Weebcentral 2025-04-01 02:09:28 +02:00
91e033a2ec Logging 2025-03-31 19:08:35 +02:00
4dd31dfe18 Dependency Updates 2025-03-31 18:14:34 +02:00
19 changed files with 1129 additions and 86 deletions

View File

@ -26,8 +26,8 @@
<PackageReference Include="PuppeteerSharp" Version="20.1.3" /> <PackageReference Include="PuppeteerSharp" Version="20.1.3" />
<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.929" /> <PackageReference Include="Soenneker.Utils.String.NeedlemanWunsch" Version="3.0.929" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="7.3.1" /> <PackageReference Include="Swashbuckle.AspNetCore" Version="8.1.0" />
<PackageReference Include="Swashbuckle.AspNetCore.Newtonsoft" Version="7.3.1" /> <PackageReference Include="Swashbuckle.AspNetCore.Newtonsoft" Version="8.1.0" />
<PackageReference Include="System.Drawing.Common" Version="9.0.3" /> <PackageReference Include="System.Drawing.Common" Version="9.0.3" />
</ItemGroup> </ItemGroup>

View File

@ -3,6 +3,7 @@ using API.Schema.Jobs;
using API.Schema.MangaConnectors; using API.Schema.MangaConnectors;
using Asp.Versioning; using Asp.Versioning;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using static Microsoft.AspNetCore.Http.StatusCodes; using static Microsoft.AspNetCore.Http.StatusCodes;
namespace API.Controllers; namespace API.Controllers;
@ -37,7 +38,7 @@ public class SearchController(PgsqlContext context) : Controller
if(add is not null) if(add is not null)
retMangas.Add(add); retMangas.Add(add);
} }
catch (Exception e) catch (DbUpdateException e)
{ {
return StatusCode(500, e); return StatusCode(500, e);
} }
@ -77,7 +78,7 @@ public class SearchController(PgsqlContext context) : Controller
if(add is not null) if(add is not null)
retMangas.Add(add); retMangas.Add(add);
} }
catch (Exception e) catch (DbUpdateException e)
{ {
return StatusCode(500, e.Message); return StatusCode(500, e.Message);
} }
@ -119,7 +120,7 @@ public class SearchController(PgsqlContext context) : Controller
return Ok(add); return Ok(add);
return StatusCode(500); return StatusCode(500);
} }
catch (Exception e) catch (DbUpdateException e)
{ {
return StatusCode(500, e.Message); return StatusCode(500, e.Message);
} }

View File

@ -0,0 +1,827 @@
// <auto-generated />
using System;
using System.Collections.Generic;
using API.Schema;
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
{
[DbContext(typeof(PgsqlContext))]
[Migration("20250401001439_dev-010425-1")]
partial class dev0104251
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "9.0.3")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "hstore");
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("API.Schema.Author", b =>
{
b.Property<string>("AuthorId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("AuthorName")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("character varying(128)");
b.HasKey("AuthorId");
b.ToTable("Authors");
});
modelBuilder.Entity("API.Schema.Chapter", b =>
{
b.Property<string>("ChapterId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("ChapterNumber")
.IsRequired()
.HasMaxLength(10)
.HasColumnType("character varying(10)");
b.Property<bool>("Downloaded")
.HasColumnType("boolean");
b.Property<string>("FileName")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("ParentMangaId")
.IsRequired()
.HasMaxLength(64)
.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.PrimitiveCollection<string[]>("DependsOnJobsIds")
.HasMaxLength(64)
.HasColumnType("text[]");
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.LibraryConnectors.LibraryConnector", b =>
{
b.Property<string>("LibraryConnectorId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("Auth")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("BaseUrl")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<byte>("LibraryType")
.HasColumnType("smallint");
b.HasKey("LibraryConnectorId");
b.ToTable("LibraryConnectors");
b.HasDiscriminator<byte>("LibraryType");
b.UseTphMappingStrategy();
});
modelBuilder.Entity("API.Schema.Link", b =>
{
b.Property<string>("LinkId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("LinkProvider")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("LinkUrl")
.IsRequired()
.HasMaxLength(2048)
.HasColumnType("character varying(2048)");
b.Property<string>("MangaId")
.HasColumnType("character varying(64)");
b.HasKey("LinkId");
b.HasIndex("MangaId");
b.ToTable("Links");
});
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")
.HasColumnType("text");
b.Property<string>("CoverUrl")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Description")
.IsRequired()
.HasColumnType("text");
b.Property<string>("DirectoryName")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("IdOnConnectorSite")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("character varying(128)");
b.Property<float>("IgnoreChapterBefore")
.HasColumnType("real");
b.Property<string>("LibraryLocalLibraryId")
.HasColumnType("character varying(64)");
b.Property<string>("MangaConnectorId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("OriginalLanguage")
.HasMaxLength(8)
.HasColumnType("character varying(8)");
b.Property<byte>("ReleaseStatus")
.HasColumnType("smallint");
b.Property<string>("WebsiteUrl")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<long>("Year")
.HasColumnType("bigint");
b.HasKey("MangaId");
b.HasIndex("LibraryLocalLibraryId");
b.HasIndex("MangaConnectorId");
b.ToTable("Mangas");
});
modelBuilder.Entity("API.Schema.MangaAltTitle", b =>
{
b.Property<string>("AltTitleId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("Language")
.IsRequired()
.HasMaxLength(8)
.HasColumnType("character varying(8)");
b.Property<string>("MangaId")
.HasColumnType("character varying(64)");
b.Property<string>("Title")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.HasKey("AltTitleId");
b.HasIndex("MangaId");
b.ToTable("AltTitles");
});
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("API.Schema.Notification", b =>
{
b.Property<string>("NotificationId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<DateTime>("Date")
.HasColumnType("timestamp with time zone");
b.Property<string>("Message")
.IsRequired()
.HasMaxLength(512)
.HasColumnType("character varying(512)");
b.Property<string>("Title")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("character varying(128)");
b.Property<byte>("Urgency")
.HasColumnType("smallint");
b.HasKey("NotificationId");
b.ToTable("Notifications");
});
modelBuilder.Entity("API.Schema.NotificationConnectors.NotificationConnector", b =>
{
b.Property<string>("Name")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("Body")
.IsRequired()
.HasMaxLength(4096)
.HasColumnType("character varying(4096)");
b.Property<Dictionary<string, string>>("Headers")
.IsRequired()
.HasColumnType("hstore");
b.Property<string>("HttpMethod")
.IsRequired()
.HasMaxLength(8)
.HasColumnType("character varying(8)");
b.Property<string>("Url")
.IsRequired()
.HasMaxLength(2048)
.HasColumnType("character varying(2048)");
b.HasKey("Name");
b.ToTable("NotificationConnectors");
});
modelBuilder.Entity("AuthorManga", b =>
{
b.Property<string>("AuthorsAuthorId")
.HasColumnType("character varying(64)");
b.Property<string>("MangaId")
.HasColumnType("character varying(64)");
b.HasKey("AuthorsAuthorId", "MangaId");
b.HasIndex("MangaId");
b.ToTable("AuthorManga");
});
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("MangaMangaTag", b =>
{
b.Property<string>("MangaId")
.HasColumnType("character varying(64)");
b.Property<string>("MangaTagsTag")
.HasColumnType("character varying(64)");
b.HasKey("MangaId", "MangaTagsTag");
b.HasIndex("MangaTagsTag");
b.ToTable("MangaMangaTag");
});
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.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 =>
{
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("UpdateMetadataJob_MangaId");
});
b.HasDiscriminator().HasValue((byte)2);
});
modelBuilder.Entity("API.Schema.LibraryConnectors.Kavita", b =>
{
b.HasBaseType("API.Schema.LibraryConnectors.LibraryConnector");
b.HasDiscriminator().HasValue((byte)1);
});
modelBuilder.Entity("API.Schema.LibraryConnectors.Komga", b =>
{
b.HasBaseType("API.Schema.LibraryConnectors.LibraryConnector");
b.HasDiscriminator().HasValue((byte)0);
});
modelBuilder.Entity("API.Schema.MangaConnectors.AsuraToon", b =>
{
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
b.HasDiscriminator().HasValue("AsuraToon");
});
modelBuilder.Entity("API.Schema.MangaConnectors.Bato", b =>
{
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
b.HasDiscriminator().HasValue("Bato");
});
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.MangaConnectors.MangaHere", b =>
{
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
b.HasDiscriminator().HasValue("MangaHere");
});
modelBuilder.Entity("API.Schema.MangaConnectors.MangaKatana", b =>
{
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
b.HasDiscriminator().HasValue("MangaKatana");
});
modelBuilder.Entity("API.Schema.MangaConnectors.Manganato", b =>
{
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
b.HasDiscriminator().HasValue("Manganato");
});
modelBuilder.Entity("API.Schema.MangaConnectors.Mangaworld", b =>
{
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
b.HasDiscriminator().HasValue("Mangaworld");
});
modelBuilder.Entity("API.Schema.MangaConnectors.ManhuaPlus", b =>
{
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
b.HasDiscriminator().HasValue("ManhuaPlus");
});
modelBuilder.Entity("API.Schema.MangaConnectors.Weebcentral", b =>
{
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
b.HasDiscriminator().HasValue("Weebcentral");
});
modelBuilder.Entity("API.Schema.Chapter", b =>
{
b.HasOne("API.Schema.Manga", "ParentManga")
.WithMany()
.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.Link", b =>
{
b.HasOne("API.Schema.Manga", null)
.WithMany("Links")
.HasForeignKey("MangaId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("API.Schema.Manga", b =>
{
b.HasOne("API.Schema.LocalLibrary", "Library")
.WithMany()
.HasForeignKey("LibraryLocalLibraryId")
.OnDelete(DeleteBehavior.Restrict);
b.HasOne("API.Schema.MangaConnectors.MangaConnector", "MangaConnector")
.WithMany()
.HasForeignKey("MangaConnectorId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Library");
b.Navigation("MangaConnector");
});
modelBuilder.Entity("API.Schema.MangaAltTitle", b =>
{
b.HasOne("API.Schema.Manga", null)
.WithMany("AltTitles")
.HasForeignKey("MangaId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("AuthorManga", b =>
{
b.HasOne("API.Schema.Author", null)
.WithMany()
.HasForeignKey("AuthorsAuthorId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("API.Schema.Manga", null)
.WithMany()
.HasForeignKey("MangaId")
.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("MangaMangaTag", b =>
{
b.HasOne("API.Schema.Manga", null)
.WithMany()
.HasForeignKey("MangaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("API.Schema.MangaTag", null)
.WithMany()
.HasForeignKey("MangaTagsTag")
.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.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 =>
{
b.HasOne("API.Schema.Manga", "Manga")
.WithMany()
.HasForeignKey("MangaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Manga");
});
modelBuilder.Entity("API.Schema.Manga", b =>
{
b.Navigation("AltTitles");
b.Navigation("Links");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,40 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace API.Migrations
{
/// <inheritdoc />
public partial class dev0104251 : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<string>(
name: "OriginalLanguage",
table: "Mangas",
type: "character varying(8)",
maxLength: 8,
nullable: true,
oldClrType: typeof(string),
oldType: "character varying(8)",
oldMaxLength: 8);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<string>(
name: "OriginalLanguage",
table: "Mangas",
type: "character varying(8)",
maxLength: 8,
nullable: false,
defaultValue: "",
oldClrType: typeof(string),
oldType: "character varying(8)",
oldMaxLength: 8,
oldNullable: true);
}
}
}

View File

@ -242,7 +242,6 @@ namespace API.Migrations
.HasColumnType("character varying(256)"); .HasColumnType("character varying(256)");
b.Property<string>("OriginalLanguage") b.Property<string>("OriginalLanguage")
.IsRequired()
.HasMaxLength(8) .HasMaxLength(8)
.HasColumnType("character varying(8)"); .HasColumnType("character varying(8)");
@ -591,6 +590,13 @@ namespace API.Migrations
b.HasDiscriminator().HasValue("Bato"); b.HasDiscriminator().HasValue("Bato");
}); });
modelBuilder.Entity("API.Schema.MangaConnectors.Global", b =>
{
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
b.HasDiscriminator().HasValue("Global");
});
modelBuilder.Entity("API.Schema.MangaConnectors.MangaDex", b => modelBuilder.Entity("API.Schema.MangaConnectors.MangaDex", b =>
{ {
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector"); b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");

View File

@ -95,7 +95,7 @@ app.UseSwaggerUI(options =>
app.UseHttpsRedirection(); app.UseHttpsRedirection();
//app.UseMiddleware<RequestTimeMiddleware>(); app.UseMiddleware<RequestTimeMiddleware>();
using (var scope = app.Services.CreateScope()) using (var scope = app.Services.CreateScope())
{ {
@ -140,7 +140,7 @@ using (var scope = app.Services.CreateScope())
TrangaSettings.Load(); TrangaSettings.Load();
Tranga.StartLogger(); Tranga.StartLogger();
Tranga.JobStarterThread.Start(app.Services); Tranga.JobStarterThread.Start(app.Services);
Tranga.NotificationSenderThread.Start(app.Services.CreateScope().ServiceProvider.GetService<PgsqlContext>()); Tranga.NotificationSenderThread.Start(app.Services);
app.UseCors("AllowAll"); app.UseCors("AllowAll");

View File

@ -16,10 +16,14 @@ public class DownloadMangaCoverJob(string mangaId, string? parentJobId = null, I
{ {
Manga? manga = Manga ?? context.Mangas.Find(this.MangaId); Manga? manga = Manga ?? context.Mangas.Find(this.MangaId);
if (manga is null) if (manga is null)
{
Log.Error($"Manga {this.MangaId} not found.");
return []; return [];
}
manga.CoverFileNameInCache = manga.SaveCoverImageToCache(); manga.CoverFileNameInCache = manga.SaveCoverImageToCache();
context.SaveChanges(); context.SaveChanges();
Log.Info($"Saved cover for Manga {this.MangaId} to cache at {manga.CoverFileNameInCache}.");
return []; return [];
} }
} }

View File

@ -24,48 +24,80 @@ public class DownloadSingleChapterJob(string chapterId, string? parentJobId = nu
protected override IEnumerable<Job> RunInternal(PgsqlContext context) protected override IEnumerable<Job> RunInternal(PgsqlContext context)
{ {
Chapter chapter = Chapter ?? context.Chapters.Find(ChapterId)!; Chapter? chapter = Chapter ?? context.Chapters.Find(ChapterId);
Manga manga = chapter.ParentManga ?? context.Mangas.Find(chapter.ParentMangaId)!; if (chapter is null)
MangaConnector connector = manga.MangaConnector ?? context.MangaConnectors.Find(manga.MangaConnectorId)!; {
Log.Error("Chapter is null.");
return [];
}
Manga? manga = chapter.ParentManga ?? context.Mangas.Find(chapter.ParentMangaId);
if (manga is null)
{
Log.Error("Manga is null.");
return [];
}
MangaConnector? connector = manga.MangaConnector ?? context.MangaConnectors.Find(manga.MangaConnectorId);
if (connector is null)
{
Log.Error("Connector is null.");
return [];
}
string[] imageUrls = connector.GetChapterImageUrls(chapter); string[] imageUrls = connector.GetChapterImageUrls(chapter);
string saveArchiveFilePath = chapter.FullArchiveFilePath; if (imageUrls.Length < 1)
{
Log.Info($"No imageUrls for chapter {chapterId}");
return [];
}
string? saveArchiveFilePath = chapter.FullArchiveFilePath;
if (saveArchiveFilePath is null)
{
Log.Error("saveArchiveFilePath is null.");
return [];
}
//Check if Publication Directory already exists //Check if Publication Directory already exists
string directoryPath = Path.GetDirectoryName(saveArchiveFilePath)!; string directoryPath = Path.GetDirectoryName(saveArchiveFilePath)!;
if (!Directory.Exists(directoryPath)) if (!Directory.Exists(directoryPath))
{
Log.Info($"Creating publication Directory: {directoryPath}");
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
Directory.CreateDirectory(directoryPath, Directory.CreateDirectory(directoryPath,
UserRead | UserWrite | UserExecute | GroupRead | GroupWrite | GroupExecute ); UserRead | UserWrite | UserExecute | GroupRead | GroupWrite | GroupExecute );
else else
Directory.CreateDirectory(directoryPath); Directory.CreateDirectory(directoryPath);
}
if (File.Exists(saveArchiveFilePath)) //Don't download twice. Redownload if (File.Exists(saveArchiveFilePath)) //Don't download twice. Redownload
{
Log.Info($"Archive {saveArchiveFilePath} already existed, but deleting and re-downloading.");
File.Delete(saveArchiveFilePath); File.Delete(saveArchiveFilePath);
}
//Create a temporary folder to store images //Create a temporary folder to store images
string tempFolder = Directory.CreateTempSubdirectory("trangatemp").FullName; string tempFolder = Directory.CreateTempSubdirectory("trangatemp").FullName;
Log.Debug($"Created temp folder: {tempFolder}");
Log.Info($"Downloading images: {ChapterId}");
int chapterNum = 0; int chapterNum = 0;
//Download all Images to temporary Folder //Download all Images to temporary Folder
if (imageUrls.Length == 0)
{
Directory.Delete(tempFolder, true);
return [];
}
foreach (string imageUrl in imageUrls) foreach (string imageUrl in imageUrls)
{ {
string extension = imageUrl.Split('.')[^1].Split('?')[0]; string extension = imageUrl.Split('.')[^1].Split('?')[0];
string imagePath = Path.Join(tempFolder, $"{chapterNum++}.{extension}"); string imagePath = Path.Join(tempFolder, $"{chapterNum++}.{extension}");
bool status = DownloadImage(imageUrl, imagePath); bool status = DownloadImage(imageUrl, imagePath);
if (status is false) if (status is false)
{
Log.Error($"Failed to download image: {imageUrl}");
return []; return [];
}
} }
CopyCoverFromCacheToDownloadLocation(manga); CopyCoverFromCacheToDownloadLocation(manga);
Log.Debug($"Creating ComicInfo.xml {ChapterId}");
File.WriteAllText(Path.Join(tempFolder, "ComicInfo.xml"), chapter.GetComicInfoXmlString()); File.WriteAllText(Path.Join(tempFolder, "ComicInfo.xml"), chapter.GetComicInfoXmlString());
Log.Debug($"Packaging images to archive {ChapterId}");
//ZIP-it and ship-it //ZIP-it and ship-it
ZipFile.CreateFromDirectory(tempFolder, saveArchiveFilePath); ZipFile.CreateFromDirectory(tempFolder, saveArchiveFilePath);
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
@ -81,7 +113,13 @@ public class DownloadSingleChapterJob(string chapterId, string? parentJobId = nu
private void ProcessImage(string imagePath) private void ProcessImage(string imagePath)
{ {
if (!TrangaSettings.bwImages && TrangaSettings.compression == 100) if (!TrangaSettings.bwImages && TrangaSettings.compression == 100)
{
Log.Debug($"No processing requested for image");
return; return;
}
Log.Debug($"Processing image: {imagePath}");
using Image image = Image.Load(imagePath); using Image image = Image.Load(imagePath);
File.Delete(imagePath); File.Delete(imagePath);
if(TrangaSettings.bwImages) if(TrangaSettings.bwImages)
@ -99,17 +137,23 @@ public class DownloadSingleChapterJob(string chapterId, string? parentJobId = nu
DirectoryInfo dirInfo = new (publicationFolder); DirectoryInfo dirInfo = new (publicationFolder);
if (dirInfo.EnumerateFiles().Any(info => info.Name.Contains("cover", StringComparison.InvariantCultureIgnoreCase))) if (dirInfo.EnumerateFiles().Any(info => info.Name.Contains("cover", StringComparison.InvariantCultureIgnoreCase)))
{ {
Log.Debug($"Cover already exists at {publicationFolder}");
return; return;
} }
Log.Info($"Copying cover to {publicationFolder}");
string? fileInCache = manga.CoverFileNameInCache ?? manga.SaveCoverImageToCache(); string? fileInCache = manga.CoverFileNameInCache ?? manga.SaveCoverImageToCache();
if (fileInCache is null) if (fileInCache is null)
{
Log.Error($"File {fileInCache} does not exist");
return; return;
}
string newFilePath = Path.Join(publicationFolder, $"cover.{Path.GetFileName(fileInCache).Split('.')[^1]}" ); string newFilePath = Path.Join(publicationFolder, $"cover.{Path.GetFileName(fileInCache).Split('.')[^1]}" );
File.Copy(fileInCache, newFilePath, true); File.Copy(fileInCache, newFilePath, true);
if(RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) if(RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
File.SetUnixFileMode(newFilePath, GroupRead | GroupWrite | UserRead | UserWrite); File.SetUnixFileMode(newFilePath, GroupRead | GroupWrite | UserRead | UserWrite);
Log.Debug($"Copied cover from {fileInCache} to {newFilePath}");
} }
private bool DownloadImage(string imageUrl, string savePath) private bool DownloadImage(string imageUrl, string savePath)

View File

@ -1,5 +1,6 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema; using System.ComponentModel.DataAnnotations.Schema;
using log4net;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Newtonsoft.Json; using Newtonsoft.Json;
@ -34,6 +35,10 @@ public abstract class Job
public JobState state { get; internal set; } = JobState.Waiting; public JobState state { get; internal set; } = JobState.Waiting;
[Required] [Required]
public bool Enabled { get; internal set; } = true; public bool Enabled { get; internal set; } = true;
[NotMapped]
[JsonIgnore]
protected ILog Log { get; init; }
public Job(string jobId, JobType jobType, ulong recurrenceMs, Job? parentJob = null, ICollection<Job>? dependsOnJobs = null) public Job(string jobId, JobType jobType, ulong recurrenceMs, Job? parentJob = null, ICollection<Job>? dependsOnJobs = null)
: this(jobId, jobType, recurrenceMs, parentJob?.JobId, dependsOnJobs?.Select(j => j.JobId).ToList()) : this(jobId, jobType, recurrenceMs, parentJob?.JobId, dependsOnJobs?.Select(j => j.JobId).ToList())
@ -44,6 +49,7 @@ public abstract class Job
public Job(string jobId, JobType jobType, ulong recurrenceMs, string? parentJobId = null, ICollection<string>? dependsOnJobsIds = null) public Job(string jobId, JobType jobType, ulong recurrenceMs, string? parentJobId = null, ICollection<string>? dependsOnJobsIds = null)
{ {
Log = LogManager.GetLogger(GetType());
JobId = jobId; JobId = jobId;
ParentJobId = parentJobId; ParentJobId = parentJobId;
DependsOnJobsIds = dependsOnJobsIds; DependsOnJobsIds = dependsOnJobsIds;
@ -53,16 +59,27 @@ public abstract class Job
public IEnumerable<Job> Run(IServiceProvider serviceProvider) public IEnumerable<Job> Run(IServiceProvider serviceProvider)
{ {
Log.Debug($"Running job {JobId}");
using IServiceScope scope = serviceProvider.CreateScope(); using IServiceScope scope = serviceProvider.CreateScope();
PgsqlContext context = scope.ServiceProvider.GetRequiredService<PgsqlContext>(); PgsqlContext context = scope.ServiceProvider.GetRequiredService<PgsqlContext>();
this.state = JobState.Running; try
context.SaveChanges(); {
Job[] newJobs = RunInternal(context).ToArray(); this.state = JobState.Running;
this.state = JobState.Completed; context.SaveChanges();
context.Jobs.AddRange(newJobs); Job[] newJobs = RunInternal(context).ToArray();
context.SaveChanges(); this.state = JobState.Completed;
return newJobs; context.Jobs.AddRange(newJobs);
context.SaveChanges();
Log.Info($"Job {JobId} completed. Generated {newJobs.Length} new jobs.");
return newJobs;
}
catch (DbUpdateException e)
{
this.state = JobState.Failed;
Log.Error($"Failed to run job {JobId}", e);
return [];
}
} }
protected abstract IEnumerable<Job> RunInternal(PgsqlContext context); protected abstract IEnumerable<Job> RunInternal(PgsqlContext context);

View File

@ -16,11 +16,18 @@ public class MoveFileOrFolderJob(string fromLocation, string toLocation, string?
{ {
try try
{ {
FileInfo fi = new FileInfo(FromLocation); FileInfo fi = new (FromLocation);
if (!fi.Exists) if (!fi.Exists)
{
Log.Error($"File does not exist at {FromLocation}");
return []; return [];
}
if (File.Exists(ToLocation))//Do not override existing if (File.Exists(ToLocation))//Do not override existing
{
Log.Error($"File already exists at {ToLocation}");
return []; return [];
}
if(fi.Attributes.HasFlag(FileAttributes.Directory)) if(fi.Attributes.HasFlag(FileAttributes.Directory))
MoveDirectory(fi, ToLocation); MoveDirectory(fi, ToLocation);
else else
@ -28,7 +35,7 @@ public class MoveFileOrFolderJob(string fromLocation, string toLocation, string?
} }
catch (Exception e) catch (Exception e)
{ {
Log.Error(e);
} }
return []; return [];

View File

@ -1,4 +1,5 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using Microsoft.EntityFrameworkCore;
namespace API.Schema.Jobs; namespace API.Schema.Jobs;
@ -15,15 +16,29 @@ public class MoveMangaLibraryJob(string mangaId, string toLibraryId, string? par
protected override IEnumerable<Job> RunInternal(PgsqlContext context) protected override IEnumerable<Job> RunInternal(PgsqlContext context)
{ {
Manga? manga = context.Mangas.Find(MangaId); Manga? manga = context.Mangas.Find(MangaId);
if(manga is null) if (manga is null)
throw new KeyNotFoundException(); {
Log.Error("Manga not found");
return [];
}
LocalLibrary? library = context.LocalLibraries.Find(ToLibraryId); LocalLibrary? library = context.LocalLibraries.Find(ToLibraryId);
if(library is null) if (library is null)
throw new KeyNotFoundException(); {
Log.Error("LocalLibrary not found");
return [];
}
Chapter[] chapters = context.Chapters.Where(c => c.ParentMangaId == MangaId).ToArray(); Chapter[] chapters = context.Chapters.Where(c => c.ParentMangaId == MangaId).ToArray();
Dictionary<Chapter, string> oldPath = chapters.ToDictionary(c => c, c => c.FullArchiveFilePath!); Dictionary<Chapter, string> oldPath = chapters.ToDictionary(c => c, c => c.FullArchiveFilePath!);
manga.Library = library; manga.Library = library;
context.SaveChanges(); try
{
context.SaveChanges();
}
catch (DbUpdateException e)
{
Log.Error(e);
return [];
}
return chapters.Select(c => new MoveFileOrFolderJob(oldPath[c], c.FullArchiveFilePath!)); return chapters.Select(c => new MoveFileOrFolderJob(oldPath[c], c.FullArchiveFilePath!));
} }

View File

@ -1,5 +1,6 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using API.Schema.MangaConnectors; using API.Schema.MangaConnectors;
using Microsoft.EntityFrameworkCore;
using Newtonsoft.Json; using Newtonsoft.Json;
namespace API.Schema.Jobs; namespace API.Schema.Jobs;
@ -16,21 +17,35 @@ public class RetrieveChaptersJob(ulong recurrenceMs, string mangaId, string? par
protected override IEnumerable<Job> RunInternal(PgsqlContext context) protected override IEnumerable<Job> RunInternal(PgsqlContext context)
{ {
/* Manga? manga = Manga ?? context.Mangas.Find(MangaId);
* For some reason, directly using Manga from above instead of finding it again causes DBContext to consider if (manga is null)
* Manga as a new entity and Postgres throws a Duplicate PK exception. {
* m.MangaConnector does not have this issue (IDK why). Log.Error("Manga is null.");
*/ return [];
Manga m = context.Mangas.Find(MangaId)!; }
MangaConnector connector = context.MangaConnectors.Find(m.MangaConnectorId)!; MangaConnector? connector = manga.MangaConnector ?? context.MangaConnectors.Find(manga.MangaConnectorId);
if (connector is null)
{
Log.Error("Connector is null.");
return [];
}
// This gets all chapters that are not downloaded // This gets all chapters that are not downloaded
Chapter[] allNewChapters = connector.GetNewChapters(m).DistinctBy(c => c.ChapterId).ToArray(); Chapter[] allNewChapters = connector.GetNewChapters(manga).DistinctBy(c => c.ChapterId).ToArray();
Log.Info($"{allNewChapters.Length} new chapters.");
// This filters out chapters that are not downloaded but already exist in the DB
string[] chapterIds = context.Chapters.Where(chapter => chapter.ParentMangaId == m.MangaId).Select(chapter => chapter.ChapterId).ToArray(); try
Chapter[] newChapters = allNewChapters.Where(chapter => !chapterIds.Contains(chapter.ChapterId)).ToArray(); {
context.Chapters.AddRange(newChapters); // This filters out chapters that are not downloaded but already exist in the DB
context.SaveChanges(); string[] chapterIds = context.Chapters.Where(chapter => chapter.ParentMangaId == manga.MangaId)
.Select(chapter => chapter.ChapterId).ToArray();
Chapter[] newChapters = allNewChapters.Where(chapter => !chapterIds.Contains(chapter.ChapterId)).ToArray();
context.Chapters.AddRange(newChapters);
context.SaveChanges();
}
catch (DbUpdateException e)
{
Log.Error(e);
}
return []; return [];
} }

View File

@ -21,6 +21,7 @@ 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)
{ {
Log.Warn("NOT IMPLEMENTED.");
return [];//TODO return [];//TODO
} }
} }

View File

@ -40,7 +40,6 @@ public class Manga
[Required] [Required]
public uint Year { get; internal set; } public uint Year { get; internal set; }
[StringLength(8)] [StringLength(8)]
[Required]
public string? OriginalLanguage { get; internal set; } public string? OriginalLanguage { get; internal set; }
[Required] [Required]
public MangaReleaseStatus ReleaseStatus { get; internal set; } public MangaReleaseStatus ReleaseStatus { get; internal set; }

View File

@ -1,6 +1,7 @@
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using API.MangaDownloadClients; using API.MangaDownloadClients;
using HtmlAgilityPack; using HtmlAgilityPack;
using log4net;
namespace API.Schema.MangaConnectors; namespace API.Schema.MangaConnectors;

View File

@ -2,6 +2,7 @@
using System.ComponentModel.DataAnnotations.Schema; using System.ComponentModel.DataAnnotations.Schema;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using API.MangaDownloadClients; using API.MangaDownloadClients;
using log4net;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Newtonsoft.Json; using Newtonsoft.Json;
@ -36,6 +37,10 @@ public abstract class MangaConnector(string name, string[] supportedLanguages, s
[JsonIgnore] [JsonIgnore]
[NotMapped] [NotMapped]
internal DownloadClient downloadClient { get; init; } = null!; internal DownloadClient downloadClient { get; init; } = null!;
[JsonIgnore]
[NotMapped]
protected ILog Log { get; init; } = LogManager.GetLogger(name);
public Chapter[] GetNewChapters(Manga manga) public Chapter[] GetNewChapters(Manga manga)
{ {

View File

@ -36,10 +36,10 @@ public class Weebcentral : MangaConnector
private (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)[] ParsePublicationsFromHtml(HtmlDocument document) private (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)[] ParsePublicationsFromHtml(HtmlDocument document)
{ {
if (document.DocumentNode.SelectNodes("//article") == null) if (document.DocumentNode.SelectNodes("//article").Count < 1)
return []; return [];
var urls = document.DocumentNode.SelectNodes("/html/body/article/a[@class='link link-hover']") var urls = document.DocumentNode.SelectNodes("/html/body/article/a[contains(concat(' ',normalize-space(@class),' '),' link ')]")
.Select(elem => elem.GetAttributeValue("href", "")).ToList(); .Select(elem => elem.GetAttributeValue("href", "")).ToList();
List<(Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)> ret = new(); List<(Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)> ret = new();

View File

@ -1,6 +1,7 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema; using System.ComponentModel.DataAnnotations.Schema;
using System.Text; using System.Text;
using log4net;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Newtonsoft.Json; using Newtonsoft.Json;
@ -35,10 +36,15 @@ public class NotificationConnector(string name, string url, Dictionary<string, s
{ {
DefaultRequestHeaders = { { "User-Agent", TrangaSettings.userAgent } } DefaultRequestHeaders = { { "User-Agent", TrangaSettings.userAgent } }
}; };
[JsonIgnore]
[NotMapped]
protected ILog Log = LogManager.GetLogger(name);
public void SendNotification(string title, string notificationText) public void SendNotification(string title, string notificationText)
{ {
CustomWebhookFormatProvider formatProvider = new CustomWebhookFormatProvider(title, notificationText); Log.Info($"Sending notification: {title} - {notificationText}");
CustomWebhookFormatProvider formatProvider = new (title, notificationText);
string formattedUrl = string.Format(formatProvider, Url); string formattedUrl = string.Format(formatProvider, Url);
string formattedBody = string.Format(formatProvider, Body, title, notificationText); string formattedBody = string.Format(formatProvider, Body, title, notificationText);
Dictionary<string, string> formattedHeaders = Headers.ToDictionary(h => h.Key, Dictionary<string, string> formattedHeaders = Headers.ToDictionary(h => h.Key,
@ -48,8 +54,10 @@ public class NotificationConnector(string name, string url, Dictionary<string, s
foreach (var (key, value) in formattedHeaders) foreach (var (key, value) in formattedHeaders)
request.Headers.Add(key, value); request.Headers.Add(key, value);
request.Content = new StringContent(formattedBody); request.Content = new StringContent(formattedBody);
Log.Debug($"Request: {request}");
HttpResponseMessage response = Client.Send(request); HttpResponseMessage response = Client.Send(request);
Log.Debug($"Response status code: {response.StatusCode}");
} }
private class CustomWebhookFormatProvider(string title, string text) : IFormatProvider private class CustomWebhookFormatProvider(string title, string text) : IFormatProvider

View File

@ -12,66 +12,115 @@ public static class Tranga
{ {
public static Thread NotificationSenderThread { get; } = new (NotificationSender); public static Thread NotificationSenderThread { get; } = new (NotificationSender);
public static Thread JobStarterThread { get; } = new (JobStarter); public static Thread JobStarterThread { get; } = new (JobStarter);
private static readonly Dictionary<Thread, Job> RunningJobs = new();
private static readonly ILog Log = LogManager.GetLogger(typeof(Tranga)); private static readonly ILog Log = LogManager.GetLogger(typeof(Tranga));
internal static void StartLogger() internal static void StartLogger()
{ {
BasicConfigurator.Configure(); BasicConfigurator.Configure();
Log.Info("Logger Configured.");
} }
private static void NotificationSender(object? pgsqlContext) private static void NotificationSender(object? serviceProviderObj)
{ {
if(pgsqlContext is null) return; if (serviceProviderObj is null)
PgsqlContext context = (PgsqlContext)pgsqlContext; {
Log.Error("serviceProviderObj is null");
return;
}
IServiceProvider serviceProvider = (IServiceProvider)serviceProviderObj!;
using IServiceScope scope = serviceProvider.CreateScope();
PgsqlContext? context = scope.ServiceProvider.GetService<PgsqlContext>();
if (context is null)
{
Log.Error("PgsqlContext is null");
return;
}
IQueryable<Notification> staleNotifications = context.Notifications.Where(n => n.Urgency < NotificationUrgency.Normal); try
context.Notifications.RemoveRange(staleNotifications); {
context.SaveChanges(); //Removing Notifications from previous runs
IQueryable<Notification> staleNotifications =
context.Notifications.Where(n => n.Urgency < NotificationUrgency.Normal);
context.Notifications.RemoveRange(staleNotifications);
context.SaveChanges();
}
catch (DbUpdateException e)
{
Log.Error("Error removing stale notifications.", e);
}
while (true) while (true)
{ {
SendNotifications(context, NotificationUrgency.High); SendNotifications(serviceProvider, NotificationUrgency.High);
SendNotifications(context, NotificationUrgency.Normal); SendNotifications(serviceProvider, NotificationUrgency.Normal);
SendNotifications(context, NotificationUrgency.Low); SendNotifications(serviceProvider, NotificationUrgency.Low);
context.SaveChanges();
Thread.Sleep(2000); Thread.Sleep(2000);
} }
} }
private static void SendNotifications(PgsqlContext context, NotificationUrgency urgency) private static void SendNotifications(IServiceProvider serviceProvider, NotificationUrgency urgency)
{ {
List<Notification> notifications = context.Notifications.Where(n => n.Urgency == urgency).ToList(); Log.Info($"Sending notifications for {urgency}");
if (notifications.Any()) using IServiceScope scope = serviceProvider.CreateScope();
PgsqlContext? context = scope.ServiceProvider.GetService<PgsqlContext>();
if (context is null)
{ {
DateTime max = notifications.MaxBy(n => n.Date)!.Date; Log.Error("PgsqlContext is null");
if (DateTime.UtcNow.Subtract(max) > TrangaSettings.NotificationUrgencyDelay(urgency)) return;
{ }
foreach (NotificationConnector notificationConnector in context.NotificationConnectors)
{ List<Notification> notifications = context.Notifications.Where(n => n.Urgency == urgency).ToList();
foreach (Notification notification in notifications) if (!notifications.Any())
notificationConnector.SendNotification(notification.Title, notification.Message); return;
}
context.Notifications.RemoveRange(notifications); try
} {
foreach (NotificationConnector notificationConnector in context.NotificationConnectors)
{
foreach (Notification notification in notifications)
notificationConnector.SendNotification(notification.Title, notification.Message);
}
context.Notifications.RemoveRange(notifications);
context.SaveChangesAsync();
}
catch (DbUpdateException e)
{
Log.Error("Error sending notifications.", e);
} }
context.SaveChanges();
} }
private const string TRANGA =
"\n\n" +
" _______ \n" +
"|_ _|.----..---.-..-----..-----..---.-.\n" +
" | | | _|| _ || || _ || _ |\n" +
" |___| |__| |___._||__|__||___ ||___._|\n" +
" |_____| \n\n";
private static readonly Dictionary<Thread, Job> RunningJobs = new();
private static void JobStarter(object? serviceProviderObj) private static void JobStarter(object? serviceProviderObj)
{ {
if(serviceProviderObj is null) return; if (serviceProviderObj is null)
{
Log.Error("serviceProviderObj is null");
return;
}
IServiceProvider serviceProvider = (IServiceProvider)serviceProviderObj; IServiceProvider serviceProvider = (IServiceProvider)serviceProviderObj;
using IServiceScope scope = serviceProvider.CreateScope(); using IServiceScope scope = serviceProvider.CreateScope();
PgsqlContext? context = scope.ServiceProvider.GetService<PgsqlContext>(); PgsqlContext? context = scope.ServiceProvider.GetService<PgsqlContext>();
if (context is null) return; if (context is null)
{
Log.Error("PgsqlContext is null");
return;
}
string TRANGA =
"\n\n _______ \n|_ _|.----..---.-..-----..-----..---.-.\n | | | _|| _ || || _ || _ |\n |___| |__| |___._||__|__||___ ||___._|\n |_____| \n\n";
Log.Info(TRANGA); Log.Info(TRANGA);
Log.Info("JobStarter Thread running.");
while (true) while (true)
{ {
List<Job> completedJobs = context.Jobs.Where(j => j.state >= JobState.Completed).ToList(); List<Job> completedJobs = context.Jobs.Where(j => j.state >= JobState.Completed).ToList();
Log.Debug($"Completed jobs: {completedJobs.Count}");
foreach (Job job in completedJobs) foreach (Job job in completedJobs)
if (job.RecurrenceMs <= 0) if (job.RecurrenceMs <= 0)
context.Jobs.Remove(job); context.Jobs.Remove(job);
@ -82,16 +131,20 @@ public static class Tranga
else else
job.state = JobState.Waiting; job.state = JobState.Waiting;
job.LastExecution = DateTime.UtcNow; job.LastExecution = DateTime.UtcNow;
context.Jobs.Update(job);
} }
List<Job> runJobs = context.Jobs.Where(j => j.state <= JobState.Running && j.Enabled == true).ToList() List<Job> runJobs = context.Jobs.Where(j => j.state <= JobState.Running && j.Enabled == true).ToList()
.Where(j => j.NextExecution < DateTime.UtcNow).ToList(); .Where(j => j.NextExecution < DateTime.UtcNow).ToList();
foreach (Job job in OrderJobs(runJobs, context)) Log.Debug($"Due jobs: {runJobs.Count}");
Log.Debug($"Running jobs: {RunningJobs.Count}");
IEnumerable<Job> orderedJobs = OrderJobs(runJobs, context).ToList();
Log.Debug($"Ordered jobs: {orderedJobs.Count()}");
foreach (Job job in orderedJobs)
{ {
// 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 a Job for that connector is already running, skip it
if (job is DownloadAvailableChaptersJob dncj) if (job is DownloadAvailableChaptersJob dncj)
{ {
if (RunningJobs.Values.Any(j => if (RunningJobs.Values.Any(j =>
@ -113,15 +166,15 @@ public static class Tranga
Thread t = new(() => Thread t = new(() =>
{ {
IEnumerable<Job> newJobs = job.Run(serviceProvider); job.Run(serviceProvider);
}); });
RunningJobs.Add(t, job); RunningJobs.Add(t, job);
t.Start(); t.Start();
context.Jobs.Update(job);
} }
(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();
Log.Debug($"Remove from Threads List: {removeFromThreadsList.Length}");
foreach ((Thread thread, Job job) thread in removeFromThreadsList) foreach ((Thread thread, Job job) thread in removeFromThreadsList)
{ {
RunningJobs.Remove(thread.thread); RunningJobs.Remove(thread.thread);
@ -135,7 +188,7 @@ public static class Tranga
} }
catch (DbUpdateException e) catch (DbUpdateException e)
{ {
Log.Error("Failed saving Job changes.", e);
} }
Thread.Sleep(TrangaSettings.startNewJobTimeoutMs); Thread.Sleep(TrangaSettings.startNewJobTimeoutMs);
} }