Improved CancellationToken usage, Added more logging to Workers, Added Code-comments (please future me, be thankful)

This commit is contained in:
2025-09-02 19:36:06 +02:00
parent 6fa166363a
commit 55f04710a7
16 changed files with 235 additions and 121 deletions

View File

@@ -71,7 +71,11 @@ public class Chapter : Identifiable, IComparable<Chapter>
/// Checks the filesystem if an archive at the ArchiveFilePath exists /// Checks the filesystem if an archive at the ArchiveFilePath exists
/// </summary> /// </summary>
/// <returns>True if archive exists on disk</returns> /// <returns>True if archive exists on disk</returns>
public bool CheckDownloaded() => File.Exists(FullArchiveFilePath); public bool CheckDownloaded()
{
//TODO Log here
return File.Exists(FullArchiveFilePath);
}
/// Placeholders: /// Placeholders:
/// %M Obj Name /// %M Obj Name

View File

@@ -32,4 +32,6 @@ public class MetadataEntry
this.Identifier = identifier; this.Identifier = identifier;
this.MetadataFetcherName = metadataFetcherName; this.MetadataFetcherName = metadataFetcherName;
} }
public override string ToString() => $"{GetType().FullName} {MangaId} {MetadataFetcherName}";
} }

View File

@@ -48,12 +48,22 @@ public class MyAnimeList : MetadataFetcher
/// <exception cref="DbUpdateException"></exception> /// <exception cref="DbUpdateException"></exception>
public override async Task UpdateMetadata(MetadataEntry metadataEntry, MangaContext dbContext, CancellationToken token) public override async Task UpdateMetadata(MetadataEntry metadataEntry, MangaContext dbContext, CancellationToken token)
{ {
if (await dbContext.Mangas.FirstOrDefaultAsync(m => m.Key == metadataEntry.MangaId, token) is not { } dbManga) Manga? dbManga = metadataEntry.Manga; //Might be null!
if (dbManga is null)
{
if (await dbContext.Mangas.FirstOrDefaultAsync(m => m.Key == metadataEntry.MangaId, token) is not
{ } update)
throw new DbUpdateException("Manga not found"); throw new DbUpdateException("Manga not found");
dbManga = update;
}
// Load all collections (tags, links, authors)...
foreach (CollectionEntry collectionEntry in dbContext.Entry(dbManga).Collections) foreach (CollectionEntry collectionEntry in dbContext.Entry(dbManga).Collections)
{
if(!collectionEntry.IsLoaded)
await collectionEntry.LoadAsync(token); await collectionEntry.LoadAsync(token);
await dbContext.Entry(dbManga).Navigation(nameof(Manga.Library)).LoadAsync(token); }
await dbContext.Entry(dbManga).Reference(m => m.Library).LoadAsync(token);
MangaFull resultData; MangaFull resultData;
try try

View File

@@ -207,28 +207,4 @@ public static class Tranga
return true; return true;
} }
internal static bool AddChapterToContext((Chapter, MangaConnectorId<Chapter>) addChapter, MangaContext context, [NotNullWhen(true)] out Chapter? chapter, CancellationToken token) =>
AddChapterToContext(addChapter.Item1, addChapter.Item2, context, out chapter, token);
internal static bool AddChapterToContext(Chapter addChapter, MangaConnectorId<Chapter> addChId, MangaContext context, [NotNullWhen(true)] out Chapter? chapter, CancellationToken token)
{
chapter = context.Chapters.Where(ch => ch.Key == addChapter.Key)
.Include(ch => ch.ParentManga)
.Include(ch => ch.MangaConnectorIds)
.FirstOrDefault();
if (chapter is not null)
{
chapter.MangaConnectorIds = chapter.MangaConnectorIds.UnionBy(addChapter.MangaConnectorIds, id => id.Key).ToList();
}
else
{
context.Chapters.Add(addChapter);
chapter = addChapter;
}
if (context.Sync(token).Result is { success: false })
return false;
return true;
}
} }

View File

