Merge pull request #449 from C9Glax/library-refresh

Add Library Refresh Logic
This commit is contained in:
2025-09-21 17:49:38 +02:00
committed by GitHub
12 changed files with 164 additions and 30 deletions

View File

@@ -0,0 +1,21 @@
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using API.Workers;
namespace API.Controllers.Requests;
public record PatchLibraryRefreshRecord
{
/// <summary>
/// When to refresh the Library
/// </summary>
[Required]
[Description("When to refresh the Library")]
public required LibraryRefreshSetting Setting { get; init; }
/// <summary>
/// When <see cref="LibraryRefreshSetting.WhileDownloading"/> is selected, update the time between refreshes
/// </summary>
[Description("When WhileDownloadingis selected, update the time between refreshes")]
public int? RefreshLibraryWhileDownloadingEveryMinutes { get; init; }
}

View File

@@ -1,4 +1,5 @@
using API.MangaDownloadClients; using API.Controllers.Requests;
using API.MangaDownloadClients;
using Asp.Versioning; using Asp.Versioning;
using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
@@ -290,4 +291,19 @@ public class SettingsController() : Controller
Tranga.Settings.SetDownloadLanguage(Language); Tranga.Settings.SetDownloadLanguage(Language);
return TypedResults.Ok(); return TypedResults.Ok();
} }
/// <summary>
/// Sets the time when Libraries are refreshed
/// </summary>
/// <response code="200"></response>
[HttpPatch("LibraryRefresh")]
[ProducesResponseType(Status200OK)]
public Ok SetLibraryRefresh([FromBody]PatchLibraryRefreshRecord requestData)
{
Tranga.Settings.SetLibraryRefreshSetting(requestData.Setting);
if(requestData.RefreshLibraryWhileDownloadingEveryMinutes is { } value)
Tranga.Settings.SetRefreshLibraryWhileDownloadingEveryMinutes(value);
return TypedResults.Ok();
}
} }

View File

@@ -2,6 +2,8 @@
using System.ComponentModel.DataAnnotations.Schema; using System.ComponentModel.DataAnnotations.Schema;
using log4net; using log4net;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
namespace API.Schema.LibraryContext.LibraryConnectors; namespace API.Schema.LibraryContext.LibraryConnectors;
@@ -40,8 +42,16 @@ public abstract class LibraryConnector : Identifiable
internal abstract Task<bool> Test(CancellationToken ct); internal abstract Task<bool> Test(CancellationToken ct);
} }
[JsonConverter(typeof(StringEnumConverter))]
public enum LibraryType : byte public enum LibraryType : byte
{ {
/// <summary>
/// <seealso cref="Komga"/>
/// </summary>
Komga = 0, Komga = 0,
/// <summary>
/// <seealso cref="Kavita"/>
/// </summary>
Kavita = 1 Kavita = 1
} }

View File

