mirror of
https://github.com/C9Glax/tranga.git
synced 2025-04-14 04:13:18 +02:00
Logging
This commit is contained in:
parent
4dd31dfe18
commit
91e033a2ec
@ -95,7 +95,7 @@ app.UseSwaggerUI(options =>
|
|||||||
|
|
||||||
app.UseHttpsRedirection();
|
app.UseHttpsRedirection();
|
||||||
|
|
||||||
//app.UseMiddleware<RequestTimeMiddleware>();
|
app.UseMiddleware<RequestTimeMiddleware>();
|
||||||
|
|
||||||
using (var scope = app.Services.CreateScope())
|
using (var scope = app.Services.CreateScope())
|
||||||
{
|
{
|
||||||
@ -140,7 +140,7 @@ using (var scope = app.Services.CreateScope())
|
|||||||
TrangaSettings.Load();
|
TrangaSettings.Load();
|
||||||
Tranga.StartLogger();
|
Tranga.StartLogger();
|
||||||
Tranga.JobStarterThread.Start(app.Services);
|
Tranga.JobStarterThread.Start(app.Services);
|
||||||
Tranga.NotificationSenderThread.Start(app.Services.CreateScope().ServiceProvider.GetService<PgsqlContext>());
|
Tranga.NotificationSenderThread.Start(app.Services);
|
||||||
|
|
||||||
app.UseCors("AllowAll");
|
app.UseCors("AllowAll");
|
||||||
|
|
||||||
|
@ -16,10 +16,14 @@ public class DownloadMangaCoverJob(string mangaId, string? parentJobId = null, I
|
|||||||
{
|
{
|
||||||
Manga? manga = Manga ?? context.Mangas.Find(this.MangaId);
|
Manga? manga = Manga ?? context.Mangas.Find(this.MangaId);
|
||||||
if (manga is null)
|
if (manga is null)
|
||||||
|
{
|
||||||
|
Log.Error($"Manga {this.MangaId} not found.");
|
||||||
return [];
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
manga.CoverFileNameInCache = manga.SaveCoverImageToCache();
|
manga.CoverFileNameInCache = manga.SaveCoverImageToCache();
|
||||||
context.SaveChanges();
|
context.SaveChanges();
|
||||||
|
Log.Info($"Saved cover for Manga {this.MangaId} to cache at {manga.CoverFileNameInCache}.");
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -24,48 +24,80 @@ public class DownloadSingleChapterJob(string chapterId, string? parentJobId = nu
|
|||||||
|
|
||||||
protected override IEnumerable<Job> RunInternal(PgsqlContext context)
|
protected override IEnumerable<Job> RunInternal(PgsqlContext context)
|
||||||
{
|
{
|
||||||
Chapter chapter = Chapter ?? context.Chapters.Find(ChapterId)!;
|
Chapter? chapter = Chapter ?? context.Chapters.Find(ChapterId);
|
||||||
Manga manga = chapter.ParentManga ?? context.Mangas.Find(chapter.ParentMangaId)!;
|
if (chapter is null)
|
||||||
MangaConnector connector = manga.MangaConnector ?? context.MangaConnectors.Find(manga.MangaConnectorId)!;
|
{
|
||||||
|
Log.Error("Chapter is null.");
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
Manga? manga = chapter.ParentManga ?? context.Mangas.Find(chapter.ParentMangaId);
|
||||||
|
if (manga is null)
|
||||||
|
{
|
||||||
|
Log.Error("Manga is null.");
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
MangaConnector? connector = manga.MangaConnector ?? context.MangaConnectors.Find(manga.MangaConnectorId);
|
||||||
|
if (connector is null)
|
||||||
|
{
|
||||||
|
Log.Error("Connector is null.");
|
||||||
|
return [];
|
||||||
|
}
|
||||||
string[] imageUrls = connector.GetChapterImageUrls(chapter);
|
string[] imageUrls = connector.GetChapterImageUrls(chapter);
|
||||||
string saveArchiveFilePath = chapter.FullArchiveFilePath;
|
if (imageUrls.Length < 1)
|
||||||
|
{
|
||||||
|
Log.Info($"No imageUrls for chapter {chapterId}");
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
string? saveArchiveFilePath = chapter.FullArchiveFilePath;
|
||||||
|
if (saveArchiveFilePath is null)
|
||||||
|
{
|
||||||
|
Log.Error("saveArchiveFilePath is null.");
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
//Check if Publication Directory already exists
|
//Check if Publication Directory already exists
|
||||||
string directoryPath = Path.GetDirectoryName(saveArchiveFilePath)!;
|
string directoryPath = Path.GetDirectoryName(saveArchiveFilePath)!;
|
||||||
if (!Directory.Exists(directoryPath))
|
if (!Directory.Exists(directoryPath))
|
||||||
|
{
|
||||||
|
Log.Info($"Creating publication Directory: {directoryPath}");
|
||||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
||||||
Directory.CreateDirectory(directoryPath,
|
Directory.CreateDirectory(directoryPath,
|
||||||
UserRead | UserWrite | UserExecute | GroupRead | GroupWrite | GroupExecute );
|
UserRead | UserWrite | UserExecute | GroupRead | GroupWrite | GroupExecute );
|
||||||
else
|
else
|
||||||
Directory.CreateDirectory(directoryPath);
|
Directory.CreateDirectory(directoryPath);
|
||||||
|
}
|
||||||
|
|
||||||
if (File.Exists(saveArchiveFilePath)) //Don't download twice. Redownload
|
if (File.Exists(saveArchiveFilePath)) //Don't download twice. Redownload
|
||||||
|
{
|
||||||
|
Log.Info($"Archive {saveArchiveFilePath} already existed, but deleting and re-downloading.");
|
||||||
File.Delete(saveArchiveFilePath);
|
File.Delete(saveArchiveFilePath);
|
||||||
|
}
|
||||||
|
|
||||||
//Create a temporary folder to store images
|
//Create a temporary folder to store images
|
||||||
string tempFolder = Directory.CreateTempSubdirectory("trangatemp").FullName;
|
string tempFolder = Directory.CreateTempSubdirectory("trangatemp").FullName;
|
||||||
|
Log.Debug($"Created temp folder: {tempFolder}");
|
||||||
|
|
||||||
|
Log.Info($"Downloading images: {ChapterId}");
|
||||||
int chapterNum = 0;
|
int chapterNum = 0;
|
||||||
//Download all Images to temporary Folder
|
//Download all Images to temporary Folder
|
||||||
if (imageUrls.Length == 0)
|
|
||||||
{
|
|
||||||
Directory.Delete(tempFolder, true);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (string imageUrl in imageUrls)
|
foreach (string imageUrl in imageUrls)
|
||||||
{
|
{
|
||||||
string extension = imageUrl.Split('.')[^1].Split('?')[0];
|
string extension = imageUrl.Split('.')[^1].Split('?')[0];
|
||||||
string imagePath = Path.Join(tempFolder, $"{chapterNum++}.{extension}");
|
string imagePath = Path.Join(tempFolder, $"{chapterNum++}.{extension}");
|
||||||
bool status = DownloadImage(imageUrl, imagePath);
|
bool status = DownloadImage(imageUrl, imagePath);
|
||||||
if (status is false)
|
if (status is false)
|
||||||
|
{
|
||||||
|
Log.Error($"Failed to download image: {imageUrl}");
|
||||||
return [];
|
return [];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
CopyCoverFromCacheToDownloadLocation(manga);
|
CopyCoverFromCacheToDownloadLocation(manga);
|
||||||
|
|
||||||
|
Log.Debug($"Creating ComicInfo.xml {ChapterId}");
|
||||||
File.WriteAllText(Path.Join(tempFolder, "ComicInfo.xml"), chapter.GetComicInfoXmlString());
|
File.WriteAllText(Path.Join(tempFolder, "ComicInfo.xml"), chapter.GetComicInfoXmlString());
|
||||||
|
|
||||||
|
Log.Debug($"Packaging images to archive {ChapterId}");
|
||||||
//ZIP-it and ship-it
|
//ZIP-it and ship-it
|
||||||
ZipFile.CreateFromDirectory(tempFolder, saveArchiveFilePath);
|
ZipFile.CreateFromDirectory(tempFolder, saveArchiveFilePath);
|
||||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
||||||
@ -81,7 +113,13 @@ public class DownloadSingleChapterJob(string chapterId, string? parentJobId = nu
|
|||||||
private void ProcessImage(string imagePath)
|
private void ProcessImage(string imagePath)
|
||||||
{
|
{
|
||||||
if (!TrangaSettings.bwImages && TrangaSettings.compression == 100)
|
if (!TrangaSettings.bwImages && TrangaSettings.compression == 100)
|
||||||
|
{
|
||||||
|
Log.Debug($"No processing requested for image");
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.Debug($"Processing image: {imagePath}");
|
||||||
|
|
||||||
using Image image = Image.Load(imagePath);
|
using Image image = Image.Load(imagePath);
|
||||||
File.Delete(imagePath);
|
File.Delete(imagePath);
|
||||||
if(TrangaSettings.bwImages)
|
if(TrangaSettings.bwImages)
|
||||||
@ -99,17 +137,23 @@ public class DownloadSingleChapterJob(string chapterId, string? parentJobId = nu
|
|||||||
DirectoryInfo dirInfo = new (publicationFolder);
|
DirectoryInfo dirInfo = new (publicationFolder);
|
||||||
if (dirInfo.EnumerateFiles().Any(info => info.Name.Contains("cover", StringComparison.InvariantCultureIgnoreCase)))
|
if (dirInfo.EnumerateFiles().Any(info => info.Name.Contains("cover", StringComparison.InvariantCultureIgnoreCase)))
|
||||||
{
|
{
|
||||||
|
Log.Debug($"Cover already exists at {publicationFolder}");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Log.Info($"Copying cover to {publicationFolder}");
|
||||||
string? fileInCache = manga.CoverFileNameInCache ?? manga.SaveCoverImageToCache();
|
string? fileInCache = manga.CoverFileNameInCache ?? manga.SaveCoverImageToCache();
|
||||||
if (fileInCache is null)
|
if (fileInCache is null)
|
||||||
|
{
|
||||||
|
Log.Error($"File {fileInCache} does not exist");
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
string newFilePath = Path.Join(publicationFolder, $"cover.{Path.GetFileName(fileInCache).Split('.')[^1]}" );
|
string newFilePath = Path.Join(publicationFolder, $"cover.{Path.GetFileName(fileInCache).Split('.')[^1]}" );
|
||||||
File.Copy(fileInCache, newFilePath, true);
|
File.Copy(fileInCache, newFilePath, true);
|
||||||
if(RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
if(RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
||||||
File.SetUnixFileMode(newFilePath, GroupRead | GroupWrite | UserRead | UserWrite);
|
File.SetUnixFileMode(newFilePath, GroupRead | GroupWrite | UserRead | UserWrite);
|
||||||
|
Log.Debug($"Copied cover from {fileInCache} to {newFilePath}");
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool DownloadImage(string imageUrl, string savePath)
|
private bool DownloadImage(string imageUrl, string savePath)
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.ComponentModel.DataAnnotations.Schema;
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
using log4net;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
@ -34,6 +35,10 @@ public abstract class Job
|
|||||||
public JobState state { get; internal set; } = JobState.Waiting;
|
public JobState state { get; internal set; } = JobState.Waiting;
|
||||||
[Required]
|
[Required]
|
||||||
public bool Enabled { get; internal set; } = true;
|
public bool Enabled { get; internal set; } = true;
|
||||||
|
|
||||||
|
[NotMapped]
|
||||||
|
[JsonIgnore]
|
||||||
|
protected ILog Log { get; init; }
|
||||||
|
|
||||||
public Job(string jobId, JobType jobType, ulong recurrenceMs, Job? parentJob = null, ICollection<Job>? dependsOnJobs = null)
|
public Job(string jobId, JobType jobType, ulong recurrenceMs, Job? parentJob = null, ICollection<Job>? dependsOnJobs = null)
|
||||||
: this(jobId, jobType, recurrenceMs, parentJob?.JobId, dependsOnJobs?.Select(j => j.JobId).ToList())
|
: this(jobId, jobType, recurrenceMs, parentJob?.JobId, dependsOnJobs?.Select(j => j.JobId).ToList())
|
||||||
@ -44,6 +49,7 @@ public abstract class Job
|
|||||||
|
|
||||||
public Job(string jobId, JobType jobType, ulong recurrenceMs, string? parentJobId = null, ICollection<string>? dependsOnJobsIds = null)
|
public Job(string jobId, JobType jobType, ulong recurrenceMs, string? parentJobId = null, ICollection<string>? dependsOnJobsIds = null)
|
||||||
{
|
{
|
||||||
|
Log = LogManager.GetLogger(GetType());
|
||||||
JobId = jobId;
|
JobId = jobId;
|
||||||
ParentJobId = parentJobId;
|
ParentJobId = parentJobId;
|
||||||
DependsOnJobsIds = dependsOnJobsIds;
|
DependsOnJobsIds = dependsOnJobsIds;
|
||||||
@ -53,16 +59,27 @@ public abstract class Job
|
|||||||
|
|
||||||
public IEnumerable<Job> Run(IServiceProvider serviceProvider)
|
public IEnumerable<Job> Run(IServiceProvider serviceProvider)
|
||||||
{
|
{
|
||||||
|
Log.Debug($"Running job {JobId}");
|
||||||
using IServiceScope scope = serviceProvider.CreateScope();
|
using IServiceScope scope = serviceProvider.CreateScope();
|
||||||
PgsqlContext context = scope.ServiceProvider.GetRequiredService<PgsqlContext>();
|
PgsqlContext context = scope.ServiceProvider.GetRequiredService<PgsqlContext>();
|
||||||
|
|
||||||
this.state = JobState.Running;
|
try
|
||||||
context.SaveChanges();
|
{
|
||||||
Job[] newJobs = RunInternal(context).ToArray();
|
this.state = JobState.Running;
|
||||||
this.state = JobState.Completed;
|
context.SaveChanges();
|
||||||
context.Jobs.AddRange(newJobs);
|
Job[] newJobs = RunInternal(context).ToArray();
|
||||||
context.SaveChanges();
|
this.state = JobState.Completed;
|
||||||
return newJobs;
|
context.Jobs.AddRange(newJobs);
|
||||||
|
context.SaveChanges();
|
||||||
|
Log.Info($"Job {JobId} completed. Generated {newJobs.Length} new jobs.");
|
||||||
|
return newJobs;
|
||||||
|
}
|
||||||
|
catch (DbUpdateException e)
|
||||||
|
{
|
||||||
|
this.state = JobState.Failed;
|
||||||
|
Log.Error($"Failed to run job {JobId}", e);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected abstract IEnumerable<Job> RunInternal(PgsqlContext context);
|
protected abstract IEnumerable<Job> RunInternal(PgsqlContext context);
|
||||||
|
@ -16,11 +16,18 @@ public class MoveFileOrFolderJob(string fromLocation, string toLocation, string?
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
FileInfo fi = new FileInfo(FromLocation);
|
FileInfo fi = new (FromLocation);
|
||||||
if (!fi.Exists)
|
if (!fi.Exists)
|
||||||
|
{
|
||||||
|
Log.Error($"File does not exist at {FromLocation}");
|
||||||
return [];
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
if (File.Exists(ToLocation))//Do not override existing
|
if (File.Exists(ToLocation))//Do not override existing
|
||||||
|
{
|
||||||
|
Log.Error($"File already exists at {ToLocation}");
|
||||||
return [];
|
return [];
|
||||||
|
}
|
||||||
if(fi.Attributes.HasFlag(FileAttributes.Directory))
|
if(fi.Attributes.HasFlag(FileAttributes.Directory))
|
||||||
MoveDirectory(fi, ToLocation);
|
MoveDirectory(fi, ToLocation);
|
||||||
else
|
else
|
||||||
@ -28,7 +35,7 @@ public class MoveFileOrFolderJob(string fromLocation, string toLocation, string?
|
|||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
|
Log.Error(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
return [];
|
return [];
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
namespace API.Schema.Jobs;
|
namespace API.Schema.Jobs;
|
||||||
|
|
||||||
@ -15,15 +16,29 @@ public class MoveMangaLibraryJob(string mangaId, string toLibraryId, string? par
|
|||||||
protected override IEnumerable<Job> RunInternal(PgsqlContext context)
|
protected override IEnumerable<Job> RunInternal(PgsqlContext context)
|
||||||
{
|
{
|
||||||
Manga? manga = context.Mangas.Find(MangaId);
|
Manga? manga = context.Mangas.Find(MangaId);
|
||||||
if(manga is null)
|
if (manga is null)
|
||||||
throw new KeyNotFoundException();
|
{
|
||||||
|
Log.Error("Manga not found");
|
||||||
|
return [];
|
||||||
|
}
|
||||||
LocalLibrary? library = context.LocalLibraries.Find(ToLibraryId);
|
LocalLibrary? library = context.LocalLibraries.Find(ToLibraryId);
|
||||||
if(library is null)
|
if (library is null)
|
||||||
throw new KeyNotFoundException();
|
{
|
||||||
|
Log.Error("LocalLibrary not found");
|
||||||
|
return [];
|
||||||
|
}
|
||||||
Chapter[] chapters = context.Chapters.Where(c => c.ParentMangaId == MangaId).ToArray();
|
Chapter[] chapters = context.Chapters.Where(c => c.ParentMangaId == MangaId).ToArray();
|
||||||
Dictionary<Chapter, string> oldPath = chapters.ToDictionary(c => c, c => c.FullArchiveFilePath!);
|
Dictionary<Chapter, string> oldPath = chapters.ToDictionary(c => c, c => c.FullArchiveFilePath!);
|
||||||
manga.Library = library;
|
manga.Library = library;
|
||||||
context.SaveChanges();
|
try
|
||||||
|
{
|
||||||
|
context.SaveChanges();
|
||||||
|
}
|
||||||
|
catch (DbUpdateException e)
|
||||||
|
{
|
||||||
|
Log.Error(e);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
return chapters.Select(c => new MoveFileOrFolderJob(oldPath[c], c.FullArchiveFilePath!));
|
return chapters.Select(c => new MoveFileOrFolderJob(oldPath[c], c.FullArchiveFilePath!));
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using API.Schema.MangaConnectors;
|
using API.Schema.MangaConnectors;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
namespace API.Schema.Jobs;
|
namespace API.Schema.Jobs;
|
||||||
@ -16,21 +17,35 @@ public class RetrieveChaptersJob(ulong recurrenceMs, string mangaId, string? par
|
|||||||
|
|
||||||
protected override IEnumerable<Job> RunInternal(PgsqlContext context)
|
protected override IEnumerable<Job> RunInternal(PgsqlContext context)
|
||||||
{
|
{
|
||||||
/*
|
Manga? manga = Manga ?? context.Mangas.Find(MangaId);
|
||||||
* For some reason, directly using Manga from above instead of finding it again causes DBContext to consider
|
if (manga is null)
|
||||||
* Manga as a new entity and Postgres throws a Duplicate PK exception.
|
{
|
||||||
* m.MangaConnector does not have this issue (IDK why).
|
Log.Error("Manga is null.");
|
||||||
*/
|
return [];
|
||||||
Manga m = context.Mangas.Find(MangaId)!;
|
}
|
||||||
MangaConnector connector = context.MangaConnectors.Find(m.MangaConnectorId)!;
|
MangaConnector? connector = manga.MangaConnector ?? context.MangaConnectors.Find(manga.MangaConnectorId);
|
||||||
|
if (connector is null)
|
||||||
|
{
|
||||||
|
Log.Error("Connector is null.");
|
||||||
|
return [];
|
||||||
|
}
|
||||||
// This gets all chapters that are not downloaded
|
// This gets all chapters that are not downloaded
|
||||||
Chapter[] allNewChapters = connector.GetNewChapters(m).DistinctBy(c => c.ChapterId).ToArray();
|
Chapter[] allNewChapters = connector.GetNewChapters(manga).DistinctBy(c => c.ChapterId).ToArray();
|
||||||
|
Log.Info($"{allNewChapters.Length} new chapters.");
|
||||||
// This filters out chapters that are not downloaded but already exist in the DB
|
|
||||||
string[] chapterIds = context.Chapters.Where(chapter => chapter.ParentMangaId == m.MangaId).Select(chapter => chapter.ChapterId).ToArray();
|
try
|
||||||
Chapter[] newChapters = allNewChapters.Where(chapter => !chapterIds.Contains(chapter.ChapterId)).ToArray();
|
{
|
||||||
context.Chapters.AddRange(newChapters);
|
// This filters out chapters that are not downloaded but already exist in the DB
|
||||||
context.SaveChanges();
|
string[] chapterIds = context.Chapters.Where(chapter => chapter.ParentMangaId == manga.MangaId)
|
||||||
|
.Select(chapter => chapter.ChapterId).ToArray();
|
||||||
|
Chapter[] newChapters = allNewChapters.Where(chapter => !chapterIds.Contains(chapter.ChapterId)).ToArray();
|
||||||
|
context.Chapters.AddRange(newChapters);
|
||||||
|
context.SaveChanges();
|
||||||
|
}
|
||||||
|
catch (DbUpdateException e)
|
||||||
|
{
|
||||||
|
Log.Error(e);
|
||||||
|
}
|
||||||
|
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
@ -21,6 +21,7 @@ public class UpdateMetadataJob(ulong recurrenceMs, string mangaId, string? paren
|
|||||||
/// <param name="context"></param>
|
/// <param name="context"></param>
|
||||||
protected override IEnumerable<Job> RunInternal(PgsqlContext context)
|
protected override IEnumerable<Job> RunInternal(PgsqlContext context)
|
||||||
{
|
{
|
||||||
|
Log.Warn("NOT IMPLEMENTED.");
|
||||||
return [];//TODO
|
return [];//TODO
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,6 +1,7 @@
|
|||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using API.MangaDownloadClients;
|
using API.MangaDownloadClients;
|
||||||
using HtmlAgilityPack;
|
using HtmlAgilityPack;
|
||||||
|
using log4net;
|
||||||
|
|
||||||
namespace API.Schema.MangaConnectors;
|
namespace API.Schema.MangaConnectors;
|
||||||
|
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
using System.ComponentModel.DataAnnotations.Schema;
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using API.MangaDownloadClients;
|
using API.MangaDownloadClients;
|
||||||
|
using log4net;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
@ -36,6 +37,10 @@ public abstract class MangaConnector(string name, string[] supportedLanguages, s
|
|||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
[NotMapped]
|
[NotMapped]
|
||||||
internal DownloadClient downloadClient { get; init; } = null!;
|
internal DownloadClient downloadClient { get; init; } = null!;
|
||||||
|
|
||||||
|
[JsonIgnore]
|
||||||
|
[NotMapped]
|
||||||
|
protected ILog Log { get; init; } = LogManager.GetLogger(name);
|
||||||
|
|
||||||
public Chapter[] GetNewChapters(Manga manga)
|
public Chapter[] GetNewChapters(Manga manga)
|
||||||
{
|
{
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.ComponentModel.DataAnnotations.Schema;
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using log4net;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
@ -35,10 +36,15 @@ public class NotificationConnector(string name, string url, Dictionary<string, s
|
|||||||
{
|
{
|
||||||
DefaultRequestHeaders = { { "User-Agent", TrangaSettings.userAgent } }
|
DefaultRequestHeaders = { { "User-Agent", TrangaSettings.userAgent } }
|
||||||
};
|
};
|
||||||
|
|
||||||
|
[JsonIgnore]
|
||||||
|
[NotMapped]
|
||||||
|
protected ILog Log = LogManager.GetLogger(name);
|
||||||
|
|
||||||
public void SendNotification(string title, string notificationText)
|
public void SendNotification(string title, string notificationText)
|
||||||
{
|
{
|
||||||
CustomWebhookFormatProvider formatProvider = new CustomWebhookFormatProvider(title, notificationText);
|
Log.Info($"Sending notification: {title} - {notificationText}");
|
||||||
|
CustomWebhookFormatProvider formatProvider = new (title, notificationText);
|
||||||
string formattedUrl = string.Format(formatProvider, Url);
|
string formattedUrl = string.Format(formatProvider, Url);
|
||||||
string formattedBody = string.Format(formatProvider, Body, title, notificationText);
|
string formattedBody = string.Format(formatProvider, Body, title, notificationText);
|
||||||
Dictionary<string, string> formattedHeaders = Headers.ToDictionary(h => h.Key,
|
Dictionary<string, string> formattedHeaders = Headers.ToDictionary(h => h.Key,
|
||||||
@ -48,8 +54,10 @@ public class NotificationConnector(string name, string url, Dictionary<string, s
|
|||||||
foreach (var (key, value) in formattedHeaders)
|
foreach (var (key, value) in formattedHeaders)
|
||||||
request.Headers.Add(key, value);
|
request.Headers.Add(key, value);
|
||||||
request.Content = new StringContent(formattedBody);
|
request.Content = new StringContent(formattedBody);
|
||||||
|
Log.Debug($"Request: {request}");
|
||||||
|
|
||||||
HttpResponseMessage response = Client.Send(request);
|
HttpResponseMessage response = Client.Send(request);
|
||||||
|
Log.Debug($"Response status code: {response.StatusCode}");
|
||||||
}
|
}
|
||||||
|
|
||||||
private class CustomWebhookFormatProvider(string title, string text) : IFormatProvider
|
private class CustomWebhookFormatProvider(string title, string text) : IFormatProvider
|
||||||
|
121
API/Tranga.cs
121
API/Tranga.cs
@ -12,66 +12,115 @@ public static class Tranga
|
|||||||
{
|
{
|
||||||
public static Thread NotificationSenderThread { get; } = new (NotificationSender);
|
public static Thread NotificationSenderThread { get; } = new (NotificationSender);
|
||||||
public static Thread JobStarterThread { get; } = new (JobStarter);
|
public static Thread JobStarterThread { get; } = new (JobStarter);
|
||||||
private static readonly Dictionary<Thread, Job> RunningJobs = new();
|
|
||||||
private static readonly ILog Log = LogManager.GetLogger(typeof(Tranga));
|
private static readonly ILog Log = LogManager.GetLogger(typeof(Tranga));
|
||||||
|
|
||||||
internal static void StartLogger()
|
internal static void StartLogger()
|
||||||
{
|
{
|
||||||
BasicConfigurator.Configure();
|
BasicConfigurator.Configure();
|
||||||
|
Log.Info("Logger Configured.");
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void NotificationSender(object? pgsqlContext)
|
private static void NotificationSender(object? serviceProviderObj)
|
||||||
{
|
{
|
||||||
if(pgsqlContext is null) return;
|
if (serviceProviderObj is null)
|
||||||
PgsqlContext context = (PgsqlContext)pgsqlContext;
|
{
|
||||||
|
Log.Error("serviceProviderObj is null");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
IServiceProvider serviceProvider = (IServiceProvider)serviceProviderObj!;
|
||||||
|
using IServiceScope scope = serviceProvider.CreateScope();
|
||||||
|
PgsqlContext? context = scope.ServiceProvider.GetService<PgsqlContext>();
|
||||||
|
if (context is null)
|
||||||
|
{
|
||||||
|
Log.Error("PgsqlContext is null");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
IQueryable<Notification> staleNotifications = context.Notifications.Where(n => n.Urgency < NotificationUrgency.Normal);
|
try
|
||||||
context.Notifications.RemoveRange(staleNotifications);
|
{
|
||||||
context.SaveChanges();
|
//Removing Notifications from previous runs
|
||||||
|
IQueryable<Notification> staleNotifications =
|
||||||
|
context.Notifications.Where(n => n.Urgency < NotificationUrgency.Normal);
|
||||||
|
context.Notifications.RemoveRange(staleNotifications);
|
||||||
|
context.SaveChanges();
|
||||||
|
}
|
||||||
|
catch (DbUpdateException e)
|
||||||
|
{
|
||||||
|
Log.Error("Error removing stale notifications.", e);
|
||||||
|
}
|
||||||
|
|
||||||
while (true)
|
while (true)
|
||||||
{
|
{
|
||||||
SendNotifications(context, NotificationUrgency.High);
|
SendNotifications(serviceProvider, NotificationUrgency.High);
|
||||||
SendNotifications(context, NotificationUrgency.Normal);
|
SendNotifications(serviceProvider, NotificationUrgency.Normal);
|
||||||
SendNotifications(context, NotificationUrgency.Low);
|
SendNotifications(serviceProvider, NotificationUrgency.Low);
|
||||||
|
|
||||||
context.SaveChanges();
|
|
||||||
Thread.Sleep(2000);
|
Thread.Sleep(2000);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void SendNotifications(PgsqlContext context, NotificationUrgency urgency)
|
private static void SendNotifications(IServiceProvider serviceProvider, NotificationUrgency urgency)
|
||||||
{
|
{
|
||||||
List<Notification> notifications = context.Notifications.Where(n => n.Urgency == urgency).ToList();
|
Log.Info($"Sending notifications for {urgency}");
|
||||||
if (notifications.Any())
|
using IServiceScope scope = serviceProvider.CreateScope();
|
||||||
|
PgsqlContext? context = scope.ServiceProvider.GetService<PgsqlContext>();
|
||||||
|
if (context is null)
|
||||||
{
|
{
|
||||||
DateTime max = notifications.MaxBy(n => n.Date)!.Date;
|
Log.Error("PgsqlContext is null");
|
||||||
if (DateTime.UtcNow.Subtract(max) > TrangaSettings.NotificationUrgencyDelay(urgency))
|
return;
|
||||||
{
|
}
|
||||||
foreach (NotificationConnector notificationConnector in context.NotificationConnectors)
|
|
||||||
{
|
List<Notification> notifications = context.Notifications.Where(n => n.Urgency == urgency).ToList();
|
||||||
foreach (Notification notification in notifications)
|
if (!notifications.Any())
|
||||||
notificationConnector.SendNotification(notification.Title, notification.Message);
|
return;
|
||||||
}
|
|
||||||
context.Notifications.RemoveRange(notifications);
|
try
|
||||||
}
|
{
|
||||||
|
foreach (NotificationConnector notificationConnector in context.NotificationConnectors)
|
||||||
|
{
|
||||||
|
foreach (Notification notification in notifications)
|
||||||
|
notificationConnector.SendNotification(notification.Title, notification.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
context.Notifications.RemoveRange(notifications);
|
||||||
|
context.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
catch (DbUpdateException e)
|
||||||
|
{
|
||||||
|
Log.Error("Error sending notifications.", e);
|
||||||
}
|
}
|
||||||
context.SaveChanges();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private const string TRANGA =
|
||||||
|
"\n\n" +
|
||||||
|
" _______ \n" +
|
||||||
|
"|_ _|.----..---.-..-----..-----..---.-.\n" +
|
||||||
|
" | | | _|| _ || || _ || _ |\n" +
|
||||||
|
" |___| |__| |___._||__|__||___ ||___._|\n" +
|
||||||
|
" |_____| \n\n";
|
||||||
|
private static readonly Dictionary<Thread, Job> RunningJobs = new();
|
||||||
private static void JobStarter(object? serviceProviderObj)
|
private static void JobStarter(object? serviceProviderObj)
|
||||||
{
|
{
|
||||||
if(serviceProviderObj is null) return;
|
if (serviceProviderObj is null)
|
||||||
|
{
|
||||||
|
Log.Error("serviceProviderObj is null");
|
||||||
|
return;
|
||||||
|
}
|
||||||
IServiceProvider serviceProvider = (IServiceProvider)serviceProviderObj;
|
IServiceProvider serviceProvider = (IServiceProvider)serviceProviderObj;
|
||||||
using IServiceScope scope = serviceProvider.CreateScope();
|
using IServiceScope scope = serviceProvider.CreateScope();
|
||||||
PgsqlContext? context = scope.ServiceProvider.GetService<PgsqlContext>();
|
PgsqlContext? context = scope.ServiceProvider.GetService<PgsqlContext>();
|
||||||
if (context is null) return;
|
if (context is null)
|
||||||
|
{
|
||||||
|
Log.Error("PgsqlContext is null");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
string TRANGA =
|
|
||||||
"\n\n _______ \n|_ _|.----..---.-..-----..-----..---.-.\n | | | _|| _ || || _ || _ |\n |___| |__| |___._||__|__||___ ||___._|\n |_____| \n\n";
|
|
||||||
Log.Info(TRANGA);
|
Log.Info(TRANGA);
|
||||||
|
Log.Info("JobStarter Thread running.");
|
||||||
while (true)
|
while (true)
|
||||||
{
|
{
|
||||||
List<Job> completedJobs = context.Jobs.Where(j => j.state >= JobState.Completed).ToList();
|
List<Job> completedJobs = context.Jobs.Where(j => j.state >= JobState.Completed).ToList();
|
||||||
|
Log.Debug($"Completed jobs: {completedJobs.Count}");
|
||||||
foreach (Job job in completedJobs)
|
foreach (Job job in completedJobs)
|
||||||
if (job.RecurrenceMs <= 0)
|
if (job.RecurrenceMs <= 0)
|
||||||
context.Jobs.Remove(job);
|
context.Jobs.Remove(job);
|
||||||
@ -82,16 +131,20 @@ public static class Tranga
|
|||||||
else
|
else
|
||||||
job.state = JobState.Waiting;
|
job.state = JobState.Waiting;
|
||||||
job.LastExecution = DateTime.UtcNow;
|
job.LastExecution = DateTime.UtcNow;
|
||||||
context.Jobs.Update(job);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Job> runJobs = context.Jobs.Where(j => j.state <= JobState.Running && j.Enabled == true).ToList()
|
List<Job> runJobs = context.Jobs.Where(j => j.state <= JobState.Running && j.Enabled == true).ToList()
|
||||||
.Where(j => j.NextExecution < DateTime.UtcNow).ToList();
|
.Where(j => j.NextExecution < DateTime.UtcNow).ToList();
|
||||||
foreach (Job job in OrderJobs(runJobs, context))
|
Log.Debug($"Due jobs: {runJobs.Count}");
|
||||||
|
Log.Debug($"Running jobs: {RunningJobs.Count}");
|
||||||
|
IEnumerable<Job> orderedJobs = OrderJobs(runJobs, context).ToList();
|
||||||
|
Log.Debug($"Ordered jobs: {orderedJobs.Count()}");
|
||||||
|
foreach (Job job in orderedJobs)
|
||||||
{
|
{
|
||||||
// If the job is already running, skip it
|
// If the job is already running, skip it
|
||||||
if (RunningJobs.Values.Any(j => j.JobId == job.JobId)) continue;
|
if (RunningJobs.Values.Any(j => j.JobId == job.JobId)) continue;
|
||||||
|
|
||||||
|
//If a Job for that connector is already running, skip it
|
||||||
if (job is DownloadAvailableChaptersJob dncj)
|
if (job is DownloadAvailableChaptersJob dncj)
|
||||||
{
|
{
|
||||||
if (RunningJobs.Values.Any(j =>
|
if (RunningJobs.Values.Any(j =>
|
||||||
@ -113,15 +166,15 @@ public static class Tranga
|
|||||||
|
|
||||||
Thread t = new(() =>
|
Thread t = new(() =>
|
||||||
{
|
{
|
||||||
IEnumerable<Job> newJobs = job.Run(serviceProvider);
|
job.Run(serviceProvider);
|
||||||
});
|
});
|
||||||
RunningJobs.Add(t, job);
|
RunningJobs.Add(t, job);
|
||||||
t.Start();
|
t.Start();
|
||||||
context.Jobs.Update(job);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
(Thread, Job)[] removeFromThreadsList = RunningJobs.Where(t => !t.Key.IsAlive)
|
(Thread, Job)[] removeFromThreadsList = RunningJobs.Where(t => !t.Key.IsAlive)
|
||||||
.Select(t => (t.Key, t.Value)).ToArray();
|
.Select(t => (t.Key, t.Value)).ToArray();
|
||||||
|
Log.Debug($"Remove from Threads List: {removeFromThreadsList.Length}");
|
||||||
foreach ((Thread thread, Job job) thread in removeFromThreadsList)
|
foreach ((Thread thread, Job job) thread in removeFromThreadsList)
|
||||||
{
|
{
|
||||||
RunningJobs.Remove(thread.thread);
|
RunningJobs.Remove(thread.thread);
|
||||||
@ -135,7 +188,7 @@ public static class Tranga
|
|||||||
}
|
}
|
||||||
catch (DbUpdateException e)
|
catch (DbUpdateException e)
|
||||||
{
|
{
|
||||||
|
Log.Error("Failed saving Job changes.", e);
|
||||||
}
|
}
|
||||||
Thread.Sleep(TrangaSettings.startNewJobTimeoutMs);
|
Thread.Sleep(TrangaSettings.startNewJobTimeoutMs);
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user