mirror of
https://github.com/C9Glax/tranga.git
synced 2025-10-16 18:30:46 +02:00
Compare commits
1 Commits
cuttingedg
...
actions
Author | SHA1 | Date | |
---|---|---|---|
53276e858b |
63
API/Controllers/ActionsController.cs
Normal file
63
API/Controllers/ActionsController.cs
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
using API.Schema.ActionsContext;
|
||||||
|
using Asp.Versioning;
|
||||||
|
using Microsoft.AspNetCore.Http.HttpResults;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using static Microsoft.AspNetCore.Http.StatusCodes;
|
||||||
|
|
||||||
|
namespace API.Controllers;
|
||||||
|
|
||||||
|
[ApiVersion(2)]
|
||||||
|
[ApiController]
|
||||||
|
[Route("v{v:apiVersion}/[controller]")]
|
||||||
|
public class ActionsController(ActionsContext context) : Controller
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the available Action Types (<see cref="ActionRecord.Action"/>) performed by Tranga
|
||||||
|
/// </summary>
|
||||||
|
/// <response code="200">List of performed action-types</response>
|
||||||
|
/// <response code="500">Database error</response>
|
||||||
|
[HttpGet("Types")]
|
||||||
|
[ProducesResponseType(Status200OK)]
|
||||||
|
[ProducesResponseType(Status500InternalServerError)]
|
||||||
|
public async Task<Results<Ok<List<string>>, InternalServerError>> GetAvailableActions()
|
||||||
|
{
|
||||||
|
if (await context.Actions.Select(a => a.Action).Distinct().ToListAsync(HttpContext.RequestAborted) is not
|
||||||
|
{ } actions)
|
||||||
|
return TypedResults.InternalServerError();
|
||||||
|
return TypedResults.Ok(actions);
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed record Interval(DateTime Start, DateTime End);
|
||||||
|
/// <summary>
|
||||||
|
/// Returns <see cref="ActionRecord"/> performed in <see cref="Interval"/>
|
||||||
|
/// </summary>
|
||||||
|
/// <response code="200">List of performed actions</response>
|
||||||
|
/// <response code="500">Database error</response>
|
||||||
|
[HttpPost("Interval")]
|
||||||
|
[ProducesResponseType(Status200OK)]
|
||||||
|
[ProducesResponseType(Status500InternalServerError)]
|
||||||
|
public async Task<Results<Ok<List<ActionRecord>>, InternalServerError>> GetActionsInterval([FromBody]Interval interval)
|
||||||
|
{
|
||||||
|
if (await context.Actions.Where(a => a.PerformedAt >= interval.Start && a.PerformedAt <= interval.End)
|
||||||
|
.ToListAsync(HttpContext.RequestAborted) is not { } actions)
|
||||||
|
return TypedResults.InternalServerError();
|
||||||
|
return TypedResults.Ok(actions);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns <see cref="ActionRecord"/> with type <paramref name="Type"/>
|
||||||
|
/// </summary>
|
||||||
|
/// <response code="200">List of performed actions</response>
|
||||||
|
/// <response code="500">Database error</response>
|
||||||
|
[HttpGet("Type/{Type}")]
|
||||||
|
[ProducesResponseType(Status200OK)]
|
||||||
|
[ProducesResponseType(Status500InternalServerError)]
|
||||||
|
public async Task<Results<Ok<List<ActionRecord>>, InternalServerError>> GetActionsWithType(string Type)
|
||||||
|
{
|
||||||
|
if (await context.Actions.Where(a => a.Action == Type)
|
||||||
|
.ToListAsync(HttpContext.RequestAborted) is not { } actions)
|
||||||
|
return TypedResults.InternalServerError();
|
||||||
|
return TypedResults.Ok(actions);
|
||||||
|
}
|
||||||
|
}
|
@@ -1,4 +1,6 @@
|
|||||||
using API.Controllers.DTOs;
|
using API.Controllers.DTOs;
|
||||||
|
using API.Schema.ActionsContext;
|
||||||
|
using API.Schema.ActionsContext.Actions;
|
||||||
using API.Schema.MangaContext;
|
using API.Schema.MangaContext;
|
||||||
using API.Workers;
|
using API.Workers;
|
||||||
using API.Workers.MangaDownloadWorkers;
|
using API.Workers.MangaDownloadWorkers;
|
||||||
@@ -11,6 +13,7 @@ using Soenneker.Utils.String.NeedlemanWunsch;
|
|||||||
using static Microsoft.AspNetCore.Http.StatusCodes;
|
using static Microsoft.AspNetCore.Http.StatusCodes;
|
||||||
using AltTitle = API.Controllers.DTOs.AltTitle;
|
using AltTitle = API.Controllers.DTOs.AltTitle;
|
||||||
using Author = API.Controllers.DTOs.Author;
|
using Author = API.Controllers.DTOs.Author;
|
||||||
|
using Chapter = API.Schema.MangaContext.Chapter;
|
||||||
using Link = API.Controllers.DTOs.Link;
|
using Link = API.Controllers.DTOs.Link;
|
||||||
using Manga = API.Controllers.DTOs.Manga;
|
using Manga = API.Controllers.DTOs.Manga;
|
||||||
|
|
||||||
@@ -21,7 +24,7 @@ namespace API.Controllers;
|
|||||||
[ApiVersion(2)]
|
[ApiVersion(2)]
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Route("v{v:apiVersion}/[controller]")]
|
[Route("v{v:apiVersion}/[controller]")]
|
||||||
public class MangaController(MangaContext context) : Controller
|
public class MangaController(MangaContext context, ActionsContext actionsContext) : Controller
|
||||||
{
|
{
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -175,12 +178,7 @@ public class MangaController(MangaContext context) : Controller
|
|||||||
|
|
||||||
if (await manga.GetCoverImage(cache, HttpContext.RequestAborted) is not { } data)
|
if (await manga.GetCoverImage(cache, HttpContext.RequestAborted) is not { } data)
|
||||||
{
|
{
|
||||||
if (Tranga.GetRunningWorkers().Any(worker => worker is DownloadCoverFromMangaconnectorWorker w && context.MangaConnectorToManga.Find(w.MangaConnectorIdId)?.ObjId == MangaId))
|
return TypedResults.NotFound("Image not in cache");
|
||||||
{
|
|
||||||
Response.Headers.Append("Retry-After","2");
|
|
||||||
return TypedResults.StatusCode(Status503ServiceUnavailable);
|
|
||||||
}
|
|
||||||
return TypedResults.NoContent();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
DateTime lastModified = data.fileInfo.LastWriteTime;
|
DateTime lastModified = data.fileInfo.LastWriteTime;
|
||||||
@@ -199,12 +197,17 @@ public class MangaController(MangaContext context) : Controller
|
|||||||
/// <param name="LibraryId"><see cref="DTOs.FileLibrary"/>.Key</param>
|
/// <param name="LibraryId"><see cref="DTOs.FileLibrary"/>.Key</param>
|
||||||
/// <response code="202">Folder is going to be moved</response>
|
/// <response code="202">Folder is going to be moved</response>
|
||||||
/// <response code="404"><paramref name="MangaId"/> or <paramref name="LibraryId"/> not found</response>
|
/// <response code="404"><paramref name="MangaId"/> or <paramref name="LibraryId"/> not found</response>
|
||||||
|
/// <response code="500">Error during Database Operation</response>
|
||||||
[HttpPost("{MangaId}/ChangeLibrary/{LibraryId}")]
|
[HttpPost("{MangaId}/ChangeLibrary/{LibraryId}")]
|
||||||
[ProducesResponseType(Status200OK)]
|
[ProducesResponseType(Status200OK)]
|
||||||
[ProducesResponseType<string>(Status404NotFound, "text/plain")]
|
[ProducesResponseType<string>(Status404NotFound, "text/plain")]
|
||||||
public async Task<Results<Ok, NotFound<string>>> ChangeLibrary(string MangaId, string LibraryId)
|
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
|
||||||
|
public async Task<Results<Ok, NotFound<string>, InternalServerError<string>>> ChangeLibrary(string MangaId, string LibraryId)
|
||||||
{
|
{
|
||||||
if (await context.Mangas.FirstOrDefaultAsync(m => m.Key == MangaId, HttpContext.RequestAborted) is not { } manga)
|
if (await context.Mangas
|
||||||
|
.Include(m => m.Library)
|
||||||
|
.Include(m => m.Chapters)
|
||||||
|
.FirstOrDefaultAsync(m => m.Key == MangaId, HttpContext.RequestAborted) is not { } manga)
|
||||||
return TypedResults.NotFound(nameof(MangaId));
|
return TypedResults.NotFound(nameof(MangaId));
|
||||||
if (await context.FileLibraries.FirstOrDefaultAsync(l => l.Key == LibraryId, HttpContext.RequestAborted) is not { } library)
|
if (await context.FileLibraries.FirstOrDefaultAsync(l => l.Key == LibraryId, HttpContext.RequestAborted) is not { } library)
|
||||||
return TypedResults.NotFound(nameof(LibraryId));
|
return TypedResults.NotFound(nameof(LibraryId));
|
||||||
@@ -212,9 +215,18 @@ public class MangaController(MangaContext context) : Controller
|
|||||||
if(manga.LibraryId == library.Key)
|
if(manga.LibraryId == library.Key)
|
||||||
return TypedResults.Ok();
|
return TypedResults.Ok();
|
||||||
|
|
||||||
MoveMangaLibraryWorker moveLibrary = new(manga, library);
|
Dictionary<Chapter, string?> oldPaths = manga.Chapters.Where(ch => ch.Downloaded).ToDictionary(ch => ch, ch => ch.FullArchiveFilePath);
|
||||||
|
manga.Library = library;
|
||||||
|
Dictionary<Chapter, string?> newPaths = oldPaths.ToDictionary(kv => kv.Key, kv => kv.Key.FullArchiveFilePath);
|
||||||
|
IEnumerable<MoveFileOrFolderWorker> workers = oldPaths.Select(kv => new MoveFileOrFolderWorker(newPaths[kv.Key]!, kv.Value!));
|
||||||
|
Tranga.AddWorkers(workers);
|
||||||
|
|
||||||
Tranga.AddWorkers([moveLibrary]);
|
if(await context.Sync(HttpContext.RequestAborted, GetType(), "Move Manga") is { success: false } mangaContextResult)
|
||||||
|
return TypedResults.InternalServerError(mangaContextResult.exceptionMessage);
|
||||||
|
|
||||||
|
actionsContext.Actions.Add(new LibraryMovedActionRecord(manga, library));
|
||||||
|
if(await actionsContext.Sync(HttpContext.RequestAborted, GetType(), "Move Manga") is { success: false } actionsContextResult)
|
||||||
|
return TypedResults.InternalServerError(actionsContextResult.exceptionMessage);
|
||||||
|
|
||||||
return TypedResults.Ok();
|
return TypedResults.Ok();
|
||||||
}
|
}
|
||||||
|
156
API/Migrations/Actions/20251016005257_Actions.Designer.cs
generated
Normal file
156
API/Migrations/Actions/20251016005257_Actions.Designer.cs
generated
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using API.Schema.ActionsContext;
|
||||||
|
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.Actions
|
||||||
|
{
|
||||||
|
[DbContext(typeof(ActionsContext))]
|
||||||
|
[Migration("20251016005257_Actions")]
|
||||||
|
partial class Actions
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasAnnotation("ProductVersion", "9.0.9")
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||||
|
|
||||||
|
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Schema.ActionsContext.ActionRecord", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Key")
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("character varying(64)");
|
||||||
|
|
||||||
|
b.Property<string>("Action")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("character varying(128)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("PerformedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.HasKey("Key");
|
||||||
|
|
||||||
|
b.ToTable("Actions");
|
||||||
|
|
||||||
|
b.HasDiscriminator<string>("Action").HasValue("ActionRecord");
|
||||||
|
|
||||||
|
b.UseTphMappingStrategy();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Schema.ActionsContext.Actions.ChapterDownloadedActionRecord", b =>
|
||||||
|
{
|
||||||
|
b.HasBaseType("API.Schema.ActionsContext.ActionRecord");
|
||||||
|
|
||||||
|
b.Property<string>("ChapterId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("character varying(64)");
|
||||||
|
|
||||||
|
b.HasDiscriminator().HasValue("Chapter.Downloaded");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Schema.ActionsContext.Actions.ChaptersRetrievedActionRecord", b =>
|
||||||
|
{
|
||||||
|
b.HasBaseType("API.Schema.ActionsContext.ActionRecord");
|
||||||
|
|
||||||
|
b.Property<string>("MangaId")
|
||||||
|
.IsRequired()
|
||||||
|
.ValueGeneratedOnUpdateSometimes()
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("character varying(64)");
|
||||||
|
|
||||||
|
b.HasDiscriminator().HasValue("Manga.ChaptersRetrieved");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Schema.ActionsContext.Actions.CoverDownloadedActionRecord", b =>
|
||||||
|
{
|
||||||
|
b.HasBaseType("API.Schema.ActionsContext.ActionRecord");
|
||||||
|
|
||||||
|
b.Property<string>("Filename")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(1024)
|
||||||
|
.HasColumnType("character varying(1024)");
|
||||||
|
|
||||||
|
b.Property<string>("MangaId")
|
||||||
|
.IsRequired()
|
||||||
|
.ValueGeneratedOnUpdateSometimes()
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("character varying(64)");
|
||||||
|
|
||||||
|
b.HasDiscriminator().HasValue("Manga.CoverDownloaded");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Schema.ActionsContext.Actions.DataMovedActionRecord", b =>
|
||||||
|
{
|
||||||
|
b.HasBaseType("API.Schema.ActionsContext.ActionRecord");
|
||||||
|
|
||||||
|
b.Property<string>("From")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(2048)
|
||||||
|
.HasColumnType("character varying(2048)");
|
||||||
|
|
||||||
|
b.Property<string>("To")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(2048)
|
||||||
|
.HasColumnType("character varying(2048)");
|
||||||
|
|
||||||
|
b.HasDiscriminator().HasValue("Tranga.DataMoved");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Schema.ActionsContext.Actions.LibraryMovedActionRecord", b =>
|
||||||
|
{
|
||||||
|
b.HasBaseType("API.Schema.ActionsContext.ActionRecord");
|
||||||
|
|
||||||
|
b.Property<string>("FileLibraryId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("character varying(64)");
|
||||||
|
|
||||||
|
b.Property<string>("MangaId")
|
||||||
|
.IsRequired()
|
||||||
|
.ValueGeneratedOnUpdateSometimes()
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("character varying(64)");
|
||||||
|
|
||||||
|
b.HasDiscriminator().HasValue("Manga.LibraryMoved");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Schema.ActionsContext.Actions.MetadataUpdatedActionRecord", b =>
|
||||||
|
{
|
||||||
|
b.HasBaseType("API.Schema.ActionsContext.ActionRecord");
|
||||||
|
|
||||||
|
b.Property<string>("MangaId")
|
||||||
|
.IsRequired()
|
||||||
|
.ValueGeneratedOnUpdateSometimes()
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("character varying(64)");
|
||||||
|
|
||||||
|
b.Property<string>("MetadataFetcher")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(1024)
|
||||||
|
.HasColumnType("character varying(1024)");
|
||||||
|
|
||||||
|
b.HasDiscriminator().HasValue("Manga.MetadataUpdated");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Schema.ActionsContext.Actions.StartupActionRecord", b =>
|
||||||
|
{
|
||||||
|
b.HasBaseType("API.Schema.ActionsContext.ActionRecord");
|
||||||
|
|
||||||
|
b.HasDiscriminator().HasValue("Tranga.Started");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
42
API/Migrations/Actions/20251016005257_Actions.cs
Normal file
42
API/Migrations/Actions/20251016005257_Actions.cs
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace API.Migrations.Actions
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class Actions : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "Actions",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Key = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
|
||||||
|
Action = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: false),
|
||||||
|
PerformedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||||
|
ChapterId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: true),
|
||||||
|
MangaId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: true),
|
||||||
|
Filename = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: true),
|
||||||
|
From = table.Column<string>(type: "character varying(2048)", maxLength: 2048, nullable: true),
|
||||||
|
To = table.Column<string>(type: "character varying(2048)", maxLength: 2048, nullable: true),
|
||||||
|
FileLibraryId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: true),
|
||||||
|
MetadataFetcher = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_Actions", x => x.Key);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "Actions");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
153
API/Migrations/Actions/ActionsContextModelSnapshot.cs
Normal file
153
API/Migrations/Actions/ActionsContextModelSnapshot.cs
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using API.Schema.ActionsContext;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace API.Migrations.Actions
|
||||||
|
{
|
||||||
|
[DbContext(typeof(ActionsContext))]
|
||||||
|
partial class ActionsContextModelSnapshot : ModelSnapshot
|
||||||
|
{
|
||||||
|
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasAnnotation("ProductVersion", "9.0.9")
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||||
|
|
||||||
|
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Schema.ActionsContext.ActionRecord", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Key")
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("character varying(64)");
|
||||||
|
|
||||||
|
b.Property<string>("Action")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("character varying(128)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("PerformedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.HasKey("Key");
|
||||||
|
|
||||||
|
b.ToTable("Actions");
|
||||||
|
|
||||||
|
b.HasDiscriminator<string>("Action").HasValue("ActionRecord");
|
||||||
|
|
||||||
|
b.UseTphMappingStrategy();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Schema.ActionsContext.Actions.ChapterDownloadedActionRecord", b =>
|
||||||
|
{
|
||||||
|
b.HasBaseType("API.Schema.ActionsContext.ActionRecord");
|
||||||
|
|
||||||
|
b.Property<string>("ChapterId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("character varying(64)");
|
||||||
|
|
||||||
|
b.HasDiscriminator().HasValue("Chapter.Downloaded");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Schema.ActionsContext.Actions.ChaptersRetrievedActionRecord", b =>
|
||||||
|
{
|
||||||
|
b.HasBaseType("API.Schema.ActionsContext.ActionRecord");
|
||||||
|
|
||||||
|
b.Property<string>("MangaId")
|
||||||
|
.IsRequired()
|
||||||
|
.ValueGeneratedOnUpdateSometimes()
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("character varying(64)");
|
||||||
|
|
||||||
|
b.HasDiscriminator().HasValue("Manga.ChaptersRetrieved");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Schema.ActionsContext.Actions.CoverDownloadedActionRecord", b =>
|
||||||
|
{
|
||||||
|
b.HasBaseType("API.Schema.ActionsContext.ActionRecord");
|
||||||
|
|
||||||
|
b.Property<string>("Filename")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(1024)
|
||||||
|
.HasColumnType("character varying(1024)");
|
||||||
|
|
||||||
|
b.Property<string>("MangaId")
|
||||||
|
.IsRequired()
|
||||||
|
.ValueGeneratedOnUpdateSometimes()
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("character varying(64)");
|
||||||
|
|
||||||
|
b.HasDiscriminator().HasValue("Manga.CoverDownloaded");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Schema.ActionsContext.Actions.DataMovedActionRecord", b =>
|
||||||
|
{
|
||||||
|
b.HasBaseType("API.Schema.ActionsContext.ActionRecord");
|
||||||
|
|
||||||
|
b.Property<string>("From")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(2048)
|
||||||
|
.HasColumnType("character varying(2048)");
|
||||||
|
|
||||||
|
b.Property<string>("To")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(2048)
|
||||||
|
.HasColumnType("character varying(2048)");
|
||||||
|
|
||||||
|
b.HasDiscriminator().HasValue("Tranga.DataMoved");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Schema.ActionsContext.Actions.LibraryMovedActionRecord", b =>
|
||||||
|
{
|
||||||
|
b.HasBaseType("API.Schema.ActionsContext.ActionRecord");
|
||||||
|
|
||||||
|
b.Property<string>("FileLibraryId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("character varying(64)");
|
||||||
|
|
||||||
|
b.Property<string>("MangaId")
|
||||||
|
.IsRequired()
|
||||||
|
.ValueGeneratedOnUpdateSometimes()
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("character varying(64)");
|
||||||
|
|
||||||
|
b.HasDiscriminator().HasValue("Manga.LibraryMoved");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Schema.ActionsContext.Actions.MetadataUpdatedActionRecord", b =>
|
||||||
|
{
|
||||||
|
b.HasBaseType("API.Schema.ActionsContext.ActionRecord");
|
||||||
|
|
||||||
|
b.Property<string>("MangaId")
|
||||||
|
.IsRequired()
|
||||||
|
.ValueGeneratedOnUpdateSometimes()
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("character varying(64)");
|
||||||
|
|
||||||
|
b.Property<string>("MetadataFetcher")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(1024)
|
||||||
|
.HasColumnType("character varying(1024)");
|
||||||
|
|
||||||
|
b.HasDiscriminator().HasValue("Manga.MetadataUpdated");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Schema.ActionsContext.Actions.StartupActionRecord", b =>
|
||||||
|
{
|
||||||
|
b.HasBaseType("API.Schema.ActionsContext.ActionRecord");
|
||||||
|
|
||||||
|
b.HasDiscriminator().HasValue("Tranga.Started");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -1,5 +1,7 @@
|
|||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using API;
|
using API;
|
||||||
|
using API.Schema.ActionsContext;
|
||||||
|
using API.Schema.ActionsContext.Actions;
|
||||||
using API.Schema.LibraryContext;
|
using API.Schema.LibraryContext;
|
||||||
using API.Schema.MangaContext;
|
using API.Schema.MangaContext;
|
||||||
using API.Schema.NotificationsContext;
|
using API.Schema.NotificationsContext;
|
||||||
@@ -92,6 +94,8 @@ builder.Services.AddDbContext<NotificationsContext>(options =>
|
|||||||
options.UseNpgsql(connectionStringBuilder.ConnectionString));
|
options.UseNpgsql(connectionStringBuilder.ConnectionString));
|
||||||
builder.Services.AddDbContext<LibraryContext>(options =>
|
builder.Services.AddDbContext<LibraryContext>(options =>
|
||||||
options.UseNpgsql(connectionStringBuilder.ConnectionString));
|
options.UseNpgsql(connectionStringBuilder.ConnectionString));
|
||||||
|
builder.Services.AddDbContext<ActionsContext>(options =>
|
||||||
|
options.UseNpgsql(connectionStringBuilder.ConnectionString));
|
||||||
|
|
||||||
builder.Services.Configure<Microsoft.AspNetCore.Http.Json.JsonOptions>(options =>
|
builder.Services.Configure<Microsoft.AspNetCore.Http.Json.JsonOptions>(options =>
|
||||||
{
|
{
|
||||||
@@ -180,6 +184,15 @@ try //Connect to DB and apply migrations
|
|||||||
|
|
||||||
await context.Sync(CancellationToken.None, reason: "Startup library");
|
await context.Sync(CancellationToken.None, reason: "Startup library");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
using (IServiceScope scope = app.Services.CreateScope())
|
||||||
|
{
|
||||||
|
ActionsContext context = scope.ServiceProvider.GetRequiredService<ActionsContext>();
|
||||||
|
await context.Database.MigrateAsync(CancellationToken.None);
|
||||||
|
context.Actions.Add(new StartupActionRecord());
|
||||||
|
|
||||||
|
await context.Sync(CancellationToken.None, reason: "Startup actions");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
|
19
API/Schema/ActionsContext/ActionRecord.cs
Normal file
19
API/Schema/ActionsContext/ActionRecord.cs
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace API.Schema.ActionsContext;
|
||||||
|
|
||||||
|
[PrimaryKey("Key")]
|
||||||
|
public abstract class ActionRecord(string action, DateTime performedAt) : Identifiable
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Constant string that describes the performed Action
|
||||||
|
/// </summary>
|
||||||
|
[StringLength(128)]
|
||||||
|
public string Action { get; init; } = action;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// UTC Time when Action was performed
|
||||||
|
/// </summary>
|
||||||
|
public DateTime PerformedAt { get; init; } = performedAt;
|
||||||
|
}
|
@@ -0,0 +1,17 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using API.Schema.MangaContext;
|
||||||
|
|
||||||
|
namespace API.Schema.ActionsContext.Actions;
|
||||||
|
|
||||||
|
public sealed class ChapterDownloadedActionRecord(string action, DateTime performedAt, string chapterId) : ActionRecord(action, performedAt)
|
||||||
|
{
|
||||||
|
public ChapterDownloadedActionRecord(Chapter chapter) : this(ChapterDownloadedAction, DateTime.UtcNow, chapter.Key) { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Chapter that was downloaded
|
||||||
|
/// </summary>
|
||||||
|
[StringLength(64)]
|
||||||
|
public string ChapterId { get; init; } = chapterId;
|
||||||
|
|
||||||
|
public const string ChapterDownloadedAction = "Chapter.Downloaded";
|
||||||
|
}
|
@@ -0,0 +1,12 @@
|
|||||||
|
using API.Schema.ActionsContext.Actions.Generic;
|
||||||
|
using API.Schema.MangaContext;
|
||||||
|
|
||||||
|
namespace API.Schema.ActionsContext.Actions;
|
||||||
|
|
||||||
|
public sealed class ChaptersRetrievedActionRecord(string action, DateTime performedAt, string mangaId)
|
||||||
|
: ActionWithMangaRecord(action, performedAt, mangaId)
|
||||||
|
{
|
||||||
|
public ChaptersRetrievedActionRecord(Manga manga) : this(ChaptersRetrievedAction, DateTime.UtcNow, manga.Key) { }
|
||||||
|
|
||||||
|
public const string ChaptersRetrievedAction = "Manga.ChaptersRetrieved";
|
||||||
|
}
|
@@ -0,0 +1,19 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using API.Schema.ActionsContext.Actions.Generic;
|
||||||
|
using API.Schema.MangaContext;
|
||||||
|
|
||||||
|
namespace API.Schema.ActionsContext.Actions;
|
||||||
|
|
||||||
|
public sealed class CoverDownloadedActionRecord(string action, DateTime performedAt, string mangaId, string filename)
|
||||||
|
: ActionWithMangaRecord(action, performedAt, mangaId)
|
||||||
|
{
|
||||||
|
public CoverDownloadedActionRecord(Manga manga, string filename) : this(CoverDownloadedAction, DateTime.UtcNow, manga.Key, filename) { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Filename on disk
|
||||||
|
/// </summary>
|
||||||
|
[StringLength(1024)]
|
||||||
|
public string Filename { get; init; } = filename;
|
||||||
|
|
||||||
|
public const string CoverDownloadedAction = "Manga.CoverDownloaded";
|
||||||
|
}
|
22
API/Schema/ActionsContext/Actions/DataMovedActionRecord.cs
Normal file
22
API/Schema/ActionsContext/Actions/DataMovedActionRecord.cs
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace API.Schema.ActionsContext.Actions;
|
||||||
|
|
||||||
|
public sealed class DataMovedActionRecord(string action, DateTime performedAt, string from, string to) : ActionRecord(action, performedAt)
|
||||||
|
{
|
||||||
|
public DataMovedActionRecord(string from, string to) : this(DataMovedAction, DateTime.UtcNow, from, to) { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// From path
|
||||||
|
/// </summary>
|
||||||
|
[StringLength(2048)]
|
||||||
|
public string From { get; init; } = from;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// To path
|
||||||
|
/// </summary>
|
||||||
|
[StringLength(2048)]
|
||||||
|
public string To { get; init; } = to;
|
||||||
|
|
||||||
|
public const string DataMovedAction = "Tranga.DataMoved";
|
||||||
|
}
|
@@ -0,0 +1,15 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using API.Schema.MangaContext;
|
||||||
|
|
||||||
|
namespace API.Schema.ActionsContext.Actions.Generic;
|
||||||
|
|
||||||
|
public abstract class ActionWithMangaRecord(string action, DateTime performedAt, string mangaId) : ActionRecord(action, performedAt)
|
||||||
|
{
|
||||||
|
protected ActionWithMangaRecord(string action, DateTime performedAt, Manga manga) : this(action, performedAt, manga.Key) { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// <see cref="Schema.MangaContext.Manga"/> for which the cover was downloaded
|
||||||
|
/// </summary>
|
||||||
|
[StringLength(64)]
|
||||||
|
public string MangaId { get; init; } = mangaId;
|
||||||
|
}
|
@@ -0,0 +1,18 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using API.Schema.ActionsContext.Actions.Generic;
|
||||||
|
using API.Schema.MangaContext;
|
||||||
|
|
||||||
|
namespace API.Schema.ActionsContext.Actions;
|
||||||
|
|
||||||
|
public sealed class LibraryMovedActionRecord(string action, DateTime performedAt, string mangaId, string fileLibraryId) : ActionWithMangaRecord(action, performedAt, mangaId)
|
||||||
|
{
|
||||||
|
public LibraryMovedActionRecord(Manga manga, FileLibrary library) : this(LibraryMovedAction, DateTime.UtcNow, manga.Key, library.Key) { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// <see cref="Schema.MangaContext.FileLibrary"/> for which the cover was downloaded
|
||||||
|
/// </summary>
|
||||||
|
[StringLength(64)]
|
||||||
|
public string FileLibraryId { get; init; } = fileLibraryId;
|
||||||
|
|
||||||
|
public const string LibraryMovedAction = "Manga.LibraryMoved";
|
||||||
|
}
|
@@ -0,0 +1,20 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using API.Schema.ActionsContext.Actions.Generic;
|
||||||
|
using API.Schema.MangaContext;
|
||||||
|
using API.Schema.MangaContext.MetadataFetchers;
|
||||||
|
|
||||||
|
namespace API.Schema.ActionsContext.Actions;
|
||||||
|
|
||||||
|
public sealed class MetadataUpdatedActionRecord(string action, DateTime performedAt, string mangaId, string metadataFetcher)
|
||||||
|
: ActionWithMangaRecord(action, performedAt, mangaId)
|
||||||
|
{
|
||||||
|
public MetadataUpdatedActionRecord(Manga manga, MetadataFetcher fetcher) : this(MetadataUpdatedAction, DateTime.UtcNow, manga.Key, fetcher.Name) { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Filename on disk
|
||||||
|
/// </summary>
|
||||||
|
[StringLength(1024)]
|
||||||
|
public string MetadataFetcher { get; init; } = metadataFetcher;
|
||||||
|
|
||||||
|
public const string MetadataUpdatedAction = "Manga.MetadataUpdated";
|
||||||
|
}
|
8
API/Schema/ActionsContext/Actions/StartupActionRecord.cs
Normal file
8
API/Schema/ActionsContext/Actions/StartupActionRecord.cs
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
namespace API.Schema.ActionsContext.Actions;
|
||||||
|
|
||||||
|
public sealed class StartupActionRecord(string action, DateTime performedAt) : ActionRecord(action, performedAt)
|
||||||
|
{
|
||||||
|
public StartupActionRecord() : this(StartupAction, DateTime.UtcNow) { }
|
||||||
|
|
||||||
|
public const string StartupAction = "Tranga.Started";
|
||||||
|
}
|
22
API/Schema/ActionsContext/ActionsContext.cs
Normal file
22
API/Schema/ActionsContext/ActionsContext.cs
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
using API.Schema.ActionsContext.Actions;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace API.Schema.ActionsContext;
|
||||||
|
|
||||||
|
public class ActionsContext(DbContextOptions<ActionsContext> options) : TrangaBaseContext<ActionsContext>(options)
|
||||||
|
{
|
||||||
|
public DbSet<ActionRecord> Actions { get; set; }
|
||||||
|
|
||||||
|
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
modelBuilder.Entity<ActionRecord>()
|
||||||
|
.HasDiscriminator(a => a.Action)
|
||||||
|
.HasValue<ChapterDownloadedActionRecord>(ChapterDownloadedActionRecord.ChapterDownloadedAction)
|
||||||
|
.HasValue<CoverDownloadedActionRecord>(CoverDownloadedActionRecord.CoverDownloadedAction)
|
||||||
|
.HasValue<ChaptersRetrievedActionRecord>(ChaptersRetrievedActionRecord.ChaptersRetrievedAction)
|
||||||
|
.HasValue<MetadataUpdatedActionRecord>(MetadataUpdatedActionRecord.MetadataUpdatedAction)
|
||||||
|
.HasValue<DataMovedActionRecord>(DataMovedActionRecord.DataMovedAction)
|
||||||
|
.HasValue<LibraryMovedActionRecord>(LibraryMovedActionRecord.LibraryMovedAction)
|
||||||
|
.HasValue<StartupActionRecord>(StartupActionRecord.StartupAction);
|
||||||
|
}
|
||||||
|
}
|
@@ -2,10 +2,8 @@
|
|||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using API.MangaConnectors;
|
using API.MangaConnectors;
|
||||||
using API.MangaDownloadClients;
|
using API.MangaDownloadClients;
|
||||||
using API.Schema.LibraryContext;
|
|
||||||
using API.Schema.MangaContext;
|
using API.Schema.MangaContext;
|
||||||
using API.Schema.MangaContext.MetadataFetchers;
|
using API.Schema.MangaContext.MetadataFetchers;
|
||||||
using API.Schema.NotificationsContext;
|
|
||||||
using API.Workers;
|
using API.Workers;
|
||||||
using API.Workers.MangaDownloadWorkers;
|
using API.Workers.MangaDownloadWorkers;
|
||||||
using API.Workers.PeriodicWorkers;
|
using API.Workers.PeriodicWorkers;
|
||||||
@@ -146,21 +144,15 @@ public static class Tranga
|
|||||||
Log.Warn($"{worker}: Max worker concurrency reached ({Settings.MaxConcurrentWorkers})! Waiting {Settings.WorkCycleTimeoutMs}ms...");
|
Log.Warn($"{worker}: Max worker concurrency reached ({Settings.MaxConcurrentWorkers})! Waiting {Settings.WorkCycleTimeoutMs}ms...");
|
||||||
Thread.Sleep(Settings.WorkCycleTimeoutMs);
|
Thread.Sleep(Settings.WorkCycleTimeoutMs);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (worker is BaseWorkerWithContext<MangaContext> mangaContextWorker)
|
if (worker is BaseWorkerWithContexts withContexts)
|
||||||
{
|
{
|
||||||
mangaContextWorker.SetScope(ServiceProvider.CreateScope());
|
RunningWorkers.TryAdd(withContexts, withContexts.DoWork(ServiceProvider.CreateScope(), afterWorkCallback));
|
||||||
RunningWorkers.TryAdd(mangaContextWorker, mangaContextWorker.DoWork(afterWorkCallback));
|
}
|
||||||
}else if (worker is BaseWorkerWithContext<NotificationsContext> notificationContextWorker)
|
else
|
||||||
{
|
{
|
||||||
notificationContextWorker.SetScope(ServiceProvider.CreateScope());
|
|
||||||
RunningWorkers.TryAdd(notificationContextWorker, notificationContextWorker.DoWork(afterWorkCallback));
|
|
||||||
}else if (worker is BaseWorkerWithContext<LibraryContext> libraryContextWorker)
|
|
||||||
{
|
|
||||||
libraryContextWorker.SetScope(ServiceProvider.CreateScope());
|
|
||||||
RunningWorkers.TryAdd(libraryContextWorker, libraryContextWorker.DoWork(afterWorkCallback));
|
|
||||||
}else
|
|
||||||
RunningWorkers.TryAdd(worker, worker.DoWork(afterWorkCallback));
|
RunningWorkers.TryAdd(worker, worker.DoWork(afterWorkCallback));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Action DefaultAfterWork(BaseWorker worker, Action? callback = null) => () =>
|
private static Action DefaultAfterWork(BaseWorker worker, Action? callback = null) => () =>
|
||||||
|
@@ -1,24 +0,0 @@
|
|||||||
using System.Configuration;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
|
|
||||||
namespace API.Workers;
|
|
||||||
|
|
||||||
public abstract class BaseWorkerWithContext<T>(IEnumerable<BaseWorker>? dependsOn = null) : BaseWorker(dependsOn) where T : DbContext
|
|
||||||
{
|
|
||||||
protected T DbContext = null!;
|
|
||||||
private IServiceScope? _scope;
|
|
||||||
|
|
||||||
public void SetScope(IServiceScope scope)
|
|
||||||
{
|
|
||||||
this._scope = scope;
|
|
||||||
this.DbContext = scope.ServiceProvider.GetRequiredService<T>();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <exception cref="ConfigurationErrorsException">Scope has not been set. <see cref="SetScope"/></exception>
|
|
||||||
public new Task<BaseWorker[]> DoWork()
|
|
||||||
{
|
|
||||||
if (DbContext is null)
|
|
||||||
throw new ConfigurationErrorsException("Scope has not been set.");
|
|
||||||
return base.DoWork();
|
|
||||||
}
|
|
||||||
}
|
|
28
API/Workers/BaseWorkerWithContexts.cs
Normal file
28
API/Workers/BaseWorkerWithContexts.cs
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace API.Workers;
|
||||||
|
|
||||||
|
public abstract class BaseWorkerWithContexts(IEnumerable<BaseWorker>? dependsOn = null) : BaseWorker(dependsOn)
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the context of requested type <typeparamref name="T"/>
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="scope"></param>
|
||||||
|
/// <typeparam name="T">Type of <see cref="DbContext"/></typeparam>
|
||||||
|
/// <returns>Context in scope</returns>
|
||||||
|
/// <exception cref="Exception">Scope not set</exception>
|
||||||
|
protected T GetContext<T>(IServiceScope scope) where T : DbContext
|
||||||
|
{
|
||||||
|
if (scope is not { } serviceScope)
|
||||||
|
throw new Exception("Scope not set!");
|
||||||
|
return serviceScope.ServiceProvider.GetRequiredService<T>();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract void SetContexts(IServiceScope serviceScope);
|
||||||
|
|
||||||
|
public new Task<BaseWorker[]> DoWork(IServiceScope serviceScope, Action? callback = null)
|
||||||
|
{
|
||||||
|
SetContexts(serviceScope);
|
||||||
|
return base.DoWork(callback);
|
||||||
|
}
|
||||||
|
}
|
@@ -1,8 +1,10 @@
|
|||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.IO.Compression;
|
using System.IO.Compression;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using API.MangaConnectors;
|
using API.MangaConnectors;
|
||||||
using API.MangaDownloadClients;
|
using API.Schema.ActionsContext;
|
||||||
|
using API.Schema.ActionsContext.Actions;
|
||||||
using API.Schema.MangaContext;
|
using API.Schema.MangaContext;
|
||||||
using API.Workers.PeriodicWorkers;
|
using API.Workers.PeriodicWorkers;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
@@ -21,14 +23,26 @@ namespace API.Workers.MangaDownloadWorkers;
|
|||||||
/// <param name="chId"></param>
|
/// <param name="chId"></param>
|
||||||
/// <param name="dependsOn"></param>
|
/// <param name="dependsOn"></param>
|
||||||
public class DownloadChapterFromMangaconnectorWorker(MangaConnectorId<Chapter> chId, IEnumerable<BaseWorker>? dependsOn = null)
|
public class DownloadChapterFromMangaconnectorWorker(MangaConnectorId<Chapter> chId, IEnumerable<BaseWorker>? dependsOn = null)
|
||||||
: BaseWorkerWithContext<MangaContext>(dependsOn)
|
: BaseWorkerWithContexts(dependsOn)
|
||||||
{
|
{
|
||||||
private readonly string _mangaConnectorIdId = chId.Key;
|
private readonly string _mangaConnectorIdId = chId.Key;
|
||||||
|
|
||||||
|
[SuppressMessage("ReSharper", "InconsistentNaming")]
|
||||||
|
private MangaContext MangaContext = null!;
|
||||||
|
[SuppressMessage("ReSharper", "InconsistentNaming")]
|
||||||
|
private ActionsContext ActionsContext = null!;
|
||||||
|
|
||||||
|
protected override void SetContexts(IServiceScope serviceScope)
|
||||||
|
{
|
||||||
|
MangaContext = GetContext<MangaContext>(serviceScope);
|
||||||
|
ActionsContext = GetContext<ActionsContext>(serviceScope);
|
||||||
|
}
|
||||||
|
|
||||||
protected override async Task<BaseWorker[]> DoWorkInternal()
|
protected override async Task<BaseWorker[]> DoWorkInternal()
|
||||||
{
|
{
|
||||||
Log.Debug($"Downloading chapter for MangaConnectorId {_mangaConnectorIdId}...");
|
Log.Debug($"Downloading chapter for MangaConnectorId {_mangaConnectorIdId}...");
|
||||||
// Getting MangaConnector info
|
// Getting MangaConnector info
|
||||||
if (await DbContext.MangaConnectorToChapter
|
if (await MangaContext.MangaConnectorToChapter
|
||||||
.Include(id => id.Obj)
|
.Include(id => id.Obj)
|
||||||
.ThenInclude(c => c.ParentManga)
|
.ThenInclude(c => c.ParentManga)
|
||||||
.ThenInclude(m => m.Library)
|
.ThenInclude(m => m.Library)
|
||||||
@@ -39,7 +53,7 @@ public class DownloadChapterFromMangaconnectorWorker(MangaConnectorId<Chapter> c
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check if Chapter already exists...
|
// Check if Chapter already exists...
|
||||||
if (await mangaConnectorId.Obj.CheckDownloaded(DbContext, CancellationToken))
|
if (await mangaConnectorId.Obj.CheckDownloaded(MangaContext, CancellationToken))
|
||||||
{
|
{
|
||||||
Log.Warn("Chapter already exists!");
|
Log.Warn("Chapter already exists!");
|
||||||
return [];
|
return [];
|
||||||
@@ -66,7 +80,7 @@ public class DownloadChapterFromMangaconnectorWorker(MangaConnectorId<Chapter> c
|
|||||||
{
|
{
|
||||||
Log.Info($"No imageUrls for chapter {chapter}");
|
Log.Info($"No imageUrls for chapter {chapter}");
|
||||||
mangaConnectorId.UseForDownload = false; // Do not try to download from this again
|
mangaConnectorId.UseForDownload = false; // Do not try to download from this again
|
||||||
if(await DbContext.Sync(CancellationToken, GetType(), "Disable Id") is { success: false } result)
|
if(await MangaContext.Sync(CancellationToken, GetType(), "Disable Id") is { success: false } result)
|
||||||
Log.Error(result.exceptionMessage);
|
Log.Error(result.exceptionMessage);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
@@ -121,7 +135,7 @@ public class DownloadChapterFromMangaconnectorWorker(MangaConnectorId<Chapter> c
|
|||||||
await CopyCoverFromCacheToDownloadLocation(chapter.ParentManga);
|
await CopyCoverFromCacheToDownloadLocation(chapter.ParentManga);
|
||||||
|
|
||||||
Log.Debug($"Loading collections {chapter}");
|
Log.Debug($"Loading collections {chapter}");
|
||||||
foreach (CollectionEntry collectionEntry in DbContext.Entry(chapter.ParentManga).Collections)
|
foreach (CollectionEntry collectionEntry in MangaContext.Entry(chapter.ParentManga).Collections)
|
||||||
await collectionEntry.LoadAsync(CancellationToken);
|
await collectionEntry.LoadAsync(CancellationToken);
|
||||||
|
|
||||||
if (File.Exists(saveArchiveFilePath))
|
if (File.Exists(saveArchiveFilePath))
|
||||||
@@ -173,11 +187,15 @@ public class DownloadChapterFromMangaconnectorWorker(MangaConnectorId<Chapter> c
|
|||||||
|
|
||||||
chapter.Downloaded = true;
|
chapter.Downloaded = true;
|
||||||
chapter.FileName = new FileInfo(saveArchiveFilePath).Name;
|
chapter.FileName = new FileInfo(saveArchiveFilePath).Name;
|
||||||
if(await DbContext.Sync(CancellationToken, GetType(), System.Reflection.MethodBase.GetCurrentMethod()?.Name) is { success: false } e)
|
if(await MangaContext.Sync(CancellationToken, GetType(), "Downloading complete") is { success: false } chapterContextException)
|
||||||
Log.Error($"Failed to save database changes: {e.exceptionMessage}");
|
Log.Error($"Failed to save database changes: {chapterContextException.exceptionMessage}");
|
||||||
|
|
||||||
Log.Debug($"Downloaded chapter {chapter}.");
|
Log.Debug($"Downloaded chapter {chapter}.");
|
||||||
|
|
||||||
|
ActionsContext.Actions.Add(new ChapterDownloadedActionRecord(chapter));
|
||||||
|
if(await ActionsContext.Sync(CancellationToken, GetType(), "Download complete") is { success: false } actionsContextException)
|
||||||
|
Log.Error($"Failed to save database changes: {actionsContextException.exceptionMessage}");
|
||||||
|
|
||||||
bool refreshLibrary = await CheckLibraryRefresh();
|
bool refreshLibrary = await CheckLibraryRefresh();
|
||||||
if(refreshLibrary)
|
if(refreshLibrary)
|
||||||
Log.Info($"Condition {Tranga.Settings.LibraryRefreshSetting} met.");
|
Log.Info($"Condition {Tranga.Settings.LibraryRefreshSetting} met.");
|
||||||
@@ -188,12 +206,12 @@ public class DownloadChapterFromMangaconnectorWorker(MangaConnectorId<Chapter> c
|
|||||||
private async Task<bool> CheckLibraryRefresh() => Tranga.Settings.LibraryRefreshSetting switch
|
private async Task<bool> CheckLibraryRefresh() => Tranga.Settings.LibraryRefreshSetting switch
|
||||||
{
|
{
|
||||||
LibraryRefreshSetting.AfterAllFinished => await AllDownloadsFinished(),
|
LibraryRefreshSetting.AfterAllFinished => await AllDownloadsFinished(),
|
||||||
LibraryRefreshSetting.AfterMangaFinished => await DbContext.MangaConnectorToChapter.Include(chId => chId.Obj).Where(chId => chId.UseForDownload).AllAsync(chId => chId.Obj.Downloaded, CancellationToken),
|
LibraryRefreshSetting.AfterMangaFinished => await MangaContext.MangaConnectorToChapter.Include(chId => chId.Obj).Where(chId => chId.UseForDownload).AllAsync(chId => chId.Obj.Downloaded, CancellationToken),
|
||||||
LibraryRefreshSetting.AfterEveryChapter => true,
|
LibraryRefreshSetting.AfterEveryChapter => true,
|
||||||
LibraryRefreshSetting.WhileDownloading => await AllDownloadsFinished() || DateTime.UtcNow.Subtract(RefreshLibrariesWorker.LastRefresh).TotalMinutes > Tranga.Settings.RefreshLibraryWhileDownloadingEveryMinutes,
|
LibraryRefreshSetting.WhileDownloading => await AllDownloadsFinished() || DateTime.UtcNow.Subtract(RefreshLibrariesWorker.LastRefresh).TotalMinutes > Tranga.Settings.RefreshLibraryWhileDownloadingEveryMinutes,
|
||||||
_ => true
|
_ => true
|
||||||
};
|
};
|
||||||
private async Task<bool> AllDownloadsFinished() => (await StartNewChapterDownloadsWorker.GetMissingChapters(DbContext, CancellationToken)).Count == 0;
|
private async Task<bool> AllDownloadsFinished() => (await StartNewChapterDownloadsWorker.GetMissingChapters(MangaContext, CancellationToken)).Count == 0;
|
||||||
|
|
||||||
private async Task<Stream> ProcessImage(Stream imageStream, CancellationToken? cancellationToken = null)
|
private async Task<Stream> ProcessImage(Stream imageStream, CancellationToken? cancellationToken = null)
|
||||||
{
|
{
|
||||||
@@ -244,7 +262,7 @@ public class DownloadChapterFromMangaconnectorWorker(MangaConnectorId<Chapter> c
|
|||||||
{
|
{
|
||||||
Log.Debug($"Copying cover for {manga}");
|
Log.Debug($"Copying cover for {manga}");
|
||||||
|
|
||||||
manga = await DbContext.MangaIncludeAll().FirstAsync(m => m.Key == manga.Key, CancellationToken);
|
manga = await MangaContext.MangaIncludeAll().FirstAsync(m => m.Key == manga.Key, CancellationToken);
|
||||||
string publicationFolder;
|
string publicationFolder;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -278,7 +296,7 @@ public class DownloadChapterFromMangaconnectorWorker(MangaConnectorId<Chapter> c
|
|||||||
|
|
||||||
coverFileNameInCache = mangaConnector.SaveCoverImageToCache(mangaConnectorId);
|
coverFileNameInCache = mangaConnector.SaveCoverImageToCache(mangaConnectorId);
|
||||||
manga.CoverFileNameInCache = coverFileNameInCache;
|
manga.CoverFileNameInCache = coverFileNameInCache;
|
||||||
if (await DbContext.Sync(CancellationToken, reason: "Update cover filename") is { success: false } result)
|
if (await MangaContext.Sync(CancellationToken, reason: "Update cover filename") is { success: false } result)
|
||||||
Log.Error($"Couldn't update cover filename {result.exceptionMessage}");
|
Log.Error($"Couldn't update cover filename {result.exceptionMessage}");
|
||||||
}
|
}
|
||||||
if (coverFileNameInCache is null)
|
if (coverFileNameInCache is null)
|
||||||
|
@@ -1,4 +1,7 @@
|
|||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using API.MangaConnectors;
|
using API.MangaConnectors;
|
||||||
|
using API.Schema.ActionsContext;
|
||||||
|
using API.Schema.ActionsContext.Actions;
|
||||||
using API.Schema.MangaContext;
|
using API.Schema.MangaContext;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
@@ -8,16 +11,28 @@ namespace API.Workers.MangaDownloadWorkers;
|
|||||||
/// Downloads the cover for Manga from Mangaconnector
|
/// Downloads the cover for Manga from Mangaconnector
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class DownloadCoverFromMangaconnectorWorker(MangaConnectorId<Manga> mcId, IEnumerable<BaseWorker>? dependsOn = null)
|
public class DownloadCoverFromMangaconnectorWorker(MangaConnectorId<Manga> mcId, IEnumerable<BaseWorker>? dependsOn = null)
|
||||||
: BaseWorkerWithContext<MangaContext>(dependsOn)
|
: BaseWorkerWithContexts(dependsOn)
|
||||||
{
|
{
|
||||||
internal readonly string MangaConnectorIdId = mcId.Key;
|
private readonly string _mangaConnectorIdId = mcId.Key;
|
||||||
|
|
||||||
|
[SuppressMessage("ReSharper", "InconsistentNaming")]
|
||||||
|
private MangaContext MangaContext = null!;
|
||||||
|
[SuppressMessage("ReSharper", "InconsistentNaming")]
|
||||||
|
private ActionsContext ActionsContext = null!;
|
||||||
|
|
||||||
|
protected override void SetContexts(IServiceScope serviceScope)
|
||||||
|
{
|
||||||
|
MangaContext = GetContext<MangaContext>(serviceScope);
|
||||||
|
ActionsContext = GetContext<ActionsContext>(serviceScope);
|
||||||
|
}
|
||||||
|
|
||||||
protected override async Task<BaseWorker[]> DoWorkInternal()
|
protected override async Task<BaseWorker[]> DoWorkInternal()
|
||||||
{
|
{
|
||||||
Log.Debug($"Getting Cover for MangaConnectorId {MangaConnectorIdId}...");
|
Log.Debug($"Getting Cover for MangaConnectorId {_mangaConnectorIdId}...");
|
||||||
// Getting MangaConnector info
|
// Getting MangaConnector info
|
||||||
if (await DbContext.MangaConnectorToManga
|
if (await MangaContext.MangaConnectorToManga
|
||||||
.Include(id => id.Obj)
|
.Include(id => id.Obj)
|
||||||
.FirstOrDefaultAsync(c => c.Key == MangaConnectorIdId, CancellationToken) is not { } mangaConnectorId)
|
.FirstOrDefaultAsync(c => c.Key == _mangaConnectorIdId, CancellationToken) is not { } mangaConnectorId)
|
||||||
{
|
{
|
||||||
Log.Error("Could not get MangaConnectorId.");
|
Log.Error("Could not get MangaConnectorId.");
|
||||||
return []; //TODO Exception?
|
return []; //TODO Exception?
|
||||||
@@ -35,13 +50,17 @@ public class DownloadCoverFromMangaconnectorWorker(MangaConnectorId<Manga> mcId,
|
|||||||
Log.Error($"Could not get Cover for MangaConnectorId {mangaConnectorId}.");
|
Log.Error($"Could not get Cover for MangaConnectorId {mangaConnectorId}.");
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
DbContext.Entry(mangaConnectorId.Obj).Property(m => m.CoverFileNameInCache).CurrentValue = coverFileName;
|
MangaContext.Entry(mangaConnectorId.Obj).Property(m => m.CoverFileNameInCache).CurrentValue = coverFileName;
|
||||||
|
|
||||||
if(await DbContext.Sync(CancellationToken, GetType(), System.Reflection.MethodBase.GetCurrentMethod()?.Name) is { success: false } e)
|
if(await MangaContext.Sync(CancellationToken, GetType(), System.Reflection.MethodBase.GetCurrentMethod()?.Name) is { success: false } mangaContextException)
|
||||||
Log.Error($"Failed to save database changes: {e.exceptionMessage}");
|
Log.Error($"Failed to save database changes: {mangaContextException.exceptionMessage}");
|
||||||
|
|
||||||
|
ActionsContext.Actions.Add(new CoverDownloadedActionRecord(mcId.Obj, coverFileName));
|
||||||
|
if(await MangaContext.Sync(CancellationToken, GetType(), "Download complete") is { success: false } actionsContextException)
|
||||||
|
Log.Error($"Failed to save database changes: {actionsContextException.exceptionMessage}");
|
||||||
|
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
public override string ToString() => $"{base.ToString()} {MangaConnectorIdId}";
|
public override string ToString() => $"{base.ToString()} {_mangaConnectorIdId}";
|
||||||
}
|
}
|
@@ -1,4 +1,7 @@
|
|||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using API.MangaConnectors;
|
using API.MangaConnectors;
|
||||||
|
using API.Schema.ActionsContext;
|
||||||
|
using API.Schema.ActionsContext.Actions;
|
||||||
using API.Schema.MangaContext;
|
using API.Schema.MangaContext;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
@@ -11,18 +14,30 @@ namespace API.Workers.MangaDownloadWorkers;
|
|||||||
/// <param name="language"></param>
|
/// <param name="language"></param>
|
||||||
/// <param name="dependsOn"></param>
|
/// <param name="dependsOn"></param>
|
||||||
public class RetrieveMangaChaptersFromMangaconnectorWorker(MangaConnectorId<Manga> mcId, string language, IEnumerable<BaseWorker>? dependsOn = null)
|
public class RetrieveMangaChaptersFromMangaconnectorWorker(MangaConnectorId<Manga> mcId, string language, IEnumerable<BaseWorker>? dependsOn = null)
|
||||||
: BaseWorkerWithContext<MangaContext>(dependsOn)
|
: BaseWorkerWithContexts(dependsOn)
|
||||||
{
|
{
|
||||||
internal readonly string MangaConnectorIdId = mcId.Key;
|
private readonly string _mangaConnectorIdId = mcId.Key;
|
||||||
|
|
||||||
|
[SuppressMessage("ReSharper", "InconsistentNaming")]
|
||||||
|
private MangaContext MangaContext = null!;
|
||||||
|
[SuppressMessage("ReSharper", "InconsistentNaming")]
|
||||||
|
private ActionsContext ActionsContext = null!;
|
||||||
|
|
||||||
|
protected override void SetContexts(IServiceScope serviceScope)
|
||||||
|
{
|
||||||
|
MangaContext = GetContext<MangaContext>(serviceScope);
|
||||||
|
ActionsContext = GetContext<ActionsContext>(serviceScope);
|
||||||
|
}
|
||||||
|
|
||||||
protected override async Task<BaseWorker[]> DoWorkInternal()
|
protected override async Task<BaseWorker[]> DoWorkInternal()
|
||||||
{
|
{
|
||||||
Log.Debug($"Getting Chapters for MangaConnectorId {MangaConnectorIdId}...");
|
Log.Debug($"Getting Chapters for MangaConnectorId {_mangaConnectorIdId}...");
|
||||||
// Getting MangaConnector info
|
// Getting MangaConnector info
|
||||||
if (await DbContext.MangaConnectorToManga
|
if (await MangaContext.MangaConnectorToManga
|
||||||
.Include(id => id.Obj)
|
.Include(id => id.Obj)
|
||||||
.ThenInclude(m => m.Chapters)
|
.ThenInclude(m => m.Chapters)
|
||||||
.ThenInclude(ch => ch.MangaConnectorIds)
|
.ThenInclude(ch => ch.MangaConnectorIds)
|
||||||
.FirstOrDefaultAsync(c => c.Key == MangaConnectorIdId, CancellationToken) is not { } mangaConnectorId)
|
.FirstOrDefaultAsync(c => c.Key == _mangaConnectorIdId, CancellationToken) is not { } mangaConnectorId)
|
||||||
{
|
{
|
||||||
Log.Error("Could not get MangaConnectorId.");
|
Log.Error("Could not get MangaConnectorId.");
|
||||||
return []; //TODO Exception?
|
return []; //TODO Exception?
|
||||||
@@ -62,7 +77,7 @@ public class RetrieveMangaChaptersFromMangaconnectorWorker(MangaConnectorId<Mang
|
|||||||
Log.Debug($"Got {newIds.Count} new download-Ids.");
|
Log.Debug($"Got {newIds.Count} new download-Ids.");
|
||||||
|
|
||||||
// Add new ChapterIds to Database
|
// Add new ChapterIds to Database
|
||||||
DbContext.MangaConnectorToChapter.AddRange(newIds);
|
MangaContext.MangaConnectorToChapter.AddRange(newIds);
|
||||||
|
|
||||||
// If Manga is marked for Download from Connector, mark the new Chapters as UseForDownload
|
// If Manga is marked for Download from Connector, mark the new Chapters as UseForDownload
|
||||||
if (mangaConnectorId.UseForDownload)
|
if (mangaConnectorId.UseForDownload)
|
||||||
@@ -73,11 +88,15 @@ public class RetrieveMangaChaptersFromMangaconnectorWorker(MangaConnectorId<Mang
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(await DbContext.Sync(CancellationToken, GetType(), System.Reflection.MethodBase.GetCurrentMethod()?.Name) is { success: false } e)
|
if(await MangaContext.Sync(CancellationToken, GetType(), "Chapters retrieved") is { success: false } mangaContextException)
|
||||||
Log.Error($"Failed to save database changes: {e.exceptionMessage}");
|
Log.Error($"Failed to save database changes: {mangaContextException.exceptionMessage}");
|
||||||
|
|
||||||
|
ActionsContext.Actions.Add(new ChaptersRetrievedActionRecord(manga));
|
||||||
|
if(await MangaContext.Sync(CancellationToken, GetType(), "Chapters retrieved") is { success: false } actionsContextException)
|
||||||
|
Log.Error($"Failed to save database changes: {actionsContextException.exceptionMessage}");
|
||||||
|
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
public override string ToString() => $"{base.ToString()} {MangaConnectorIdId}";
|
public override string ToString() => $"{base.ToString()} {_mangaConnectorIdId}";
|
||||||
}
|
}
|
@@ -1,12 +1,24 @@
|
|||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using API.Schema.ActionsContext;
|
||||||
|
using API.Schema.ActionsContext.Actions;
|
||||||
|
|
||||||
namespace API.Workers;
|
namespace API.Workers;
|
||||||
|
|
||||||
public class MoveFileOrFolderWorker(string toLocation, string fromLocation, IEnumerable<BaseWorker>? dependsOn = null)
|
public class MoveFileOrFolderWorker(string toLocation, string fromLocation, IEnumerable<BaseWorker>? dependsOn = null)
|
||||||
: BaseWorker(dependsOn)
|
: BaseWorkerWithContexts(dependsOn)
|
||||||
{
|
{
|
||||||
public readonly string FromLocation = fromLocation;
|
public readonly string FromLocation = fromLocation;
|
||||||
public readonly string ToLocation = toLocation;
|
public readonly string ToLocation = toLocation;
|
||||||
|
|
||||||
|
[SuppressMessage("ReSharper", "InconsistentNaming")]
|
||||||
|
private ActionsContext ActionsContext = null!;
|
||||||
|
|
||||||
protected override Task<BaseWorker[]> DoWorkInternal()
|
protected override void SetContexts(IServiceScope serviceScope)
|
||||||
|
{
|
||||||
|
ActionsContext = GetContext<ActionsContext>(serviceScope);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async Task<BaseWorker[]> DoWorkInternal()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -14,13 +26,13 @@ public class MoveFileOrFolderWorker(string toLocation, string fromLocation, IEnu
|
|||||||
if (!fi.Exists)
|
if (!fi.Exists)
|
||||||
{
|
{
|
||||||
Log.Error($"File does not exist at {FromLocation}");
|
Log.Error($"File does not exist at {FromLocation}");
|
||||||
return new Task<BaseWorker[]>(() => []);
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (File.Exists(ToLocation))//Do not override existing
|
if (File.Exists(ToLocation))//Do not override existing
|
||||||
{
|
{
|
||||||
Log.Error($"File already exists at {ToLocation}");
|
Log.Error($"File already exists at {ToLocation}");
|
||||||
return new Task<BaseWorker[]>(() => []);
|
return [];
|
||||||
}
|
}
|
||||||
if(fi.Attributes.HasFlag(FileAttributes.Directory))
|
if(fi.Attributes.HasFlag(FileAttributes.Directory))
|
||||||
MoveDirectory(fi, ToLocation);
|
MoveDirectory(fi, ToLocation);
|
||||||
@@ -32,7 +44,11 @@ public class MoveFileOrFolderWorker(string toLocation, string fromLocation, IEnu
|
|||||||
Log.Error(e);
|
Log.Error(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Task<BaseWorker[]>(() => []);
|
ActionsContext.Actions.Add(new DataMovedActionRecord(FromLocation, ToLocation));
|
||||||
|
if(await ActionsContext.Sync(CancellationToken, GetType(), "Library Moved") is { success: false } actionsContextException)
|
||||||
|
Log.Error($"Failed to save database changes: {actionsContextException.exceptionMessage}");
|
||||||
|
|
||||||
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
private void MoveDirectory(FileInfo from, string toLocation)
|
private void MoveDirectory(FileInfo from, string toLocation)
|
||||||
|
@@ -1,47 +0,0 @@
|
|||||||
using API.Schema.MangaContext;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
|
|
||||||
namespace API.Workers;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Moves a Manga to a different Library
|
|
||||||
/// </summary>
|
|
||||||
public class MoveMangaLibraryWorker(Manga manga, FileLibrary toLibrary, IEnumerable<BaseWorker>? dependsOn = null)
|
|
||||||
: BaseWorkerWithContext<MangaContext>(dependsOn)
|
|
||||||
{
|
|
||||||
internal readonly string MangaId = manga.Key;
|
|
||||||
internal readonly string LibraryId = toLibrary.Key;
|
|
||||||
protected override async Task<BaseWorker[]> DoWorkInternal()
|
|
||||||
{
|
|
||||||
Log.Debug("Moving Manga...");
|
|
||||||
// Get Manga (with and Library)
|
|
||||||
if (await DbContext.Mangas
|
|
||||||
.Include(m => m.Library)
|
|
||||||
.Include(m => m.Chapters)
|
|
||||||
.FirstOrDefaultAsync(m => m.Key == MangaId, CancellationToken) is not { } manga)
|
|
||||||
{
|
|
||||||
Log.Error("Could not find Manga.");
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get new Library
|
|
||||||
if (await DbContext.FileLibraries.FirstOrDefaultAsync(l => l.Key == LibraryId, CancellationToken) is not { } toLibrary)
|
|
||||||
{
|
|
||||||
Log.Error("Could not find Library.");
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save old Path (to later move chapters)
|
|
||||||
Dictionary<string, string> oldPath = manga.Chapters.Where(c => c.FileName != null).ToDictionary(c => c.Key, c => c.FullArchiveFilePath)!;
|
|
||||||
// Set new Path
|
|
||||||
manga.Library = toLibrary;
|
|
||||||
|
|
||||||
if (await DbContext.Sync(CancellationToken, GetType(), System.Reflection.MethodBase.GetCurrentMethod()?.Name) is { success: false })
|
|
||||||
return [];
|
|
||||||
|
|
||||||
// Create Jobs to move chapters from old to new Path
|
|
||||||
return oldPath.Select(kv => new MoveFileOrFolderWorker(manga.Chapters.First(ch => ch.Key == kv.Key).FullArchiveFilePath!, kv.Value)).ToArray<BaseWorker>();
|
|
||||||
}
|
|
||||||
|
|
||||||
public override string ToString() => $"{base.ToString()} {MangaId} {LibraryId}";
|
|
||||||
}
|
|
@@ -1,3 +1,4 @@
|
|||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using API.Schema.MangaContext;
|
using API.Schema.MangaContext;
|
||||||
using API.Workers.MangaDownloadWorkers;
|
using API.Workers.MangaDownloadWorkers;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
@@ -8,15 +9,23 @@ namespace API.Workers.PeriodicWorkers;
|
|||||||
/// Creates Jobs to update available Chapters for all Manga that are marked for Download
|
/// Creates Jobs to update available Chapters for all Manga that are marked for Download
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class CheckForNewChaptersWorker(TimeSpan? interval = null, IEnumerable<BaseWorker>? dependsOn = null)
|
public class CheckForNewChaptersWorker(TimeSpan? interval = null, IEnumerable<BaseWorker>? dependsOn = null)
|
||||||
: BaseWorkerWithContext<MangaContext>(dependsOn), IPeriodic
|
: BaseWorkerWithContexts(dependsOn), IPeriodic
|
||||||
{
|
{
|
||||||
public DateTime LastExecution { get; set; } = DateTime.UnixEpoch;
|
public DateTime LastExecution { get; set; } = DateTime.UnixEpoch;
|
||||||
public TimeSpan Interval { get; set; } = interval??TimeSpan.FromMinutes(60);
|
public TimeSpan Interval { get; set; } = interval??TimeSpan.FromMinutes(60);
|
||||||
|
|
||||||
|
[SuppressMessage("ReSharper", "InconsistentNaming")]
|
||||||
|
private MangaContext MangaContext = null!;
|
||||||
|
|
||||||
|
protected override void SetContexts(IServiceScope serviceScope)
|
||||||
|
{
|
||||||
|
MangaContext = GetContext<MangaContext>(serviceScope);
|
||||||
|
}
|
||||||
|
|
||||||
protected override async Task<BaseWorker[]> DoWorkInternal()
|
protected override async Task<BaseWorker[]> DoWorkInternal()
|
||||||
{
|
{
|
||||||
Log.Debug("Checking for new chapters...");
|
Log.Debug("Checking for new chapters...");
|
||||||
List<MangaConnectorId<Manga>> connectorIdsManga = await DbContext.MangaConnectorToManga
|
List<MangaConnectorId<Manga>> connectorIdsManga = await MangaContext.MangaConnectorToManga
|
||||||
.Include(id => id.Obj)
|
.Include(id => id.Obj)
|
||||||
.Where(id => id.UseForDownload)
|
.Where(id => id.UseForDownload)
|
||||||
.ToListAsync(CancellationToken);
|
.ToListAsync(CancellationToken);
|
||||||
@@ -27,5 +36,4 @@ public class CheckForNewChaptersWorker(TimeSpan? interval = null, IEnumerable<Ba
|
|||||||
|
|
||||||
return newWorkers.ToArray();
|
return newWorkers.ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@@ -1,18 +1,27 @@
|
|||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using API.Schema.MangaContext;
|
using API.Schema.MangaContext;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
namespace API.Workers.PeriodicWorkers.MaintenanceWorkers;
|
namespace API.Workers.PeriodicWorkers.MaintenanceWorkers;
|
||||||
|
|
||||||
public class CleanupMangaCoversWorker(TimeSpan? interval = null, IEnumerable<BaseWorker>? dependsOn = null)
|
public class CleanupMangaCoversWorker(TimeSpan? interval = null, IEnumerable<BaseWorker>? dependsOn = null)
|
||||||
: BaseWorkerWithContext<MangaContext>(dependsOn), IPeriodic
|
: BaseWorkerWithContexts(dependsOn), IPeriodic
|
||||||
{
|
{
|
||||||
public DateTime LastExecution { get; set; } = DateTime.UnixEpoch;
|
public DateTime LastExecution { get; set; } = DateTime.UnixEpoch;
|
||||||
public TimeSpan Interval { get; set; } = interval ?? TimeSpan.FromHours(24);
|
public TimeSpan Interval { get; set; } = interval ?? TimeSpan.FromHours(24);
|
||||||
|
|
||||||
|
[SuppressMessage("ReSharper", "InconsistentNaming")]
|
||||||
|
private MangaContext MangaContext = null!;
|
||||||
|
|
||||||
|
protected override void SetContexts(IServiceScope serviceScope)
|
||||||
|
{
|
||||||
|
MangaContext = GetContext<MangaContext>(serviceScope);
|
||||||
|
}
|
||||||
|
|
||||||
protected override async Task<BaseWorker[]> DoWorkInternal()
|
protected override async Task<BaseWorker[]> DoWorkInternal()
|
||||||
{
|
{
|
||||||
Log.Info("Removing stale files...");
|
Log.Info("Removing stale files...");
|
||||||
string[] usedFiles = await DbContext.Mangas.Where(m => m.CoverFileNameInCache != null).Select(m => m.CoverFileNameInCache!).ToArrayAsync(CancellationToken);
|
string[] usedFiles = await MangaContext.Mangas.Where(m => m.CoverFileNameInCache != null).Select(m => m.CoverFileNameInCache!).ToArrayAsync(CancellationToken);
|
||||||
CleanupImageCache(usedFiles, TrangaSettings.CoverImageCacheOriginal);
|
CleanupImageCache(usedFiles, TrangaSettings.CoverImageCacheOriginal);
|
||||||
CleanupImageCache(usedFiles, TrangaSettings.CoverImageCacheLarge);
|
CleanupImageCache(usedFiles, TrangaSettings.CoverImageCacheLarge);
|
||||||
CleanupImageCache(usedFiles, TrangaSettings.CoverImageCacheMedium);
|
CleanupImageCache(usedFiles, TrangaSettings.CoverImageCacheMedium);
|
||||||
|
@@ -1,29 +1,37 @@
|
|||||||
using System.Text;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using API.Schema.MangaContext;
|
using API.Schema.MangaContext;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
namespace API.Workers.PeriodicWorkers.MaintenanceWorkers;
|
namespace API.Workers.PeriodicWorkers.MaintenanceWorkers;
|
||||||
|
|
||||||
public class CleanupMangaconnectorIdsWithoutConnector : BaseWorkerWithContext<MangaContext>
|
public class CleanupMangaconnectorIdsWithoutConnector : BaseWorkerWithContexts
|
||||||
{
|
{
|
||||||
|
[SuppressMessage("ReSharper", "InconsistentNaming")]
|
||||||
|
private MangaContext MangaContext = null!;
|
||||||
|
|
||||||
|
protected override void SetContexts(IServiceScope serviceScope)
|
||||||
|
{
|
||||||
|
MangaContext = GetContext<MangaContext>(serviceScope);
|
||||||
|
}
|
||||||
|
|
||||||
protected override async Task<BaseWorker[]> DoWorkInternal()
|
protected override async Task<BaseWorker[]> DoWorkInternal()
|
||||||
{
|
{
|
||||||
Log.Info("Cleaning up old connector-data");
|
Log.Info("Cleaning up old connector-data");
|
||||||
string[] connectorNames = Tranga.MangaConnectors.Select(c => c.Name).ToArray();
|
string[] connectorNames = Tranga.MangaConnectors.Select(c => c.Name).ToArray();
|
||||||
int deletedChapterIds = await DbContext.MangaConnectorToChapter.Where(chId => connectorNames.All(n => n != chId.MangaConnectorName)).ExecuteDeleteAsync(CancellationToken);
|
int deletedChapterIds = await MangaContext.MangaConnectorToChapter.Where(chId => connectorNames.All(n => n != chId.MangaConnectorName)).ExecuteDeleteAsync(CancellationToken);
|
||||||
Log.Info($"Deleted {deletedChapterIds} chapterIds.");
|
Log.Info($"Deleted {deletedChapterIds} chapterIds.");
|
||||||
|
|
||||||
// Manga without Connector get printed to file, to not lose data...
|
// Manga without Connector get printed to file, to not lose data...
|
||||||
if (await DbContext.MangaConnectorToManga.Include(id => id.Obj) .Where(mcId => connectorNames.All(name => name != mcId.MangaConnectorName)).ToListAsync() is { Count: > 0 } list)
|
if (await MangaContext.MangaConnectorToManga.Include(id => id.Obj) .Where(mcId => connectorNames.All(name => name != mcId.MangaConnectorName)).ToListAsync() is { Count: > 0 } list)
|
||||||
{
|
{
|
||||||
string filePath = Path.Join(TrangaSettings.WorkingDirectory, $"deletedManga-{DateTime.UtcNow.Ticks}.txt");
|
string filePath = Path.Join(TrangaSettings.WorkingDirectory, $"deletedManga-{DateTime.UtcNow.Ticks}.txt");
|
||||||
Log.Debug($"Writing deleted manga to {filePath}.");
|
Log.Debug($"Writing deleted manga to {filePath}.");
|
||||||
await File.WriteAllLinesAsync(filePath, list.Select(id => string.Join('-', id.MangaConnectorName, id.IdOnConnectorSite, id.Obj.Name, id.WebsiteUrl)), CancellationToken);
|
await File.WriteAllLinesAsync(filePath, list.Select(id => string.Join('-', id.MangaConnectorName, id.IdOnConnectorSite, id.Obj.Name, id.WebsiteUrl)), CancellationToken);
|
||||||
}
|
}
|
||||||
int deletedMangaIds = await DbContext.MangaConnectorToManga.Where(mcId => connectorNames.All(name => name != mcId.MangaConnectorName)).ExecuteDeleteAsync(CancellationToken);
|
int deletedMangaIds = await MangaContext.MangaConnectorToManga.Where(mcId => connectorNames.All(name => name != mcId.MangaConnectorName)).ExecuteDeleteAsync(CancellationToken);
|
||||||
Log.Info($"Deleted {deletedMangaIds} mangaIds.");
|
Log.Info($"Deleted {deletedMangaIds} mangaIds.");
|
||||||
|
|
||||||
await DbContext.SaveChangesAsync(CancellationToken);
|
await MangaContext.SaveChangesAsync(CancellationToken);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -1,3 +1,4 @@
|
|||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using API.Schema.NotificationsContext;
|
using API.Schema.NotificationsContext;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
@@ -7,18 +8,26 @@ namespace API.Workers.PeriodicWorkers.MaintenanceWorkers;
|
|||||||
/// Removes sent notifications from database
|
/// Removes sent notifications from database
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class RemoveOldNotificationsWorker(TimeSpan? interval = null, IEnumerable<BaseWorker>? dependsOn = null)
|
public class RemoveOldNotificationsWorker(TimeSpan? interval = null, IEnumerable<BaseWorker>? dependsOn = null)
|
||||||
: BaseWorkerWithContext<NotificationsContext>(dependsOn), IPeriodic
|
: BaseWorkerWithContexts(dependsOn), IPeriodic
|
||||||
{
|
{
|
||||||
public DateTime LastExecution { get; set; } = DateTime.UnixEpoch;
|
public DateTime LastExecution { get; set; } = DateTime.UnixEpoch;
|
||||||
public TimeSpan Interval { get; set; } = interval ?? TimeSpan.FromHours(1);
|
public TimeSpan Interval { get; set; } = interval ?? TimeSpan.FromHours(1);
|
||||||
|
|
||||||
|
[SuppressMessage("ReSharper", "InconsistentNaming")]
|
||||||
|
private NotificationsContext NotificationsContext = null!;
|
||||||
|
|
||||||
|
protected override void SetContexts(IServiceScope serviceScope)
|
||||||
|
{
|
||||||
|
NotificationsContext = GetContext<NotificationsContext>(serviceScope);
|
||||||
|
}
|
||||||
|
|
||||||
protected override async Task<BaseWorker[]> DoWorkInternal()
|
protected override async Task<BaseWorker[]> DoWorkInternal()
|
||||||
{
|
{
|
||||||
Log.Debug("Removing old notifications...");
|
Log.Debug("Removing old notifications...");
|
||||||
int removed = await DbContext.Notifications.Where(n => n.IsSent).ExecuteDeleteAsync(CancellationToken);
|
int removed = await NotificationsContext.Notifications.Where(n => n.IsSent).ExecuteDeleteAsync(CancellationToken);
|
||||||
Log.Debug($"Removed {removed} old notifications...");
|
Log.Debug($"Removed {removed} old notifications...");
|
||||||
|
|
||||||
if(await DbContext.Sync(CancellationToken, GetType(), System.Reflection.MethodBase.GetCurrentMethod()?.Name) is { success: false } e)
|
if(await NotificationsContext.Sync(CancellationToken, GetType(), System.Reflection.MethodBase.GetCurrentMethod()?.Name) is { success: false } e)
|
||||||
Log.Error($"Failed to save database changes: {e.exceptionMessage}");
|
Log.Error($"Failed to save database changes: {e.exceptionMessage}");
|
||||||
|
|
||||||
return [];
|
return [];
|
||||||
|
@@ -1,3 +1,4 @@
|
|||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using API.Schema.NotificationsContext;
|
using API.Schema.NotificationsContext;
|
||||||
using API.Schema.NotificationsContext.NotificationConnectors;
|
using API.Schema.NotificationsContext.NotificationConnectors;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
@@ -10,15 +11,24 @@ namespace API.Workers.PeriodicWorkers;
|
|||||||
/// <param name="interval"></param>
|
/// <param name="interval"></param>
|
||||||
/// <param name="dependsOn"></param>
|
/// <param name="dependsOn"></param>
|
||||||
public class SendNotificationsWorker(TimeSpan? interval = null, IEnumerable<BaseWorker>? dependsOn = null)
|
public class SendNotificationsWorker(TimeSpan? interval = null, IEnumerable<BaseWorker>? dependsOn = null)
|
||||||
: BaseWorkerWithContext<NotificationsContext>(dependsOn), IPeriodic
|
: BaseWorkerWithContexts(dependsOn), IPeriodic
|
||||||
{
|
{
|
||||||
public DateTime LastExecution { get; set; } = DateTime.UnixEpoch;
|
public DateTime LastExecution { get; set; } = DateTime.UnixEpoch;
|
||||||
public TimeSpan Interval { get; set; } = interval??TimeSpan.FromMinutes(1);
|
public TimeSpan Interval { get; set; } = interval??TimeSpan.FromMinutes(1);
|
||||||
|
|
||||||
|
[SuppressMessage("ReSharper", "InconsistentNaming")]
|
||||||
|
private NotificationsContext NotificationsContext = null!;
|
||||||
|
|
||||||
|
protected override void SetContexts(IServiceScope serviceScope)
|
||||||
|
{
|
||||||
|
NotificationsContext = GetContext<NotificationsContext>(serviceScope);
|
||||||
|
}
|
||||||
|
|
||||||
protected override async Task<BaseWorker[]> DoWorkInternal()
|
protected override async Task<BaseWorker[]> DoWorkInternal()
|
||||||
{
|
{
|
||||||
Log.Debug("Sending notifications...");
|
Log.Debug("Sending notifications...");
|
||||||
List<NotificationConnector> connectors = await DbContext.NotificationConnectors.ToListAsync(CancellationToken);
|
List<NotificationConnector> connectors = await NotificationsContext.NotificationConnectors.ToListAsync(CancellationToken);
|
||||||
List<Notification> unsentNotifications = await DbContext.Notifications.Where(n => n.IsSent == false).ToListAsync(CancellationToken);
|
List<Notification> unsentNotifications = await NotificationsContext.Notifications.Where(n => n.IsSent == false).ToListAsync(CancellationToken);
|
||||||
|
|
||||||
Log.Debug($"Sending {unsentNotifications.Count} notifications to {connectors.Count} connectors...");
|
Log.Debug($"Sending {unsentNotifications.Count} notifications to {connectors.Count} connectors...");
|
||||||
|
|
||||||
@@ -27,16 +37,15 @@ public class SendNotificationsWorker(TimeSpan? interval = null, IEnumerable<Base
|
|||||||
connectors.ForEach(connector =>
|
connectors.ForEach(connector =>
|
||||||
{
|
{
|
||||||
connector.SendNotification(notification.Title, notification.Message);
|
connector.SendNotification(notification.Title, notification.Message);
|
||||||
DbContext.Entry(notification).Property(n => n.IsSent).CurrentValue = true;
|
NotificationsContext.Entry(notification).Property(n => n.IsSent).CurrentValue = true;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
Log.Debug("Notifications sent.");
|
Log.Debug("Notifications sent.");
|
||||||
|
|
||||||
if(await DbContext.Sync(CancellationToken, GetType(), System.Reflection.MethodBase.GetCurrentMethod()?.Name) is { success: false } e)
|
if(await NotificationsContext.Sync(CancellationToken, GetType(), System.Reflection.MethodBase.GetCurrentMethod()?.Name) is { success: false } e)
|
||||||
Log.Error($"Failed to save database changes: {e.exceptionMessage}");
|
Log.Error($"Failed to save database changes: {e.exceptionMessage}");
|
||||||
|
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@@ -1,3 +1,4 @@
|
|||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using API.Schema.MangaContext;
|
using API.Schema.MangaContext;
|
||||||
using API.Workers.MangaDownloadWorkers;
|
using API.Workers.MangaDownloadWorkers;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
@@ -8,17 +9,26 @@ namespace API.Workers.PeriodicWorkers;
|
|||||||
/// Create new Workers for Chapters on Manga marked for Download, that havent been downloaded yet.
|
/// Create new Workers for Chapters on Manga marked for Download, that havent been downloaded yet.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class StartNewChapterDownloadsWorker(TimeSpan? interval = null, IEnumerable<BaseWorker>? dependsOn = null)
|
public class StartNewChapterDownloadsWorker(TimeSpan? interval = null, IEnumerable<BaseWorker>? dependsOn = null)
|
||||||
: BaseWorkerWithContext<MangaContext>(dependsOn), IPeriodic
|
: BaseWorkerWithContexts(dependsOn), IPeriodic
|
||||||
{
|
{
|
||||||
|
|
||||||
public DateTime LastExecution { get; set; } = DateTime.UnixEpoch;
|
public DateTime LastExecution { get; set; } = DateTime.UnixEpoch;
|
||||||
public TimeSpan Interval { get; set; } = interval ?? TimeSpan.FromMinutes(1);
|
public TimeSpan Interval { get; set; } = interval ?? TimeSpan.FromMinutes(1);
|
||||||
|
|
||||||
|
[SuppressMessage("ReSharper", "InconsistentNaming")]
|
||||||
|
private MangaContext MangaContext = null!;
|
||||||
|
|
||||||
|
protected override void SetContexts(IServiceScope serviceScope)
|
||||||
|
{
|
||||||
|
MangaContext = GetContext<MangaContext>(serviceScope);
|
||||||
|
}
|
||||||
|
|
||||||
protected override async Task<BaseWorker[]> DoWorkInternal()
|
protected override async Task<BaseWorker[]> DoWorkInternal()
|
||||||
{
|
{
|
||||||
Log.Debug("Checking for missing chapters...");
|
Log.Debug("Checking for missing chapters...");
|
||||||
|
|
||||||
// Get missing chapters
|
// Get missing chapters
|
||||||
List<MangaConnectorId<Chapter>> missingChapters = await GetMissingChapters(DbContext, CancellationToken);
|
List<MangaConnectorId<Chapter>> missingChapters = await GetMissingChapters(MangaContext, CancellationToken);
|
||||||
|
|
||||||
Log.Debug($"Found {missingChapters.Count} missing downloads.");
|
Log.Debug($"Found {missingChapters.Count} missing downloads.");
|
||||||
|
|
||||||
|
@@ -1,3 +1,4 @@
|
|||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using API.Schema.MangaContext;
|
using API.Schema.MangaContext;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
@@ -7,20 +8,29 @@ namespace API.Workers.PeriodicWorkers;
|
|||||||
/// Updates the database to reflect changes made on disk
|
/// Updates the database to reflect changes made on disk
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class UpdateChaptersDownloadedWorker(TimeSpan? interval = null, IEnumerable<BaseWorker>? dependsOn = null)
|
public class UpdateChaptersDownloadedWorker(TimeSpan? interval = null, IEnumerable<BaseWorker>? dependsOn = null)
|
||||||
: BaseWorkerWithContext<MangaContext>(dependsOn), IPeriodic
|
: BaseWorkerWithContexts(dependsOn), IPeriodic
|
||||||
{
|
{
|
||||||
public DateTime LastExecution { get; set; } = DateTime.UnixEpoch;
|
public DateTime LastExecution { get; set; } = DateTime.UnixEpoch;
|
||||||
public TimeSpan Interval { get; set; } = interval??TimeSpan.FromDays(1);
|
public TimeSpan Interval { get; set; } = interval??TimeSpan.FromDays(1);
|
||||||
|
|
||||||
|
[SuppressMessage("ReSharper", "InconsistentNaming")]
|
||||||
|
private MangaContext MangaContext = null!;
|
||||||
|
|
||||||
|
protected override void SetContexts(IServiceScope serviceScope)
|
||||||
|
{
|
||||||
|
MangaContext = GetContext<MangaContext>(serviceScope);
|
||||||
|
}
|
||||||
|
|
||||||
protected override async Task<BaseWorker[]> DoWorkInternal()
|
protected override async Task<BaseWorker[]> DoWorkInternal()
|
||||||
{
|
{
|
||||||
Log.Debug("Checking chapter files...");
|
Log.Debug("Checking chapter files...");
|
||||||
List<Chapter> chapters = await DbContext.Chapters.ToListAsync(CancellationToken);
|
List<Chapter> chapters = await MangaContext.Chapters.ToListAsync(CancellationToken);
|
||||||
Log.Debug($"Checking {chapters.Count} chapters...");
|
Log.Debug($"Checking {chapters.Count} chapters...");
|
||||||
foreach (Chapter chapter in chapters)
|
foreach (Chapter chapter in chapters)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
chapter.Downloaded = await chapter.CheckDownloaded(DbContext, CancellationToken);
|
chapter.Downloaded = await chapter.CheckDownloaded(MangaContext, CancellationToken);
|
||||||
}
|
}
|
||||||
catch (Exception exception)
|
catch (Exception exception)
|
||||||
{
|
{
|
||||||
@@ -28,7 +38,7 @@ public class UpdateChaptersDownloadedWorker(TimeSpan? interval = null, IEnumerab
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(await DbContext.Sync(CancellationToken, GetType(), System.Reflection.MethodBase.GetCurrentMethod()?.Name) is { success: false } e)
|
if(await MangaContext.Sync(CancellationToken, GetType(), System.Reflection.MethodBase.GetCurrentMethod()?.Name) is { success: false } e)
|
||||||
Log.Error($"Failed to save database changes: {e.exceptionMessage}");
|
Log.Error($"Failed to save database changes: {e.exceptionMessage}");
|
||||||
|
|
||||||
return [];
|
return [];
|
||||||
|
@@ -1,3 +1,4 @@
|
|||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using API.Schema.MangaContext;
|
using API.Schema.MangaContext;
|
||||||
using API.Workers.MangaDownloadWorkers;
|
using API.Workers.MangaDownloadWorkers;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
@@ -10,15 +11,22 @@ namespace API.Workers.PeriodicWorkers;
|
|||||||
/// <param name="interval"></param>
|
/// <param name="interval"></param>
|
||||||
/// <param name="dependsOn"></param>
|
/// <param name="dependsOn"></param>
|
||||||
public class UpdateCoversWorker(TimeSpan? interval = null, IEnumerable<BaseWorker>? dependsOn = null)
|
public class UpdateCoversWorker(TimeSpan? interval = null, IEnumerable<BaseWorker>? dependsOn = null)
|
||||||
: BaseWorkerWithContext<MangaContext>(dependsOn), IPeriodic
|
: BaseWorkerWithContexts(dependsOn), IPeriodic
|
||||||
{
|
{
|
||||||
|
|
||||||
public DateTime LastExecution { get; set; } = DateTime.UnixEpoch;
|
public DateTime LastExecution { get; set; } = DateTime.UnixEpoch;
|
||||||
public TimeSpan Interval { get; set; } = interval ?? TimeSpan.FromHours(6);
|
public TimeSpan Interval { get; set; } = interval ?? TimeSpan.FromHours(6);
|
||||||
|
|
||||||
|
[SuppressMessage("ReSharper", "InconsistentNaming")]
|
||||||
|
private MangaContext MangaContext = null!;
|
||||||
|
|
||||||
|
protected override void SetContexts(IServiceScope serviceScope)
|
||||||
|
{
|
||||||
|
MangaContext = GetContext<MangaContext>(serviceScope);
|
||||||
|
}
|
||||||
|
|
||||||
protected override async Task<BaseWorker[]> DoWorkInternal()
|
protected override async Task<BaseWorker[]> DoWorkInternal()
|
||||||
{
|
{
|
||||||
List<MangaConnectorId<Manga>> manga = await DbContext.MangaConnectorToManga.Where(mcId => mcId.UseForDownload).ToListAsync(CancellationToken);
|
List<MangaConnectorId<Manga>> manga = await MangaContext.MangaConnectorToManga.Where(mcId => mcId.UseForDownload).ToListAsync(CancellationToken);
|
||||||
List<BaseWorker> newWorkers = manga.Select(m => new DownloadCoverFromMangaconnectorWorker(m)).ToList<BaseWorker>();
|
List<BaseWorker> newWorkers = manga.Select(m => new DownloadCoverFromMangaconnectorWorker(m)).ToList<BaseWorker>();
|
||||||
return newWorkers.ToArray();
|
return newWorkers.ToArray();
|
||||||
}
|
}
|
||||||
|
@@ -1,3 +1,6 @@
|
|||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using API.Schema.ActionsContext;
|
||||||
|
using API.Schema.ActionsContext.Actions;
|
||||||
using API.Schema.MangaContext;
|
using API.Schema.MangaContext;
|
||||||
using API.Schema.MangaContext.MetadataFetchers;
|
using API.Schema.MangaContext.MetadataFetchers;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
@@ -10,20 +13,31 @@ namespace API.Workers.PeriodicWorkers;
|
|||||||
/// <param name="interval"></param>
|
/// <param name="interval"></param>
|
||||||
/// <param name="dependsOn"></param>
|
/// <param name="dependsOn"></param>
|
||||||
public class UpdateMetadataWorker(TimeSpan? interval = null, IEnumerable<BaseWorker>? dependsOn = null)
|
public class UpdateMetadataWorker(TimeSpan? interval = null, IEnumerable<BaseWorker>? dependsOn = null)
|
||||||
: BaseWorkerWithContext<MangaContext>(dependsOn), IPeriodic
|
: BaseWorkerWithContexts(dependsOn), IPeriodic
|
||||||
{
|
{
|
||||||
|
|
||||||
public DateTime LastExecution { get; set; } = DateTime.UnixEpoch;
|
public DateTime LastExecution { get; set; } = DateTime.UnixEpoch;
|
||||||
public TimeSpan Interval { get; set; } = interval ?? TimeSpan.FromHours(12);
|
public TimeSpan Interval { get; set; } = interval ?? TimeSpan.FromHours(12);
|
||||||
|
|
||||||
|
[SuppressMessage("ReSharper", "InconsistentNaming")]
|
||||||
|
private MangaContext MangaContext = null!;
|
||||||
|
[SuppressMessage("ReSharper", "InconsistentNaming")]
|
||||||
|
private ActionsContext ActionsContext = null!;
|
||||||
|
|
||||||
|
protected override void SetContexts(IServiceScope serviceScope)
|
||||||
|
{
|
||||||
|
MangaContext = GetContext<MangaContext>(serviceScope);
|
||||||
|
ActionsContext = GetContext<ActionsContext>(serviceScope);
|
||||||
|
}
|
||||||
|
|
||||||
protected override async Task<BaseWorker[]> DoWorkInternal()
|
protected override async Task<BaseWorker[]> DoWorkInternal()
|
||||||
{
|
{
|
||||||
Log.Debug("Updating metadata...");
|
Log.Debug("Updating metadata...");
|
||||||
// Get MetadataEntries of Manga marked for download
|
// Get MetadataEntries of Manga marked for download
|
||||||
List<MetadataEntry> metadataEntriesToUpdate = await DbContext.MangaConnectorToManga
|
List<MetadataEntry> metadataEntriesToUpdate = await MangaContext.MangaConnectorToManga
|
||||||
.Where(m => m.UseForDownload) // Get marked Manga
|
.Where(m => m.UseForDownload) // Get marked Manga
|
||||||
.Join(
|
.Join(
|
||||||
DbContext.MetadataEntries.Include(e => e.MetadataFetcher).Include(e => e.Manga),
|
MangaContext.MetadataEntries.Include(e => e.MetadataFetcher).Include(e => e.Manga),
|
||||||
mcId => mcId.ObjId,
|
mcId => mcId.ObjId,
|
||||||
e => e.MangaId,
|
e => e.MangaId,
|
||||||
(mcId, e) => e) // return MetadataEntry
|
(mcId, e) => e) // return MetadataEntry
|
||||||
@@ -33,12 +47,16 @@ public class UpdateMetadataWorker(TimeSpan? interval = null, IEnumerable<BaseWor
|
|||||||
foreach (MetadataEntry metadataEntry in metadataEntriesToUpdate)
|
foreach (MetadataEntry metadataEntry in metadataEntriesToUpdate)
|
||||||
{
|
{
|
||||||
Log.Debug($"Updating metadata of {metadataEntry}...");
|
Log.Debug($"Updating metadata of {metadataEntry}...");
|
||||||
await metadataEntry.MetadataFetcher.UpdateMetadata(metadataEntry, DbContext, CancellationToken);
|
await metadataEntry.MetadataFetcher.UpdateMetadata(metadataEntry, MangaContext, CancellationToken);
|
||||||
|
ActionsContext.Actions.Add(new MetadataUpdatedActionRecord(metadataEntry.Manga, metadataEntry.MetadataFetcher));
|
||||||
}
|
}
|
||||||
Log.Debug("Updated metadata.");
|
Log.Debug("Updated metadata.");
|
||||||
|
|
||||||
if(await DbContext.Sync(CancellationToken, GetType(), System.Reflection.MethodBase.GetCurrentMethod()?.Name) is { success: false } e)
|
if(await MangaContext.Sync(CancellationToken, GetType(), System.Reflection.MethodBase.GetCurrentMethod()?.Name) is { success: false } e)
|
||||||
Log.Error($"Failed to save database changes: {e.exceptionMessage}");
|
Log.Error($"Failed to save database changes: {e.exceptionMessage}");
|
||||||
|
|
||||||
|
if(await ActionsContext.Sync(CancellationToken, GetType(), "Metadata Updated") is { success: false } actionsContextException)
|
||||||
|
Log.Error($"Failed to save database changes: {actionsContextException.exceptionMessage}");
|
||||||
|
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
@@ -1,20 +1,27 @@
|
|||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using API.Schema.LibraryContext;
|
using API.Schema.LibraryContext;
|
||||||
using API.Schema.LibraryContext.LibraryConnectors;
|
using API.Schema.LibraryContext.LibraryConnectors;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Newtonsoft.Json;
|
|
||||||
using Newtonsoft.Json.Converters;
|
|
||||||
|
|
||||||
namespace API.Workers;
|
namespace API.Workers;
|
||||||
|
|
||||||
public class RefreshLibrariesWorker(IEnumerable<BaseWorker>? dependsOn = null) : BaseWorkerWithContext<LibraryContext>(dependsOn)
|
public class RefreshLibrariesWorker(IEnumerable<BaseWorker>? dependsOn = null) : BaseWorkerWithContexts(dependsOn)
|
||||||
{
|
{
|
||||||
public static DateTime LastRefresh { get; set; } = DateTime.UnixEpoch;
|
public static DateTime LastRefresh { get; set; } = DateTime.UnixEpoch;
|
||||||
|
|
||||||
|
[SuppressMessage("ReSharper", "InconsistentNaming")]
|
||||||
|
private LibraryContext LibraryContext = null!;
|
||||||
|
|
||||||
|
protected override void SetContexts(IServiceScope serviceScope)
|
||||||
|
{
|
||||||
|
LibraryContext = GetContext<LibraryContext>(serviceScope);
|
||||||
|
}
|
||||||
|
|
||||||
protected override async Task<BaseWorker[]> DoWorkInternal()
|
protected override async Task<BaseWorker[]> DoWorkInternal()
|
||||||
{
|
{
|
||||||
Log.Debug("Refreshing libraries...");
|
Log.Debug("Refreshing libraries...");
|
||||||
LastRefresh = DateTime.UtcNow;
|
LastRefresh = DateTime.UtcNow;
|
||||||
List<LibraryConnector> libraries = await DbContext.LibraryConnectors.ToListAsync(CancellationToken);
|
List<LibraryConnector> libraries = await LibraryContext.LibraryConnectors.ToListAsync(CancellationToken);
|
||||||
foreach (LibraryConnector connector in libraries)
|
foreach (LibraryConnector connector in libraries)
|
||||||
await connector.UpdateLibrary(CancellationToken);
|
await connector.UpdateLibrary(CancellationToken);
|
||||||
Log.Debug("Libraries Refreshed...");
|
Log.Debug("Libraries Refreshed...");
|
||||||
|
@@ -5,6 +5,88 @@
|
|||||||
"version": "2.0"
|
"version": "2.0"
|
||||||
},
|
},
|
||||||
"paths": {
|
"paths": {
|
||||||
|
"/v2/Actions/Types": {
|
||||||
|
"get": {
|
||||||
|
"tags": [
|
||||||
|
"Actions"
|
||||||
|
],
|
||||||
|
"summary": "Returns the available Action Types (API.Schema.ActionsContext.ActionRecord.Action) performed by Tranga",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "List of performed action-types"
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Database error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/v2/Actions/Interval": {
|
||||||
|
"post": {
|
||||||
|
"tags": [
|
||||||
|
"Actions"
|
||||||
|
],
|
||||||
|
"summary": "Returns API.Schema.ActionsContext.ActionRecord performed in API.Controllers.ActionsController.Interval",
|
||||||
|
"requestBody": {
|
||||||
|
"content": {
|
||||||
|
"application/json-patch+json; x-version=2.0": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/Interval"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"application/json; x-version=2.0": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/Interval"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"text/json; x-version=2.0": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/Interval"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"application/*+json; x-version=2.0": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/Interval"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "List of performed actions"
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Database error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/v2/Actions/Type/{Type}": {
|
||||||
|
"get": {
|
||||||
|
"tags": [
|
||||||
|
"Actions"
|
||||||
|
],
|
||||||
|
"summary": "Returns API.Schema.ActionsContext.ActionRecord with type Type",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "Type",
|
||||||
|
"in": "path",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "List of performed actions"
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Database error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/v2/Chapters/{MangaId}": {
|
"/v2/Chapters/{MangaId}": {
|
||||||
"get": {
|
"get": {
|
||||||
"tags": [
|
"tags": [
|
||||||
@@ -1128,6 +1210,16 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Error during Database Operation",
|
||||||
|
"content": {
|
||||||
|
"text/plain; x-version=2.0": {
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"202": {
|
"202": {
|
||||||
"description": "Folder is going to be moved"
|
"description": "Folder is going to be moved"
|
||||||
}
|
}
|
||||||
@@ -3401,6 +3493,20 @@
|
|||||||
},
|
},
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
},
|
},
|
||||||
|
"Interval": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"start": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time"
|
||||||
|
},
|
||||||
|
"end": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
"LibraryConnector": {
|
"LibraryConnector": {
|
||||||
"required": [
|
"required": [
|
||||||
"baseUrl",
|
"baseUrl",
|
||||||
|
10
CreateMigrations.sh
Executable file
10
CreateMigrations.sh
Executable file
@@ -0,0 +1,10 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
if [ "$#" -ne 1 ]; then
|
||||||
|
echo "Usage: $0 <migrationname>"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
cd API || exit
|
||||||
|
dotnet ef migrations add $1 --context MangaContext --output-dir Migrations/Manga
|
||||||
|
dotnet ef migrations add $1 --context LibraryContext --output-dir Migrations/Library
|
||||||
|
dotnet ef migrations add $1 --context NotificationsContext --output-dir Migrations/Notifications
|
||||||
|
dotnet ef migrations add $1 --context ActionsContext --output-dir Migrations/Actions
|
Reference in New Issue
Block a user