@@ -1,13 +1,10 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema; using System.ComponentModel.DataAnnotations.Schema;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Text;
using API.Workers; using API.Workers;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using SixLabors.ImageSharp; using Newtonsoft.Json;
using SixLabors.ImageSharp.Formats.Jpeg; using Newtonsoft.Json.Converters;
using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp.Processing.Processors.Transforms;
using static System.IO.UnixFileMode; using static System.IO.UnixFileMode;
namespace API.Schema.MangaContext; namespace API.Schema.MangaContext;
@@ -138,6 +135,7 @@ public class Manga : Identifiable
public override string ToString() => $"{base.ToString()} {Name}"; public override string ToString() => $"{base.ToString()} {Name}";
} }
[JsonConverter(typeof(StringEnumConverter))]
public enum MangaReleaseStatus : byte public enum MangaReleaseStatus : byte
{ {
Continuing = 0, Continuing = 0,

View File

@@ -1,5 +1,7 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
namespace API.Schema.NotificationsContext; namespace API.Schema.NotificationsContext;
@@ -48,6 +50,7 @@ public class Notification : Identifiable
public override string ToString() => $"{base.ToString()} {Urgency} {Title} {Message}"; public override string ToString() => $"{base.ToString()} {Urgency} {Title} {Message}";
} }
[JsonConverter(typeof(StringEnumConverter))]
public enum NotificationUrgency : byte public enum NotificationUrgency : byte
{ {
Low = 1, Low = 1,

View File

@@ -146,25 +146,27 @@ public static class Tranga
private static Action DefaultAfterWork(BaseWorker worker, Action? callback = null) => () => private static Action DefaultAfterWork(BaseWorker worker, Action? callback = null) => () =>
{ {
Log.Debug($"DefaultAfterWork {worker}"); Log.Debug($"DefaultAfterWork {worker}");
if (RunningWorkers.TryGetValue(worker, out Task<BaseWorker[]>? task)) try
{ {
Log.Debug($"Waiting for Children to exit {worker}"); if (RunningWorkers.TryGetValue(worker, out Task<BaseWorker[]>? task))
task.Wait();
if (task.IsCompleted)
{ {
try Log.Debug($"Waiting for Children to exit {worker}");
task.Wait();
if (task.IsCompleted)
{ {
Log.Debug($"Children done {worker}");
BaseWorker[] newWorkers = task.Result; BaseWorker[] newWorkers = task.Result;
Log.Debug($"{worker} created {newWorkers.Length} new Workers."); Log.Debug($"{worker} created {newWorkers.Length} new Workers.");
AddWorkers(newWorkers); AddWorkers(newWorkers);
} }else
catch (Exception e) Log.Warn($"Children failed: {worker}");
{ }
Log.Error(e); RunningWorkers.Remove(worker, out _);
} }
}else Log.Warn($"Children failed: {worker}"); catch (Exception e)
{
Log.Error(e);
} }
RunningWorkers.Remove(worker, out _);
callback?.Invoke(); callback?.Invoke();
}; };

View File

@@ -1,6 +1,8 @@
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using API.MangaDownloadClients; using API.MangaDownloadClients;
using API.Workers;
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
namespace API; namespace API;
@@ -54,16 +56,20 @@ public struct TrangaSettings()
public int MaxConcurrentWorkers { get; set; } = Math.Max(Environment.ProcessorCount, 4); // Minimum of 4 Tasks, maximum of 1 per Core public int MaxConcurrentWorkers { get; set; } = Math.Max(Environment.ProcessorCount, 4); // Minimum of 4 Tasks, maximum of 1 per Core
public LibraryRefreshSetting LibraryRefreshSetting { get; set; } = LibraryRefreshSetting.AfterMangaFinished;
public int RefreshLibraryWhileDownloadingEveryMinutes { get; set; } = 10;
public static TrangaSettings Load() public static TrangaSettings Load()
{ {
if (!File.Exists(SettingsFilePath)) if (!File.Exists(SettingsFilePath))
new TrangaSettings().Save(); new TrangaSettings().Save();
return JsonConvert.DeserializeObject<TrangaSettings>(File.ReadAllText(SettingsFilePath)); return JsonConvert.DeserializeObject<TrangaSettings>(File.ReadAllText(SettingsFilePath), new StringEnumConverter());
} }
public void Save() public void Save()
{ {
File.WriteAllText(SettingsFilePath, JsonConvert.SerializeObject(this, Formatting.Indented)); File.WriteAllText(SettingsFilePath, JsonConvert.SerializeObject(this, Formatting.Indented, new StringEnumConverter()));
} }
public void SetUserAgent(string value) public void SetUserAgent(string value)
@@ -125,4 +131,16 @@ public struct TrangaSettings()
this.MaxConcurrentWorkers = value; this.MaxConcurrentWorkers = value;
Save(); Save();
} }
public void SetLibraryRefreshSetting(LibraryRefreshSetting setting)
{
this.LibraryRefreshSetting = setting;
Save();
}
public void SetRefreshLibraryWhileDownloadingEveryMinutes(int value)
{
this.RefreshLibraryWhileDownloadingEveryMinutes = value;
Save();
}
} }

View File

@@ -1,5 +1,7 @@
using API.Schema; using API.Schema;
using log4net; using log4net;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
namespace API.Workers; namespace API.Workers;
@@ -116,6 +118,7 @@ public abstract class BaseWorker : Identifiable
} }
} }
[JsonConverter(typeof(StringEnumConverter))]
public enum WorkerExecutionState public enum WorkerExecutionState
{ {
Failed = 0, Failed = 0,

View File

@@ -3,6 +3,7 @@ using System.Runtime.InteropServices;
using API.MangaConnectors; using API.MangaConnectors;
using API.MangaDownloadClients; using API.MangaDownloadClients;
using API.Schema.MangaContext; using API.Schema.MangaContext;
using API.Workers.PeriodicWorkers;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.ChangeTracking; using Microsoft.EntityFrameworkCore.ChangeTracking;
using SixLabors.ImageSharp; using SixLabors.ImageSharp;
@@ -21,16 +22,16 @@ namespace API.Workers.MangaDownloadWorkers;
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; private readonly string _mangaConnectorIdId = chId.Key;
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 DbContext.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)
.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?
@@ -137,8 +138,22 @@ public class DownloadChapterFromMangaconnectorWorker(MangaConnectorId<Chapter> c
Log.Debug($"Downloaded chapter {chapter}."); Log.Debug($"Downloaded chapter {chapter}.");
return []; bool refreshLibrary = await CheckLibraryRefresh();
if(refreshLibrary)
Log.Info($"Condition {Tranga.Settings.LibraryRefreshSetting} met.");
return refreshLibrary? [new RefreshLibrariesWorker()] : [];
} }
private async Task<bool> CheckLibraryRefresh() => Tranga.Settings.LibraryRefreshSetting switch
{
LibraryRefreshSetting.AfterAllFinished => await AllDownloadsFinished(),
LibraryRefreshSetting.AfterMangaFinished => await DbContext.MangaConnectorToChapter.Include(chId => chId.Obj).Where(chId => chId.UseForDownload).AllAsync(chId => chId.Obj.Downloaded, CancellationToken),
LibraryRefreshSetting.AfterEveryChapter => true,
LibraryRefreshSetting.WhileDownloading => await AllDownloadsFinished() || DateTime.UtcNow.Subtract(RefreshLibrariesWorker.LastRefresh).TotalMinutes > Tranga.Settings.RefreshLibraryWhileDownloadingEveryMinutes,
_ => true
};
private async Task<bool> AllDownloadsFinished() => (await StartNewChapterDownloadsWorker.GetMissingChapters(DbContext, CancellationToken)).Count == 0;
private void ProcessImage(string imagePath) private void ProcessImage(string imagePath)
{ {
@@ -232,5 +247,5 @@ public class DownloadChapterFromMangaconnectorWorker(MangaConnectorId<Chapter> c
return true; return true;
} }
public override string ToString() => $"{base.ToString()} {MangaConnectorIdId}"; public override string ToString() => $"{base.ToString()} {_mangaConnectorIdId}";
} }