@@ -26,7 +26,8 @@ public abstract class BaseWorker : Identifiable
public IEnumerable<BaseWorker> MissingDependencies => DependsOn.Where(d => d.State < WorkerExecutionState.Completed); public IEnumerable<BaseWorker> MissingDependencies => DependsOn.Where(d => d.State < WorkerExecutionState.Completed);
public bool AllDependenciesFulfilled => DependsOn.All(d => d.State >= WorkerExecutionState.Completed); public bool AllDependenciesFulfilled => DependsOn.All(d => d.State >= WorkerExecutionState.Completed);
internal WorkerExecutionState State { get; private set; } internal WorkerExecutionState State { get; private set; }
protected CancellationTokenSource CancellationTokenSource = new (); private CancellationTokenSource _cancellationTokenSource = new ();
protected CancellationToken CancellationToken => _cancellationTokenSource.Token;
protected ILog Log { get; init; } protected ILog Log { get; init; }
/// <summary> /// <summary>
@@ -36,7 +37,7 @@ public abstract class BaseWorker : Identifiable
{ {
Log.Debug($"Cancelled {this}"); Log.Debug($"Cancelled {this}");
this.State = WorkerExecutionState.Cancelled; this.State = WorkerExecutionState.Cancelled;
CancellationTokenSource.Cancel(); _cancellationTokenSource.Cancel();
} }
/// <summary> /// <summary>
@@ -46,7 +47,7 @@ public abstract class BaseWorker : Identifiable
{ {
Log.Debug($"Failed {this}"); Log.Debug($"Failed {this}");
this.State = WorkerExecutionState.Failed; this.State = WorkerExecutionState.Failed;
CancellationTokenSource.Cancel(); _cancellationTokenSource.Cancel();
} }
public BaseWorker(IEnumerable<BaseWorker>? dependsOn = null) public BaseWorker(IEnumerable<BaseWorker>? dependsOn = null)
@@ -75,7 +76,7 @@ public abstract class BaseWorker : Identifiable
{ {
// Start the worker // Start the worker
Log.Debug($"Checking {this}"); Log.Debug($"Checking {this}");
this.CancellationTokenSource = new(TimeSpan.FromMinutes(10)); this._cancellationTokenSource = new(TimeSpan.FromMinutes(10));
this.State = WorkerExecutionState.Waiting; this.State = WorkerExecutionState.Waiting;
// Wait for dependencies, start them if necessary // Wait for dependencies, start them if necessary
@@ -111,7 +112,7 @@ public abstract class BaseWorker : Identifiable
private BaseWorker[] WaitForDependencies() private BaseWorker[] WaitForDependencies()
{ {
Log.Info($"Waiting for {MissingDependencies.Count()} Dependencies {this}:\n\t{string.Join("\n\t", MissingDependencies.Select(d => d.ToString()))}"); Log.Info($"Waiting for {MissingDependencies.Count()} Dependencies {this}:\n\t{string.Join("\n\t", MissingDependencies.Select(d => d.ToString()))}");
while (CancellationTokenSource.IsCancellationRequested == false && MissingDependencies.Any()) while (_cancellationTokenSource.IsCancellationRequested == false && MissingDependencies.Any())
{ {
Thread.Sleep(Tranga.Settings.WorkCycleTimeoutMs); Thread.Sleep(Tranga.Settings.WorkCycleTimeoutMs);
} }

View File

@@ -13,28 +13,41 @@ using static System.IO.UnixFileMode;
namespace API.Workers; namespace API.Workers;
/// <summary>
/// Downloads single chapter for Manga from Mangaconnector
/// </summary>
/// <param name="chId"></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) : BaseWorkerWithContext<MangaContext>(dependsOn)
{ {
internal readonly string MangaConnectorIdId = chId.Key; internal readonly string MangaConnectorIdId = chId.Key;
protected override async Task<BaseWorker[]> DoWorkInternal() protected override async Task<BaseWorker[]> DoWorkInternal()
{ {
if(await DbContext.MangaConnectorToChapter.FirstOrDefaultAsync(c => c.Key == MangaConnectorIdId, CancellationTokenSource.Token) is not { } mangaConnectorId) Log.Debug($"Downloading chapter for MangaConnectorId {MangaConnectorIdId}...");
// Getting MangaConnector info
if (await DbContext.MangaConnectorToChapter
.Include(id => id.Obj)
.ThenInclude(c => c.ParentManga)
.ThenInclude(m => m.Library)
.FirstOrDefaultAsync(c => c.Key == MangaConnectorIdId, CancellationToken) is not { } mangaConnectorId)
{
Log.Error("Could not get MangaConnectorId.");
return []; //TODO Exception? return []; //TODO Exception?
}
if (!Tranga.TryGetMangaConnector(mangaConnectorId.MangaConnectorName, out MangaConnector? mangaConnector)) if (!Tranga.TryGetMangaConnector(mangaConnectorId.MangaConnectorName, out MangaConnector? mangaConnector))
{
Log.Error("Could not get MangaConnector.");
return []; //TODO Exception? return []; //TODO Exception?
}
Log.Debug($"Downloading chapter for MangaConnectorId {mangaConnectorId}...");
await DbContext.Entry(mangaConnectorId).Navigation(nameof(MangaConnectorId<Chapter>.Obj)).LoadAsync(CancellationTokenSource.Token);
Chapter chapter = mangaConnectorId.Obj; Chapter chapter = mangaConnectorId.Obj;
if (chapter.Downloaded) if (chapter.Downloaded)
{ {
Log.Info("Chapter was already downloaded."); Log.Info("Chapter was already downloaded.");
return []; return [];
} }
await DbContext.Entry(chapter).Navigation(nameof(Chapter.ParentManga)).LoadAsync(CancellationTokenSource.Token);
await DbContext.Entry(chapter.ParentManga).Navigation(nameof(Manga.Library)).LoadAsync(CancellationTokenSource.Token);
if (chapter.ParentManga.LibraryId is null) if (chapter.ParentManga.LibraryId is null)
{ {
Log.Info($"Library is not set for {chapter.ParentManga} {chapter}"); Log.Info($"Library is not set for {chapter.ParentManga} {chapter}");
@@ -97,9 +110,8 @@ public class DownloadChapterFromMangaconnectorWorker(MangaConnectorId<Chapter> c
Log.Debug($"Creating ComicInfo.xml {chapter}"); Log.Debug($"Creating ComicInfo.xml {chapter}");
foreach (CollectionEntry collectionEntry in DbContext.Entry(chapter.ParentManga).Collections) foreach (CollectionEntry collectionEntry in DbContext.Entry(chapter.ParentManga).Collections)
await collectionEntry.LoadAsync(CancellationTokenSource.Token); await collectionEntry.LoadAsync(CancellationToken);
await DbContext.Entry(chapter.ParentManga).Navigation(nameof(Manga.Library)).LoadAsync(CancellationTokenSource.Token); await File.WriteAllTextAsync(Path.Join(tempFolder, "ComicInfo.xml"), chapter.GetComicInfoXmlString(), CancellationToken);
await File.WriteAllTextAsync(Path.Join(tempFolder, "ComicInfo.xml"), chapter.GetComicInfoXmlString(), CancellationTokenSource.Token);
Log.Debug($"Packaging images to archive {chapter}"); Log.Debug($"Packaging images to archive {chapter}");
//ZIP-it and ship-it //ZIP-it and ship-it
@@ -108,8 +120,11 @@ public class DownloadChapterFromMangaconnectorWorker(MangaConnectorId<Chapter> c
File.SetUnixFileMode(saveArchiveFilePath, UserRead | UserWrite | UserExecute | GroupRead | GroupWrite | GroupExecute | OtherRead | OtherExecute); File.SetUnixFileMode(saveArchiveFilePath, UserRead | UserWrite | UserExecute | GroupRead | GroupWrite | GroupExecute | OtherRead | OtherExecute);
Directory.Delete(tempFolder, true); //Cleanup Directory.Delete(tempFolder, true); //Cleanup
chapter.Downloaded = true; DbContext.Entry(chapter).Property(c => c.Downloaded).CurrentValue = true;
await DbContext.Sync(CancellationTokenSource.Token); if(await DbContext.Sync(CancellationToken) is { success: false } e)
Log.Error($"Failed to save database changes: {e.exceptionMessage}");
Log.Debug($"Downloaded chapter {chapter}.");
return []; return [];
} }
@@ -164,7 +179,7 @@ public class DownloadChapterFromMangaconnectorWorker(MangaConnectorId<Chapter> c
} }
//TODO MangaConnector Selection //TODO MangaConnector Selection
await DbContext.Entry(manga).Collection(m => m.MangaConnectorIds).LoadAsync(CancellationTokenSource.Token); await DbContext.Entry(manga).Collection(m => m.MangaConnectorIds).LoadAsync(CancellationToken);
MangaConnectorId<Manga> mangaConnectorId = manga.MangaConnectorIds.First(); MangaConnectorId<Manga> mangaConnectorId = manga.MangaConnectorIds.First();
if (!Tranga.TryGetMangaConnector(mangaConnectorId.MangaConnectorName, out MangaConnector? mangaConnector)) if (!Tranga.TryGetMangaConnector(mangaConnectorId.MangaConnectorName, out MangaConnector? mangaConnector))
{ {
@@ -173,7 +188,7 @@ public class DownloadChapterFromMangaconnectorWorker(MangaConnectorId<Chapter> c
} }
Log.Info($"Copying cover to {publicationFolder}"); Log.Info($"Copying cover to {publicationFolder}");
await DbContext.Entry(mangaConnectorId).Navigation(nameof(MangaConnectorId<Manga>.Obj)).LoadAsync(CancellationTokenSource.Token); await DbContext.Entry(mangaConnectorId).Navigation(nameof(MangaConnectorId<Manga>.Obj)).LoadAsync(CancellationToken);
string? fileInCache = manga.CoverFileNameInCache ?? mangaConnector.SaveCoverImageToCache(mangaConnectorId); string? fileInCache = manga.CoverFileNameInCache ?? mangaConnector.SaveCoverImageToCache(mangaConnectorId);
if (fileInCache is null) if (fileInCache is null)
{ {

View File

@@ -4,23 +4,42 @@ using Microsoft.EntityFrameworkCore;
namespace API.Workers; namespace API.Workers;
/// <summary>
/// Downloads the cover for Manga from Mangaconnector
/// </summary>
public class DownloadCoverFromMangaconnectorWorker(MangaConnectorId<Manga> mcId, IEnumerable<BaseWorker>? dependsOn = null) public class DownloadCoverFromMangaconnectorWorker(MangaConnectorId<Manga> mcId, IEnumerable<BaseWorker>? dependsOn = null)
: BaseWorkerWithContext<MangaContext>(dependsOn) : BaseWorkerWithContext<MangaContext>(dependsOn)
{ {
internal readonly string MangaConnectorIdId = mcId.Key; internal readonly string MangaConnectorIdId = mcId.Key;
protected override async Task<BaseWorker[]> DoWorkInternal() protected override async Task<BaseWorker[]> DoWorkInternal()
{ {
if (await DbContext.MangaConnectorToManga.FirstOrDefaultAsync(c => c.Key == MangaConnectorIdId) is not { } mangaConnectorId) Log.Debug($"Getting Cover for MangaConnectorId {MangaConnectorIdId}...");
// Getting MangaConnector info
if (await DbContext.MangaConnectorToManga
.Include(id => id.Obj)
.FirstOrDefaultAsync(c => c.Key == MangaConnectorIdId, CancellationToken) is not { } mangaConnectorId)
{
Log.Error("Could not get MangaConnectorId.");
return []; //TODO Exception? return []; //TODO Exception?
}
if (!Tranga.TryGetMangaConnector(mangaConnectorId.MangaConnectorName, out MangaConnector? mangaConnector)) if (!Tranga.TryGetMangaConnector(mangaConnectorId.MangaConnectorName, out MangaConnector? mangaConnector))
{
Log.Error("Could not get MangaConnector.");
return []; //TODO Exception? return []; //TODO Exception?
}
Log.Debug($"Getting Cover for MangaConnectorId {mangaConnectorId}...");
await DbContext.Entry(mangaConnectorId).Navigation(nameof(MangaConnectorId<Manga>.Obj)).LoadAsync(CancellationTokenSource.Token); string? coverFileName = mangaConnector.SaveCoverImageToCache(mangaConnectorId);
Manga manga = mangaConnectorId.Obj; if (coverFileName is null)
{
Log.Error($"Could not get Cover for MangaConnectorId {mangaConnectorId}.");
return [];
}
DbContext.Entry(mangaConnectorId.Obj).Property(m => m.CoverFileNameInCache).CurrentValue = coverFileName;
manga.CoverFileNameInCache = mangaConnector.SaveCoverImageToCache(mangaConnectorId); if(await DbContext.Sync(CancellationToken) is { success: false } e)
Log.Error($"Failed to save database changes: {e.exceptionMessage}");
await DbContext.Sync(CancellationTokenSource.Token);
return []; return [];
} }

View File

@@ -4,35 +4,51 @@ using Microsoft.EntityFrameworkCore;
namespace API.Workers; namespace API.Workers;
/// <summary>
/// Retrieves the metadata of available chapters on the Mangaconnector
/// </summary>
/// <param name="mcId"></param>
/// <param name="language"></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) : BaseWorkerWithContext<MangaContext>(dependsOn)
{ {
internal readonly string MangaConnectorIdId = mcId.Key; internal readonly string MangaConnectorIdId = mcId.Key;
protected override async Task<BaseWorker[]> DoWorkInternal() protected override async Task<BaseWorker[]> DoWorkInternal()
{ {
if (await DbContext.MangaConnectorToManga.FirstOrDefaultAsync(c => c.Key == MangaConnectorIdId) is not { } mangaConnectorId) Log.Debug($"Getting Chapters for MangaConnectorId {MangaConnectorIdId}...");
// Getting MangaConnector info
if (await DbContext.MangaConnectorToManga
.Include(id => id.Obj)
.FirstOrDefaultAsync(c => c.Key == MangaConnectorIdId, CancellationToken) is not { } mangaConnectorId)
{
Log.Error("Could not get MangaConnectorId.");
return []; //TODO Exception? return []; //TODO Exception?
}
if (!Tranga.TryGetMangaConnector(mangaConnectorId.MangaConnectorName, out MangaConnector? mangaConnector)) if (!Tranga.TryGetMangaConnector(mangaConnectorId.MangaConnectorName, out MangaConnector? mangaConnector))
{
Log.Error("Could not get MangaConnector.");
return []; //TODO Exception? return []; //TODO Exception?
}
Log.Debug($"Getting Chapters for MangaConnectorId {mangaConnectorId}...");
await DbContext.Entry(mangaConnectorId).Navigation(nameof(MangaConnectorId<Manga>.Obj)).LoadAsync(CancellationTokenSource.Token);
Manga manga = mangaConnectorId.Obj; Manga manga = mangaConnectorId.Obj;
await DbContext.Entry(manga).Collection(m => m.Chapters).LoadAsync(CancellationTokenSource.Token); // Load existing Chapters (in database)
await DbContext.Entry(manga).Collection(m => m.Chapters).LoadAsync(CancellationToken);
// This gets all chapters that are not downloaded // This gets all chapters that are not downloaded
(Chapter, MangaConnectorId<Chapter>)[] allChapters = (Chapter chapter, MangaConnectorId<Chapter> chapterId)[] allChapters =
mangaConnector.GetChapters(mangaConnectorId, language).DistinctBy(c => c.Item1.Key).ToArray(); mangaConnector.GetChapters(mangaConnectorId, language).DistinctBy(c => c.Item1.Key).ToArray();
int addedChapters = 0; int beforeAmount = manga.Chapters.Count;
foreach ((Chapter chapter, MangaConnectorId<Chapter> mcId) newChapter in allChapters) Log.Debug($"Got {allChapters.Length} chapters from connector.");
{ DbContext.Entry(manga).Collection(m => m.Chapters).CurrentValue = manga.Chapters.UnionBy(allChapters.Select(c => c.chapter), c => c.Key);
if (Tranga.AddChapterToContext(newChapter, DbContext, out Chapter? addedChapter, CancellationTokenSource.Token) == false) int afterAmount = manga.Chapters.Count;
continue;
manga.Chapters.Add(addedChapter);
}
Log.Info($"{manga.Chapters.Count} existing + {addedChapters} new chapters.");
await DbContext.Sync(CancellationTokenSource.Token); Log.Debug($"Got {afterAmount} new chapters.");
if(await DbContext.Sync(CancellationToken) is { success: false } e)
Log.Error($"Failed to save database changes: {e.exceptionMessage}");
return []; return [];
} }

View File

@@ -3,6 +3,9 @@ using Microsoft.EntityFrameworkCore;
namespace API.Workers; namespace API.Workers;
/// <summary>
/// Moves a Manga to a different Library
/// </summary>
public class MoveMangaLibraryWorker(Manga manga, FileLibrary toLibrary, IEnumerable<BaseWorker>? dependsOn = null) public class MoveMangaLibraryWorker(Manga manga, FileLibrary toLibrary, IEnumerable<BaseWorker>? dependsOn = null)
: BaseWorkerWithContext<MangaContext>(dependsOn) : BaseWorkerWithContext<MangaContext>(dependsOn)
{ {
@@ -10,20 +13,32 @@ public class MoveMangaLibraryWorker(Manga manga, FileLibrary toLibrary, IEnumera
internal readonly string LibraryId = toLibrary.Key; internal readonly string LibraryId = toLibrary.Key;
protected override async Task<BaseWorker[]> DoWorkInternal() protected override async Task<BaseWorker[]> DoWorkInternal()
{ {
if (await DbContext.Mangas.FirstOrDefaultAsync(m => m.Key == MangaId, CancellationTokenSource.Token) is not { } manga) Log.Debug("Moving Manga...");
// Get Manga (with Chapters and Library)
if (await DbContext.Mangas
.Include(m => m.Chapters)
.Include(m => m.Library)
.FirstOrDefaultAsync(m => m.Key == MangaId, CancellationToken) is not { } manga)
{
Log.Error("Could not find Manga.");
return []; //TODO Exception? return []; //TODO Exception?
if (await DbContext.FileLibraries.FirstOrDefaultAsync(l => l.Key == LibraryId, CancellationTokenSource.Token) is not { } toLibrary) }
// Get new Library
if (await DbContext.FileLibraries.FirstOrDefaultAsync(l => l.Key == LibraryId, CancellationToken) is not { } toLibrary)
{
Log.Error("Could not find Library.");
return []; //TODO Exception? return []; //TODO Exception?
}
await DbContext.Entry(manga).Collection(m => m.Chapters).LoadAsync(CancellationTokenSource.Token); // Save old Path (to later move chapters)
await DbContext.Entry(manga).Navigation(nameof(Manga.Library)).LoadAsync(CancellationTokenSource.Token);
Dictionary<Chapter, string> oldPath = manga.Chapters.ToDictionary(c => c, c => c.FullArchiveFilePath); Dictionary<Chapter, string> oldPath = manga.Chapters.ToDictionary(c => c, c => c.FullArchiveFilePath);
manga.Library = toLibrary; // Set new Path
DbContext.Entry(manga).Property(m => m.Library).CurrentValue = toLibrary;
if (await DbContext.Sync(CancellationTokenSource.Token) is { success: false }) if (await DbContext.Sync(CancellationToken) is { success: false })
return []; return [];
// Create Jobs to move chapters from old to new Path
return manga.Chapters.Select(c => new MoveFileOrFolderWorker(c.FullArchiveFilePath, oldPath[c])).ToArray<BaseWorker>(); return manga.Chapters.Select(c => new MoveFileOrFolderWorker(c.FullArchiveFilePath, oldPath[c])).ToArray<BaseWorker>();
} }

View File

@@ -3,23 +3,28 @@ using Microsoft.EntityFrameworkCore;
namespace API.Workers; namespace API.Workers;
/// <summary>
/// Creates Jobs to update available Chapters for all Manga that are marked for Download
/// </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 : BaseWorkerWithContext<MangaContext>(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);
protected override Task<BaseWorker[]> DoWorkInternal() protected override async Task<BaseWorker[]> DoWorkInternal()
{ {
IQueryable<MangaConnectorId<Manga>> connectorIdsManga = DbContext.MangaConnectorToManga Log.Debug("Checking for new chapters...");
List<MangaConnectorId<Manga>> connectorIdsManga = await DbContext.MangaConnectorToManga
.Include(id => id.Obj) .Include(id => id.Obj)
.Where(id => id.UseForDownload); .Where(id => id.UseForDownload)
.ToListAsync(CancellationToken);
Log.Debug($"Creating {connectorIdsManga.Count} update jobs...");
List<BaseWorker> newWorkers = new(); List<BaseWorker> newWorkers = connectorIdsManga.Select(id => new RetrieveMangaChaptersFromMangaconnectorWorker(id, Tranga.Settings.DownloadLanguage))
foreach (MangaConnectorId<Manga> mangaConnectorId in connectorIdsManga) .ToList<BaseWorker>();
newWorkers.Add(new RetrieveMangaChaptersFromMangaconnectorWorker(mangaConnectorId, Tranga.Settings.DownloadLanguage));
return new Task<BaseWorker[]>(() => newWorkers.ToArray()); return newWorkers.ToArray();
} }
} }

View File

@@ -1,7 +1,11 @@
using API.Schema.NotificationsContext; using API.Schema.NotificationsContext;
using Microsoft.EntityFrameworkCore;
namespace API.Workers.MaintenanceWorkers; namespace API.Workers.MaintenanceWorkers;
/// <summary>
/// Removes sent notifications from database
/// </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 : BaseWorkerWithContext<NotificationsContext>(dependsOn), IPeriodic
{ {
@@ -10,10 +14,14 @@ public class RemoveOldNotificationsWorker(TimeSpan? interval = null, IEnumerable
protected override async Task<BaseWorker[]> DoWorkInternal() protected override async Task<BaseWorker[]> DoWorkInternal()
{ {
IQueryable<Notification> toRemove = DbContext.Notifications.Where(n => n.IsSent || DateTime.UtcNow - n.Date > Interval); Log.Debug("Removing old notifications...");
List<Notification> toRemove = await DbContext.Notifications.Where(n => n.IsSent).ToListAsync(CancellationToken);
Log.Debug($"Removing {toRemove.Count} old notifications...");
DbContext.RemoveRange(toRemove); DbContext.RemoveRange(toRemove);
await DbContext.Sync(CancellationTokenSource.Token); if(await DbContext.Sync(CancellationToken) is { success: false } e)
Log.Error($"Failed to save database changes: {e.exceptionMessage}");
return []; return [];
} }

View File

@@ -1,8 +1,14 @@
using API.Schema.NotificationsContext; using API.Schema.NotificationsContext;
using API.Schema.NotificationsContext.NotificationConnectors; using API.Schema.NotificationsContext.NotificationConnectors;
using Microsoft.EntityFrameworkCore;
namespace API.Workers; namespace API.Workers;
/// <summary>
/// Send notifications to NotificationConnectors
/// </summary>
/// <param name="interval"></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 : BaseWorkerWithContext<NotificationsContext>(dependsOn), IPeriodic
{ {
@@ -10,19 +16,26 @@ public class SendNotificationsWorker(TimeSpan? interval = null, IEnumerable<Base
public TimeSpan Interval { get; set; } = interval??TimeSpan.FromMinutes(1); public TimeSpan Interval { get; set; } = interval??TimeSpan.FromMinutes(1);
protected override async Task<BaseWorker[]> DoWorkInternal() protected override async Task<BaseWorker[]> DoWorkInternal()
{ {
NotificationConnector[] connectors = DbContext.NotificationConnectors.ToArray(); Log.Debug("Sending notifications...");
Notification[] notifications = DbContext.Notifications.Where(n => n.IsSent == false).ToArray(); List<NotificationConnector> connectors = await DbContext.NotificationConnectors.ToListAsync(CancellationToken);
List<Notification> unsentNotifications = await DbContext.Notifications.Where(n => n.IsSent == false).ToListAsync(CancellationToken);
foreach (Notification notification in notifications) Log.Debug($"Sending {unsentNotifications.Count} notifications to {connectors.Count} connectors...");
unsentNotifications.ForEach(notification =>
{ {
foreach (NotificationConnector connector in connectors) connectors.ForEach(connector =>
{ {
connector.SendNotification(notification.Title, notification.Message); connector.SendNotification(notification.Title, notification.Message);
notification.IsSent = true; DbContext.Entry(notification).Property(n => n.IsSent).CurrentValue = true;
} });
} });
Log.Debug("Notifications sent.");
if(await DbContext.Sync(CancellationToken) is { success: false } e)
Log.Error($"Failed to save database changes: {e.exceptionMessage}");
await DbContext.Sync(CancellationTokenSource.Token);
return []; return [];
} }

View File

@@ -3,22 +3,26 @@ using Microsoft.EntityFrameworkCore;
namespace API.Workers; namespace API.Workers;
/// <summary>
/// Create new Workers for Chapters on Manga marked for Download, that havent been downloaded yet.
/// </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 : BaseWorkerWithContext<MangaContext>(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);
protected override Task<BaseWorker[]> DoWorkInternal() protected override async Task<BaseWorker[]> DoWorkInternal()
{ {
IQueryable<MangaConnectorId<Chapter>> mangaConnectorIds = DbContext.MangaConnectorToChapter // Get missing chapters
List<MangaConnectorId<Chapter>> missingChapters = await DbContext.MangaConnectorToChapter
.Include(id => id.Obj) .Include(id => id.Obj)
.Where(id => id.Obj.Downloaded == false && id.UseForDownload); .Where(id => id.Obj.Downloaded == false && id.UseForDownload)
.ToListAsync(CancellationToken);
List<BaseWorker> newWorkers = new(); // Create new jobs
foreach (MangaConnectorId<Chapter> mangaConnectorId in mangaConnectorIds) List<BaseWorker> newWorkers = missingChapters.Select(mcId => new DownloadChapterFromMangaconnectorWorker(mcId)).ToList<BaseWorker>();
newWorkers.Add(new DownloadChapterFromMangaconnectorWorker(mangaConnectorId));
return new Task<BaseWorker[]>(() => newWorkers.ToArray()); return newWorkers.ToArray();
} }
} }

View File

@@ -3,6 +3,9 @@ using Microsoft.EntityFrameworkCore;
namespace API.Workers; namespace API.Workers;
/// <summary>
/// Updates the database to reflect changes made on disk
/// </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 : BaseWorkerWithContext<MangaContext>(dependsOn), IPeriodic
{ {
@@ -10,10 +13,14 @@ public class UpdateChaptersDownloadedWorker(TimeSpan? interval = null, IEnumerab
public TimeSpan Interval { get; set; } = interval??TimeSpan.FromMinutes(60); public TimeSpan Interval { get; set; } = interval??TimeSpan.FromMinutes(60);
protected override async Task<BaseWorker[]> DoWorkInternal() protected override async Task<BaseWorker[]> DoWorkInternal()
{ {
foreach (Chapter dbContextChapter in DbContext.Chapters.Include(c => c.ParentManga)) Log.Debug("Checking chapter files...");
dbContextChapter.Downloaded = dbContextChapter.CheckDownloaded(); List<Chapter> chapters = await DbContext.Chapters.Include(c => c.ParentManga).ToListAsync(CancellationToken);
Log.Debug($"Checking {chapters.Count} chapters...");
chapters.ForEach(chapter => DbContext.Entry(chapter).Property(c => c.Downloaded).CurrentValue = chapter.CheckDownloaded());
if(await DbContext.Sync(CancellationToken) is { success: false } e)
Log.Error($"Failed to save database changes: {e.exceptionMessage}");
await DbContext.Sync(CancellationTokenSource.Token);
return []; return [];
} }
} }

View File

@@ -1,7 +1,13 @@
using API.Schema.MangaContext; using API.Schema.MangaContext;
using Microsoft.EntityFrameworkCore;
namespace API.Workers; namespace API.Workers;
/// <summary>
/// Creates Workers to update covers for Manga
/// </summary>
/// <param name="interval"></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 : BaseWorkerWithContext<MangaContext>(dependsOn), IPeriodic
{ {
@@ -9,11 +15,10 @@ public class UpdateCoversWorker(TimeSpan? interval = null, IEnumerable<BaseWorke
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);
protected override Task<BaseWorker[]> DoWorkInternal() protected override async Task<BaseWorker[]> DoWorkInternal()
{ {
List<BaseWorker> workers = new(); List<MangaConnectorId<Manga>> manga = await DbContext.MangaConnectorToManga.Where(mcId => mcId.UseForDownload).ToListAsync(CancellationToken);
foreach (MangaConnectorId<Manga> mangaConnectorId in DbContext.MangaConnectorToManga) List<BaseWorker> newWorkers = manga.Select(m => new DownloadCoverFromMangaconnectorWorker(m)).ToList<BaseWorker>();
workers.Add(new DownloadCoverFromMangaconnectorWorker(mangaConnectorId)); return newWorkers.ToArray();
return new Task<BaseWorker[]>(() => workers.ToArray());
} }
} }

View File

@@ -4,6 +4,11 @@ using Microsoft.EntityFrameworkCore;
namespace API.Workers; namespace API.Workers;
/// <summary>
/// Updates Metadata for all Manga
/// </summary>
/// <param name="interval"></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 : BaseWorkerWithContext<MangaContext>(dependsOn), IPeriodic
{ {
@@ -13,18 +18,27 @@ public class UpdateMetadataWorker(TimeSpan? interval = null, IEnumerable<BaseWor
protected override async Task<BaseWorker[]> DoWorkInternal() protected override async Task<BaseWorker[]> DoWorkInternal()
{ {
IQueryable<string> mangaIds = DbContext.MangaConnectorToManga Log.Debug("Updating metadata...");
.Where(m => m.UseForDownload) // Get MetadataEntries of Manga marked for download
.Select(m => m.ObjId); List<MetadataEntry> metadataEntriesToUpdate = await DbContext.MangaConnectorToManga
IQueryable<MetadataEntry> metadataEntriesToUpdate = DbContext.MetadataEntries .Where(m => m.UseForDownload) // Get marked Manga
.Include(e => e.MetadataFetcher) .Join(
.Where(e => DbContext.MetadataEntries.Include(e => e.MetadataFetcher).Include(e => e.Manga),
mangaIds.Any(id => id == e.MangaId)); mcId => mcId.ObjId,
e => e.MangaId,
(mcId, e) => e) // return MetadataEntry
.ToListAsync(CancellationToken);
Log.Debug($"Updating metadata of {metadataEntriesToUpdate.Count} manga...");
foreach (MetadataEntry metadataEntry in metadataEntriesToUpdate) foreach (MetadataEntry metadataEntry in metadataEntriesToUpdate)
await metadataEntry.MetadataFetcher.UpdateMetadata(metadataEntry, DbContext, CancellationTokenSource.Token); {
Log.Debug($"Updating metadata of {metadataEntry}...");
await metadataEntry.MetadataFetcher.UpdateMetadata(metadataEntry, DbContext, CancellationToken);
}
Log.Debug("Updated metadata.");
await DbContext.Sync(CancellationTokenSource.Token); if(await DbContext.Sync(CancellationToken) is { success: false } e)
Log.Error($"Failed to save database changes: {e.exceptionMessage}");
return []; return [];
} }