View File

@@ -18,10 +18,7 @@ public class StartNewChapterDownloadsWorker(TimeSpan? interval = null, IEnumerab
Log.Debug("Checking for missing chapters..."); Log.Debug("Checking for missing chapters...");
// Get missing chapters // Get missing chapters
List<MangaConnectorId<Chapter>> missingChapters = await DbContext.MangaConnectorToChapter List<MangaConnectorId<Chapter>> missingChapters = await GetMissingChapters(DbContext, CancellationToken);
.Include(id => id.Obj)
.Where(id => id.Obj.Downloaded == false && id.UseForDownload)
.ToListAsync(CancellationToken);
Log.Debug($"Found {missingChapters.Count} missing downloads."); Log.Debug($"Found {missingChapters.Count} missing downloads.");
@@ -37,4 +34,9 @@ public class StartNewChapterDownloadsWorker(TimeSpan? interval = null, IEnumerab
return newWorkers.ToArray(); return newWorkers.ToArray();
} }
internal static async Task<List<MangaConnectorId<Chapter>>> GetMissingChapters(MangaContext ctx, CancellationToken cancellationToken) => await ctx.MangaConnectorToChapter
.Include(id => id.Obj)
.Where(id => id.Obj.Downloaded == false && id.UseForDownload)
.ToListAsync(cancellationToken);
} }

View File

@@ -0,0 +1,44 @@
using API.Schema.LibraryContext;
using API.Schema.LibraryContext.LibraryConnectors;
using Microsoft.EntityFrameworkCore;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
namespace API.Workers;
public class RefreshLibrariesWorker(IEnumerable<BaseWorker>? dependsOn = null) : BaseWorkerWithContext<LibraryContext>(dependsOn)
{
public static DateTime LastRefresh { get; set; } = DateTime.UnixEpoch;
protected override async Task<BaseWorker[]> DoWorkInternal()
{
Log.Debug("Refreshing libraries...");
LastRefresh = DateTime.UtcNow;
List<LibraryConnector> libraries = await DbContext.LibraryConnectors.ToListAsync(CancellationToken);
foreach (LibraryConnector connector in libraries)
await connector.UpdateLibrary(CancellationToken);
Log.Debug("Libraries Refreshed...");
return [];
}
}
[JsonConverter(typeof(StringEnumConverter))]
public enum LibraryRefreshSetting : byte
{
/// <summary>
/// Refresh Libraries after all Manga are downloaded
/// </summary>
AfterAllFinished = 0,
/// <summary>
/// Refresh Libraries after a Manga is downloaded
/// </summary>
AfterMangaFinished = 1,
/// <summary>
/// Refresh Libraries after every download
/// </summary>
AfterEveryChapter = 2,
/// <summary>
/// Refresh Libraries while downloading chapters, every x minutes
/// </summary>
WhileDownloading = 3
}

View File

@@ -8,6 +8,7 @@
<s:Boolean x:Key="/Default/UserDictionary/Words/=kitsu/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/UserDictionary/Words/=kitsu/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Komga/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/UserDictionary/Words/=Komga/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=lunasea/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/UserDictionary/Words/=lunasea/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=mangaconnector/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=mangakatana/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/UserDictionary/Words/=mangakatana/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Manganato/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/UserDictionary/Words/=Manganato/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Mangasee/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/UserDictionary/Words/=Mangasee/@EntryIndexedValue">True</s:Boolean>
@@ -15,4 +16,5 @@
<s:Boolean x:Key="/Default/UserDictionary/Words/=Ntfy/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/UserDictionary/Words/=Ntfy/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=solverr/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/UserDictionary/Words/=solverr/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Taskmanager/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/UserDictionary/Words/=Taskmanager/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Tranga/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary> <s:Boolean x:Key="/Default/UserDictionary/Words/=Tranga/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=trangatemp/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>