diff --git a/.gitignore b/.gitignore
index d439ba7..baeb8bd 100644
--- a/.gitignore
+++ b/.gitignore
@@ -20,6 +20,8 @@ riderModule.iml
cover.jpg
cover.png
/.vscode
+/.vs/
+Tranga/Properties/launchSettings.json
/Manga
/settings
*.DotSettings.user
\ No newline at end of file
diff --git a/README.md b/README.md
index 0151452..3a557cd 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
-
Tranga
+
Tranga v2
Automatic Manga and Metadata downloader
@@ -62,7 +62,8 @@ Notifications can be sent to your devices using [Gotify](https://gotify.net/), [
### What this does and doesn't do
Tranga (this git-repo) will open a port (standard 6531) and listen for requests to add Jobs to Monitor and/or download specific Manga.
-The configuration is all done through HTTP-Requests.
+The configuration is all done through HTTP-Requests. [Documentation](docs/API_Calls_v2.md)
+
_**For a web-frontend use [tranga-website](https://github.com/C9Glax/tranga-website).**_
This project downloads the images for a Manga from the specified Scanlation-Website and packages them with some metadata - from that same website - in a .cbz-archive (per chapter).
@@ -90,6 +91,8 @@ That is why I wanted to create my own project, in a language I understand, and t
- [PuppeteerSharp](https://www.puppeteersharp.com/)
- [Html Agility Pack (HAP)](https://html-agility-pack.net/)
- [Soenneker.Utils.String.NeedlemanWunsch](https://github.com/soenneker/soenneker.utils.string.needlemanwunsch)
+- [Sixlabors.ImageSharp](https://docs-v2.sixlabors.com/articles/imagesharp/index.html#license)
+- [zstd-wrapper](https://github.com/oleg-st/ZstdSharp) [zstd](https://github.com/facebook/zstd)
- 💙 Blåhaj 🦈
(back to top)
diff --git a/Tranga/GlobalBase.cs b/Tranga/GlobalBase.cs
index 5466cd1..3825c21 100644
--- a/Tranga/GlobalBase.cs
+++ b/Tranga/GlobalBase.cs
@@ -1,8 +1,11 @@
using System.Globalization;
+using System.Runtime.InteropServices;
+using System.Text;
using System.Text.RegularExpressions;
using Logging;
using Newtonsoft.Json;
using Tranga.LibraryConnectors;
+using Tranga.MangaConnectors;
using Tranga.NotificationConnectors;
namespace Tranga;
@@ -14,6 +17,7 @@ public abstract class GlobalBase
protected HashSet
notificationConnectors { get; init; }
protected HashSet libraryConnectors { get; init; }
private Dictionary cachedPublications { get; init; }
+ protected HashSet _connectors;
public static readonly NumberFormatInfo numberFormatDecimalPoint = new (){ NumberDecimalSeparator = "." };
protected static readonly Regex baseUrlRex = new(@"https?:\/\/[0-9A-z\.-]+(:[0-9]+)?");
@@ -23,6 +27,7 @@ public abstract class GlobalBase
this.notificationConnectors = clone.notificationConnectors;
this.libraryConnectors = clone.libraryConnectors;
this.cachedPublications = clone.cachedPublications;
+ this._connectors = clone._connectors;
}
protected GlobalBase(Logger? logger)
@@ -31,15 +36,7 @@ public abstract class GlobalBase
this.notificationConnectors = TrangaSettings.LoadNotificationConnectors(this);
this.libraryConnectors = TrangaSettings.LoadLibraryConnectors(this);
this.cachedPublications = new();
- }
-
- protected void AddMangaToCache(Manga manga)
- {
- if (!this.cachedPublications.TryAdd(manga.internalId, manga))
- {
- Log($"Overwriting Manga {manga.internalId}");
- this.cachedPublications[manga.internalId] = manga;
- }
+ this._connectors = new();
}
protected Manga? GetCachedManga(string internalId)
@@ -51,9 +48,71 @@ public abstract class GlobalBase
};
}
- protected IEnumerable GetAllCachedManga()
+ protected IEnumerable GetAllCachedManga() => cachedPublications.Values;
+
+ protected void AddMangaToCache(Manga manga)
{
- return cachedPublications.Values;
+ if (!cachedPublications.TryAdd(manga.internalId, manga))
+ {
+ Log($"Overwriting Manga {manga.internalId}");
+ cachedPublications[manga.internalId] = manga;
+ }
+ ExportManga();
+ }
+
+ protected void RemoveMangaFromCache(Manga manga) => RemoveMangaFromCache(manga.internalId);
+
+ protected void RemoveMangaFromCache(string internalId)
+ {
+ cachedPublications.Remove(internalId);
+ ExportManga();
+ }
+
+ internal void ImportManga()
+ {
+ string folder = TrangaSettings.mangaCacheFolderPath;
+ Directory.CreateDirectory(folder);
+
+ foreach (FileInfo fileInfo in new DirectoryInfo(folder).GetFiles())
+ {
+ string content = File.ReadAllText(fileInfo.FullName);
+ try
+ {
+ Manga m = JsonConvert.DeserializeObject(content, new MangaConnectorJsonConverter(this, _connectors));
+ this.cachedPublications.TryAdd(m.internalId, m);
+ }
+ catch (JsonException e)
+ {
+ Log($"Error parsing Manga {fileInfo.Name}:\n{e.Message}");
+ }
+ }
+
+ }
+
+ private static bool ExportRunning = false;
+ private void ExportManga()
+ {
+ while (ExportRunning)
+ Thread.Sleep(1);
+ ExportRunning = true;
+ string folder = TrangaSettings.mangaCacheFolderPath;
+ Directory.CreateDirectory(folder);
+ Manga[] copy = new Manga[cachedPublications.Values.Count];
+ cachedPublications.Values.CopyTo(copy, 0);
+ foreach (Manga manga in copy)
+ {
+ string content = JsonConvert.SerializeObject(manga, Formatting.Indented);
+ string filePath = Path.Combine(folder, $"{manga.internalId}.json");
+ File.WriteAllText(filePath, content, Encoding.UTF8);
+ }
+
+ foreach (FileInfo fileInfo in new DirectoryInfo(folder).GetFiles())
+ {
+ if(!cachedPublications.Keys.Any(key => fileInfo.Name.Substring(0, fileInfo.Name.LastIndexOf('.')).Equals(key)))
+ fileInfo.Delete();
+ }
+
+ ExportRunning = false;
}
protected void Log(string message)
diff --git a/Tranga/Jobs/DownloadChapter.cs b/Tranga/Jobs/DownloadChapter.cs
index 4e8456b..0092f6c 100644
--- a/Tranga/Jobs/DownloadChapter.cs
+++ b/Tranga/Jobs/DownloadChapter.cs
@@ -7,12 +7,12 @@ public class DownloadChapter : Job
{
public Chapter chapter { get; init; }
- public DownloadChapter(GlobalBase clone, MangaConnector connector, Chapter chapter, DateTime lastExecution, string? parentJobId = null) : base(clone, JobType.DownloadChapterJob, connector, lastExecution, parentJobId: parentJobId)
+ public DownloadChapter(GlobalBase clone, Chapter chapter, DateTime lastExecution, string? parentJobId = null) : base(clone, JobType.DownloadChapterJob, lastExecution, parentJobId: parentJobId)
{
this.chapter = chapter;
}
- public DownloadChapter(GlobalBase clone, MangaConnector connector, Chapter chapter, string? parentJobId = null) : base(clone, JobType.DownloadChapterJob, connector, parentJobId: parentJobId)
+ public DownloadChapter(GlobalBase clone, Chapter chapter, string? parentJobId = null) : base(clone, JobType.DownloadChapterJob, parentJobId: parentJobId)
{
this.chapter = chapter;
}
@@ -44,11 +44,15 @@ public class DownloadChapter : Job
return Array.Empty();
}
+ protected override MangaConnector GetMangaConnector()
+ {
+ return chapter.parentManga.mangaConnector;
+ }
+
public override bool Equals(object? obj)
{
if (obj is not DownloadChapter otherJob)
return false;
- return otherJob.mangaConnector == this.mangaConnector &&
- otherJob.chapter.Equals(this.chapter);
+ return otherJob.chapter.Equals(this.chapter);
}
}
\ No newline at end of file
diff --git a/Tranga/Jobs/DownloadNewChapters.cs b/Tranga/Jobs/DownloadNewChapters.cs
index b07d46b..d7edf96 100644
--- a/Tranga/Jobs/DownloadNewChapters.cs
+++ b/Tranga/Jobs/DownloadNewChapters.cs
@@ -1,29 +1,29 @@
-using Tranga.MangaConnectors;
+using Newtonsoft.Json;
+using Tranga.MangaConnectors;
namespace Tranga.Jobs;
public class DownloadNewChapters : Job
{
- public Manga manga { get; set; }
+ public string mangaInternalId { get; set; }
+ [JsonIgnore] private Manga? manga => GetCachedManga(mangaInternalId);
public string translatedLanguage { get; init; }
- public DownloadNewChapters(GlobalBase clone, MangaConnector connector, Manga manga, DateTime lastExecution,
- bool recurring = false, TimeSpan? recurrence = null, string? parentJobId = null, string translatedLanguage = "en") : base(clone, JobType.DownloadNewChaptersJob, connector, lastExecution, recurring,
- recurrence, parentJobId)
+ public DownloadNewChapters(GlobalBase clone, string mangaInternalId, DateTime lastExecution, bool recurring = false, TimeSpan? recurrence = null, string? parentJobId = null, string translatedLanguage = "en") : base(clone, JobType.DownloadNewChaptersJob, lastExecution, recurring, recurrence, parentJobId)
{
- this.manga = manga;
+ this.mangaInternalId = mangaInternalId;
this.translatedLanguage = translatedLanguage;
}
- public DownloadNewChapters(GlobalBase clone, MangaConnector connector, Manga manga, bool recurring = false, TimeSpan? recurrence = null, string? parentJobId = null, string translatedLanguage = "en") : base (clone, JobType.DownloadNewChaptersJob, connector, recurring, recurrence, parentJobId)
+ public DownloadNewChapters(GlobalBase clone, MangaConnector connector, string mangaInternalId, bool recurring = false, TimeSpan? recurrence = null, string? parentJobId = null, string translatedLanguage = "en") : base (clone, JobType.DownloadNewChaptersJob, recurring, recurrence, parentJobId)
{
- this.manga = manga;
+ this.mangaInternalId = mangaInternalId;
this.translatedLanguage = translatedLanguage;
}
protected override string GetId()
{
- return $"{GetType()}-{manga.internalId}";
+ return $"{GetType()}-{mangaInternalId}";
}
public override string ToString()
@@ -33,27 +33,39 @@ public class DownloadNewChapters : Job
protected override IEnumerable ExecuteReturnSubTasksInternal(JobBoss jobBoss)
{
- manga.SaveSeriesInfoJson();
- Chapter[] chapters = mangaConnector.GetNewChapters(manga, this.translatedLanguage);
+ if (manga is null)
+ {
+ Log($"Manga {mangaInternalId} is missing! Can not execute job.");
+ return Array.Empty();
+ }
+ manga.Value.SaveSeriesInfoJson();
+ Chapter[] chapters = manga.Value.mangaConnector.GetNewChapters(manga.Value, this.translatedLanguage);
this.progressToken.increments = chapters.Length;
List jobs = new();
- mangaConnector.CopyCoverFromCacheToDownloadLocation(manga);
+ manga.Value.mangaConnector.CopyCoverFromCacheToDownloadLocation(manga.Value);
foreach (Chapter chapter in chapters)
{
- DownloadChapter downloadChapterJob = new(this, this.mangaConnector, chapter, parentJobId: this.id);
+ DownloadChapter downloadChapterJob = new(this, chapter, parentJobId: this.id);
jobs.Add(downloadChapterJob);
}
- UpdateMetadata updateMetadataJob = new(this, this.mangaConnector, this.manga, parentJobId: this.id);
+ UpdateMetadata updateMetadataJob = new(this, mangaInternalId, parentJobId: this.id);
jobs.Add(updateMetadataJob);
progressToken.Complete();
return jobs;
}
+ protected override MangaConnector GetMangaConnector()
+ {
+ if (manga is null)
+ throw new Exception($"Missing Manga {mangaInternalId}");
+ return manga.Value.mangaConnector;
+ }
+
public override bool Equals(object? obj)
{
if (obj is not DownloadNewChapters otherJob)
return false;
return otherJob.mangaConnector == this.mangaConnector &&
- otherJob.manga.publicationId == this.manga.publicationId;
+ otherJob.manga?.publicationId == this.manga?.publicationId;
}
}
\ No newline at end of file
diff --git a/Tranga/Jobs/Job.cs b/Tranga/Jobs/Job.cs
index deefd77..ac48b39 100644
--- a/Tranga/Jobs/Job.cs
+++ b/Tranga/Jobs/Job.cs
@@ -4,7 +4,6 @@ namespace Tranga.Jobs;
public abstract class Job : GlobalBase
{
- public MangaConnector mangaConnector { get; init; }
public ProgressToken progressToken { get; private set; }
public bool recurring { get; init; }
public TimeSpan? recurrenceTime { get; set; }
@@ -13,14 +12,15 @@ public abstract class Job : GlobalBase
public string id => GetId();
internal IEnumerable? subJobs { get; private set; }
public string? parentJobId { get; init; }
- public enum JobType : byte { DownloadChapterJob, DownloadNewChaptersJob, UpdateMetaDataJob }
+ public enum JobType : byte { DownloadChapterJob = 0, DownloadNewChaptersJob = 1, UpdateMetaDataJob = 2, MonitorManga = 3 }
+
+ public MangaConnector mangaConnector => GetMangaConnector();
public JobType jobType;
- internal Job(GlobalBase clone, JobType jobType, MangaConnector connector, bool recurring = false, TimeSpan? recurrenceTime = null, string? parentJobId = null) : base(clone)
+ internal Job(GlobalBase clone, JobType jobType, bool recurring = false, TimeSpan? recurrenceTime = null, string? parentJobId = null) : base(clone)
{
this.jobType = jobType;
- this.mangaConnector = connector;
this.progressToken = new ProgressToken(0);
this.recurring = recurring;
if (recurring && recurrenceTime is null)
@@ -31,11 +31,10 @@ public abstract class Job : GlobalBase
this.parentJobId = parentJobId;
}
- internal Job(GlobalBase clone, JobType jobType, MangaConnector connector, DateTime lastExecution, bool recurring = false,
+ internal Job(GlobalBase clone, JobType jobType, DateTime lastExecution, bool recurring = false,
TimeSpan? recurrenceTime = null, string? parentJobId = null) : base(clone)
{
this.jobType = jobType;
- this.mangaConnector = connector;
this.progressToken = new ProgressToken(0);
this.recurring = recurring;
if (recurring && recurrenceTime is null)
@@ -95,4 +94,6 @@ public abstract class Job : GlobalBase
}
protected abstract IEnumerable ExecuteReturnSubTasksInternal(JobBoss jobBoss);
+
+ protected abstract MangaConnector GetMangaConnector();
}
\ No newline at end of file
diff --git a/Tranga/Jobs/JobBoss.cs b/Tranga/Jobs/JobBoss.cs
index b1b38dd..c032f69 100644
--- a/Tranga/Jobs/JobBoss.cs
+++ b/Tranga/Jobs/JobBoss.cs
@@ -70,11 +70,9 @@ public class JobBoss : GlobalBase
RemoveJob(job);
}
- public IEnumerable GetJobsLike(string? connectorName = null, string? internalId = null, float? chapterNumber = null)
+ public IEnumerable GetJobsLike(string? internalId = null, float? chapterNumber = null)
{
IEnumerable ret = this.jobs;
- if (connectorName is not null)
- ret = ret.Where(job => job.mangaConnector.name == connectorName);
if (internalId is not null && chapterNumber is not null)
ret = ret.Where(jjob =>
@@ -89,18 +87,18 @@ public class JobBoss : GlobalBase
{
if (jjob is not DownloadNewChapters job)
return false;
- return job.manga.internalId == internalId;
+ return job.mangaInternalId == internalId;
});
return ret;
}
- public IEnumerable GetJobsLike(MangaConnector? mangaConnector = null, Manga? publication = null,
+ public IEnumerable GetJobsLike(Manga? publication = null,
Chapter? chapter = null)
{
if (chapter is not null)
- return GetJobsLike(mangaConnector?.name, chapter.Value.parentManga.internalId, chapter.Value.chapterNumber);
+ return GetJobsLike(chapter.Value.parentManga.internalId, chapter.Value.chapterNumber);
else
- return GetJobsLike(mangaConnector?.name, publication?.internalId);
+ return GetJobsLike(publication?.internalId);
}
public Job? GetJobById(string jobId)
@@ -150,6 +148,9 @@ public class JobBoss : GlobalBase
File.SetUnixFileMode(TrangaSettings.jobsFolderPath, UserRead | UserWrite | UserExecute | GroupRead | OtherRead);
if (!Directory.Exists(TrangaSettings.jobsFolderPath)) //No jobs to load
return;
+
+ //Load Manga-Files
+ ImportManga();
//Load json-job-files
foreach (FileInfo file in Directory.GetFiles(TrangaSettings.jobsFolderPath, "*.json").Select(f => new FileInfo(f)))
@@ -185,12 +186,24 @@ public class JobBoss : GlobalBase
parentJob.AddSubJob(job);
Log($"Parent Job {parentJob}");
}
- if (job is DownloadNewChapters dncJob)
- AddMangaToCache(dncJob.manga);
}
+ string[] jobMangaInternalIds = this.jobs.Where(job => job is DownloadNewChapters)
+ .Select(dnc => ((DownloadNewChapters)dnc).mangaInternalId).ToArray();
+ jobMangaInternalIds = jobMangaInternalIds.Concat(
+ this.jobs.Where(job => job is UpdateMetadata)
+ .Select(dnc => ((UpdateMetadata)dnc).mangaInternalId)).ToArray();
+ string[] internalIds = GetAllCachedManga().Select(m => m.internalId).ToArray();
+
+ string[] extraneousIds = internalIds.Except(jobMangaInternalIds).ToArray();
+ foreach (string internalId in extraneousIds)
+ RemoveMangaFromCache(internalId);
+
string[] coverFiles = Directory.GetFiles(TrangaSettings.coverImageCache);
foreach(string fileName in coverFiles.Where(fileName => !GetAllCachedManga().Any(manga => manga.coverFileNameInCache == fileName)))
+ File.Delete(fileName);
+ string[] mangaFiles = Directory.GetFiles(TrangaSettings.mangaCacheFolderPath);
+ foreach(string fileName in mangaFiles.Where(fileName => !GetAllCachedManga().Any(manga => fileName.Split('.')[0] == manga.internalId)))
File.Delete(fileName);
}
diff --git a/Tranga/Jobs/JobJsonConverter.cs b/Tranga/Jobs/JobJsonConverter.cs
index 5b12143..26a1a2a 100644
--- a/Tranga/Jobs/JobJsonConverter.cs
+++ b/Tranga/Jobs/JobJsonConverter.cs
@@ -23,53 +23,32 @@ public class JobJsonConverter : JsonConverter
public override object ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
{
JObject jo = JObject.Load(reader);
+
+ if(!jo.ContainsKey("jobType"))
+ throw new Exception();
- if (jo.ContainsKey("jobType") && jo["jobType"]!.Value() == (byte)Job.JobType.UpdateMetaDataJob)
+ return Enum.Parse(jo["jobType"]!.Value().ToString()) switch
{
- return new UpdateMetadata(this._clone,
- jo.GetValue("mangaConnector")!.ToObject(JsonSerializer.Create(new JsonSerializerSettings()
+ Job.JobType.UpdateMetaDataJob => new UpdateMetadata(_clone,
+ jo.GetValue("mangaInternalId")!.Value()!,
+ jo.GetValue("parentJobId")!.Value()),
+ Job.JobType.DownloadChapterJob => new DownloadChapter(this._clone,
+ jo.GetValue("chapter")!.ToObject(JsonSerializer.Create(new JsonSerializerSettings()
{
- Converters =
- {
- this._mangaConnectorJsonConverter
- }
- }))!,
- jo.GetValue("manga")!.ToObject(),
- jo.GetValue("parentJobId")!.Value());
- }else if ((jo.ContainsKey("jobType") && jo["jobType"]!.Value() == (byte)Job.JobType.DownloadNewChaptersJob) || jo.ContainsKey("translatedLanguage"))//TODO change to jobType
- {
- DateTime lastExecution = jo.GetValue("lastExecution") is {} le
- ? le.ToObject()
- : DateTime.UnixEpoch; //TODO do null checks on all variables
- return new DownloadNewChapters(this._clone,
- jo.GetValue("mangaConnector")!.ToObject(JsonSerializer.Create(new JsonSerializerSettings()
- {
- Converters =
- {
- this._mangaConnectorJsonConverter
- }
- }))!,
- jo.GetValue("manga")!.ToObject(),
- lastExecution,
+ Converters = { this._mangaConnectorJsonConverter }
+ })),
+ DateTime.UnixEpoch,
+ jo.GetValue("parentJobId")!.Value()),
+ Job.JobType.DownloadNewChaptersJob => new DownloadNewChapters(this._clone,
+ jo.GetValue("mangaInternalId")!.Value()!,
+ jo.GetValue("lastExecution") is {} le
+ ? le.ToObject()
+ : DateTime.UnixEpoch,
jo.GetValue("recurring")!.Value(),
jo.GetValue("recurrenceTime")!.ToObject(),
- jo.GetValue("parentJobId")!.Value());
- }else if ((jo.ContainsKey("jobType") && jo["jobType"]!.Value() == (byte)Job.JobType.DownloadChapterJob) || jo.ContainsKey("chapter"))//TODO change to jobType
- {
- return new DownloadChapter(this._clone,
- jo.GetValue("mangaConnector")!.ToObject(JsonSerializer.Create(new JsonSerializerSettings()
- {
- Converters =
- {
- this._mangaConnectorJsonConverter
- }
- }))!,
- jo.GetValue("chapter")!.ToObject(),
- DateTime.UnixEpoch,
- jo.GetValue("parentJobId")!.Value());
- }
-
- throw new Exception();
+ jo.GetValue("parentJobId")!.Value()),
+ _ => throw new Exception()
+ };
}
public override bool CanWrite => false;
diff --git a/Tranga/Jobs/ProgressToken.cs b/Tranga/Jobs/ProgressToken.cs
index e718c7d..3823ac2 100644
--- a/Tranga/Jobs/ProgressToken.cs
+++ b/Tranga/Jobs/ProgressToken.cs
@@ -10,7 +10,7 @@ public class ProgressToken
public DateTime executionStarted { get; private set; }
public TimeSpan timeRemaining => GetTimeRemaining();
- public enum State { Running, Complete, Standby, Cancelled, Waiting }
+ public enum State : byte { Running = 0, Complete = 1, Standby = 2, Cancelled = 3, Waiting = 4 }
public State state { get; private set; }
public ProgressToken(int increments)
diff --git a/Tranga/Jobs/UpdateMetadata.cs b/Tranga/Jobs/UpdateMetadata.cs
index ac8bdd6..f122dc0 100644
--- a/Tranga/Jobs/UpdateMetadata.cs
+++ b/Tranga/Jobs/UpdateMetadata.cs
@@ -1,19 +1,21 @@
-using Tranga.MangaConnectors;
+using System.Text.Json.Serialization;
+using Tranga.MangaConnectors;
namespace Tranga.Jobs;
public class UpdateMetadata : Job
{
- public Manga manga { get; set; }
+ public string mangaInternalId { get; set; }
+ [JsonIgnore] private Manga? manga => GetCachedManga(mangaInternalId);
- public UpdateMetadata(GlobalBase clone, MangaConnector connector, Manga manga, string? parentJobId = null) : base(clone, JobType.UpdateMetaDataJob, connector, parentJobId: parentJobId)
+ public UpdateMetadata(GlobalBase clone, string mangaInternalId, string? parentJobId = null) : base(clone, JobType.UpdateMetaDataJob, parentJobId: parentJobId)
{
- this.manga = manga;
+ this.mangaInternalId = mangaInternalId;
}
protected override string GetId()
{
- return $"{GetType()}-{manga.internalId}";
+ return $"{GetType()}-{mangaInternalId}";
}
public override string ToString()
@@ -23,8 +25,14 @@ public class UpdateMetadata : Job
protected override IEnumerable ExecuteReturnSubTasksInternal(JobBoss jobBoss)
{
+ if (manga is null)
+ {
+ Log($"Manga {mangaInternalId} is missing! Can not execute job.");
+ return Array.Empty();
+ }
+
//Retrieve new Metadata
- Manga? possibleUpdatedManga = mangaConnector.GetMangaFromId(manga.publicationId);
+ Manga? possibleUpdatedManga = mangaConnector.GetMangaFromId(manga.Value.publicationId);
if (possibleUpdatedManga is { } updatedManga)
{
if (updatedManga.Equals(this.manga)) //Check if anything changed
@@ -33,26 +41,9 @@ public class UpdateMetadata : Job
return Array.Empty();
}
- this.manga = manga.WithMetadata(updatedManga);
- this.manga.SaveSeriesInfoJson(true);
- this.mangaConnector.CopyCoverFromCacheToDownloadLocation(manga);
- foreach (Job job in jobBoss.GetJobsLike(publication: this.manga))
- {
- string oldFile;
- if (job is DownloadNewChapters dc)
- {
- oldFile = dc.id;
- dc.manga = this.manga;
- }
- else if (job is UpdateMetadata um)
- {
- oldFile = um.id;
- um.manga = this.manga;
- }
- else
- continue;
- jobBoss.UpdateJobFile(job, oldFile);
- }
+ AddMangaToCache(manga.Value.WithMetadata(updatedManga));
+ this.manga.Value.SaveSeriesInfoJson(true);
+ this.mangaConnector.CopyCoverFromCacheToDownloadLocation((Manga)manga);
this.progressToken.Complete();
}
else
@@ -65,12 +56,19 @@ public class UpdateMetadata : Job
return Array.Empty();
}
+ protected override MangaConnector GetMangaConnector()
+ {
+ if (manga is null)
+ throw new Exception($"Missing Manga {mangaInternalId}");
+ return manga.Value.mangaConnector;
+ }
+
public override bool Equals(object? obj)
{
if (obj is not UpdateMetadata otherJob)
return false;
return otherJob.mangaConnector == this.mangaConnector &&
- otherJob.manga.publicationId == this.manga.publicationId;
+ otherJob.manga?.publicationId == this.manga?.publicationId;
}
}
\ No newline at end of file
diff --git a/Tranga/Manga.cs b/Tranga/Manga.cs
index 6989d7d..ea7298a 100644
--- a/Tranga/Manga.cs
+++ b/Tranga/Manga.cs
@@ -3,6 +3,7 @@ using System.Text;
using System.Text.RegularExpressions;
using System.Web;
using Newtonsoft.Json;
+using Tranga.MangaConnectors;
using static System.IO.UnixFileMode;
namespace Tranga;
@@ -27,8 +28,6 @@ public struct Manga
// ReSharper disable once MemberCanBePrivate.Global
public int? year { get; private set; }
public string? originalLanguage { get; }
- // ReSharper disable twice MemberCanBePrivate.Global
- public string status { get; private set; }
public ReleaseStatusByte releaseStatus { get; private set; }
public enum ReleaseStatusByte : byte
{
@@ -44,14 +43,15 @@ public struct Manga
public float ignoreChaptersBelow { get; set; }
public float latestChapterDownloaded { get; set; }
public float latestChapterAvailable { get; set; }
-
- public string? websiteUrl { get; private set; }
+ public string websiteUrl { get; private set; }
+ public MangaConnector mangaConnector { get; private set; }
private static readonly Regex LegalCharacters = new (@"[A-Za-zÀ-ÖØ-öø-ÿ0-9 \.\-,'\'\)\(~!\+]*");
[JsonConstructor]
- public Manga(string sortName, List authors, string? description, Dictionary altTitles, string[] tags, string? coverUrl, string? coverFileNameInCache, Dictionary? links, int? year, string? originalLanguage, string publicationId, ReleaseStatusByte releaseStatus, string? websiteUrl = null, string? folderName = null, float? ignoreChaptersBelow = 0)
+ public Manga(MangaConnector mangaConnector, string sortName, List authors, string? description, Dictionary altTitles, string[] tags, string? coverUrl, string? coverFileNameInCache, Dictionary? links, int? year, string? originalLanguage, string publicationId, ReleaseStatusByte releaseStatus, string? websiteUrl, string? folderName = null, float? ignoreChaptersBelow = 0)
{
+ this.mangaConnector = mangaConnector;
this.sortName = HttpUtility.HtmlDecode(sortName);
this.authors = authors.Select(HttpUtility.HtmlDecode).ToList()!;
this.description = HttpUtility.HtmlDecode(description);
@@ -72,8 +72,7 @@ public struct Manga
this.latestChapterDownloaded = 0;
this.latestChapterAvailable = 0;
this.releaseStatus = releaseStatus;
- this.status = Enum.GetName(releaseStatus) ?? "";
- this.websiteUrl = websiteUrl;
+ this.websiteUrl = websiteUrl??"";
}
public Manga WithMetadata(Manga newManga)
@@ -86,7 +85,6 @@ public struct Manga
authors = authors.Union(newManga.authors).ToList(),
altTitles = altTitles.UnionBy(newManga.altTitles, kv => kv.Key).ToDictionary(x => x.Key, x => x.Value),
tags = tags.Union(newManga.tags).ToArray(),
- status = newManga.status,
releaseStatus = newManga.releaseStatus,
websiteUrl = newManga.websiteUrl,
year = newManga.year,
@@ -100,7 +98,6 @@ public struct Manga
return false;
return this.description == compareManga.description &&
this.year == compareManga.year &&
- this.status == compareManga.status &&
this.releaseStatus == compareManga.releaseStatus &&
this.sortName == compareManga.sortName &&
this.latestChapterAvailable.Equals(compareManga.latestChapterAvailable) &&
diff --git a/Tranga/MangaConnectors/AsuraToon.cs b/Tranga/MangaConnectors/AsuraToon.cs
index 8ec5265..6dd75b8 100644
--- a/Tranga/MangaConnectors/AsuraToon.cs
+++ b/Tranga/MangaConnectors/AsuraToon.cs
@@ -8,7 +8,7 @@ namespace Tranga.MangaConnectors;
public class AsuraToon : MangaConnector
{
- public AsuraToon(GlobalBase clone) : base(clone, "AsuraToon", ["en"])
+ public AsuraToon(GlobalBase clone) : base(clone, "AsuraToon", ["en"], ["asuracomic.net"])
{
this.downloadClient = new ChromiumDownloadClient(clone);
}
@@ -113,7 +113,7 @@ public class AsuraToon : MangaConnector
HtmlNode? firstChapterNode = document.DocumentNode.SelectSingleNode("//a[contains(@href, 'chapter/1')]/../following-sibling::h3");
int? year = int.Parse(firstChapterNode?.InnerText.Split(' ')[^1] ?? "2000");
- Manga manga = new (sortName, authors, description, altTitles, tags, coverUrl, coverFileNameInCache, links,
+ Manga manga = new (this, sortName, authors, description, altTitles, tags, coverUrl, coverFileNameInCache, links,
year, originalLanguage, publicationId, releaseStatus, websiteUrl);
AddMangaToCache(manga);
return manga;
diff --git a/Tranga/MangaConnectors/Bato.cs b/Tranga/MangaConnectors/Bato.cs
index 1e00ca7..da0626d 100644
--- a/Tranga/MangaConnectors/Bato.cs
+++ b/Tranga/MangaConnectors/Bato.cs
@@ -8,7 +8,7 @@ namespace Tranga.MangaConnectors;
public class Bato : MangaConnector
{
- public Bato(GlobalBase clone) : base(clone, "Bato", ["en"])
+ public Bato(GlobalBase clone) : base(clone, "Bato", ["en"], ["bato.to"])
{
this.downloadClient = new HttpDownloadClient(clone);
}
@@ -114,8 +114,8 @@ public class Bato : MangaConnector
case "pending": releaseStatus = Manga.ReleaseStatusByte.Unreleased; break;
}
- Manga manga = new (sortName, authors, description, altTitles, tags, posterUrl, coverFileNameInCache, new Dictionary(),
- year, originalLanguage, publicationId, releaseStatus, websiteUrl: websiteUrl);
+ Manga manga = new (this, sortName, authors, description, altTitles, tags, posterUrl, coverFileNameInCache, new Dictionary(),
+ year, originalLanguage, publicationId, releaseStatus, websiteUrl);
AddMangaToCache(manga);
return manga;
}
diff --git a/Tranga/MangaConnectors/MangaConnector.cs b/Tranga/MangaConnectors/MangaConnector.cs
index c98bfad..e5d9343 100644
--- a/Tranga/MangaConnectors/MangaConnector.cs
+++ b/Tranga/MangaConnectors/MangaConnector.cs
@@ -2,6 +2,10 @@
using System.Net;
using System.Runtime.InteropServices;
using System.Text.RegularExpressions;
+using SixLabors.ImageSharp;
+using SixLabors.ImageSharp.Formats.Jpeg;
+using SixLabors.ImageSharp.Processing;
+using SixLabors.ImageSharp.Processing.Processors.Binarization;
using Tranga.Jobs;
using static System.IO.UnixFileMode;
@@ -15,11 +19,13 @@ public abstract class MangaConnector : GlobalBase
{
internal DownloadClient downloadClient { get; init; } = null!;
public string[] SupportedLanguages;
+ public string[] BaseUris;
- protected MangaConnector(GlobalBase clone, string name, string[] supportedLanguages) : base(clone)
+ protected MangaConnector(GlobalBase clone, string name, string[] supportedLanguages, string[] baseUris) : base(clone)
{
this.name = name;
this.SupportedLanguages = supportedLanguages;
+ this.BaseUris = baseUris;
Directory.CreateDirectory(TrangaSettings.coverImageCache);
}
@@ -140,6 +146,22 @@ public abstract class MangaConnector : GlobalBase
return requestResult.statusCode;
}
+ private void ProcessImage(string imagePath)
+ {
+ if (!TrangaSettings.bwImages && TrangaSettings.compression == 100)
+ return;
+ DateTime start = DateTime.Now;
+ using Image image = Image.Load(imagePath);
+ File.Delete(imagePath);
+ if(TrangaSettings.bwImages)
+ image.Mutate(i => i.ApplyProcessor(new AdaptiveThresholdProcessor()));
+ image.SaveAsJpeg(imagePath, new JpegEncoder()
+ {
+ Quality = TrangaSettings.compression
+ });
+ Log($"Image processing took {DateTime.Now.Subtract(start):s\\.fff} B/W:{TrangaSettings.bwImages} Compression: {TrangaSettings.compression}");
+ }
+
protected HttpStatusCode DownloadChapterImages(string[] imageUrls, Chapter chapter, RequestType requestType, string? referrer = null, ProgressToken? progressToken = null)
{
string saveArchiveFilePath = chapter.GetArchiveFilePath();
@@ -178,11 +200,14 @@ public abstract class MangaConnector : GlobalBase
progressToken?.Complete();
return HttpStatusCode.NoContent;
}
+
foreach (string imageUrl in imageUrls)
{
string extension = imageUrl.Split('.')[^1].Split('?')[0];
- Log($"Downloading image {chapterNum + 1:000}/{imageUrls.Length:000}"); //TODO
- HttpStatusCode status = DownloadImage(imageUrl, Path.Join(tempFolder, $"{chapterNum++}.{extension}"), requestType, referrer);
+ Log($"Downloading image {chapterNum + 1:000}/{imageUrls.Length:000}");
+ string imagePath = Path.Join(tempFolder, $"{chapterNum++}.{extension}");
+ HttpStatusCode status = DownloadImage(imageUrl, imagePath, requestType, referrer);
+ ProcessImage(imagePath);
Log($"{saveArchiveFilePath} {chapterNum + 1:000}/{imageUrls.Length:000} {status}");
if ((int)status < 200 || (int)status >= 300)
{
diff --git a/Tranga/MangaConnectors/MangaDex.cs b/Tranga/MangaConnectors/MangaDex.cs
index 8b23c2d..d1a4a96 100644
--- a/Tranga/MangaConnectors/MangaDex.cs
+++ b/Tranga/MangaConnectors/MangaDex.cs
@@ -10,7 +10,7 @@ public class MangaDex : MangaConnector
//https://api.mangadex.org/docs/3-enumerations/#language-codes--localization
//https://en.wikipedia.org/wiki/List_of_ISO_639_language_codes
//https://gist.github.com/Josantonius/b455e315bc7f790d14b136d61d9ae469
- public MangaDex(GlobalBase clone) : base(clone, "MangaDex", ["en","pt","pt-br","it","de","ru","aa","ab","ae","af","ak","am","an","ar-ae","ar-bh","ar-dz","ar-eg","ar-iq","ar-jo","ar-kw","ar-lb","ar-ly","ar-ma","ar-om","ar-qa","ar-sa","ar-sy","ar-tn","ar-ye","ar","as","av","ay","az","ba","be","bg","bh","bi","bm","bn","bo","br","bs","ca","ce","ch","co","cr","cs","cu","cv","cy","da","de-at","de-ch","de-de","de-li","de-lu","div","dv","dz","ee","el","en-au","en-bz","en-ca","en-cb","en-gb","en-ie","en-jm","en-nz","en-ph","en-tt","en-us","en-za","en-zw","eo","es-ar","es-bo","es-cl","es-co","es-cr","es-do","es-ec","es-es","es-gt","es-hn","es-la","es-mx","es-ni","es-pa","es-pe","es-pr","es-py","es-sv","es-us","es-uy","es-ve","es","et","eu","fa","ff","fi","fj","fo","fr-be","fr-ca","fr-ch","fr-fr","fr-lu","fr-mc","fr","fy","ga","gd","gl","gn","gu","gv","ha","he","hi","ho","hr-ba","hr-hr","hr","ht","hu","hy","hz","ia","id","ie","ig","ii","ik","in","io","is","it-ch","it-it","iu","iw","ja","ja-ro","ji","jv","jw","ka","kg","ki","kj","kk","kl","km","kn","ko","ko-ro","kr","ks","ku","kv","kw","ky","kz","la","lb","lg","li","ln","lo","ls","lt","lu","lv","mg","mh","mi","mk","ml","mn","mo","mr","ms-bn","ms-my","ms","mt","my","na","nb","nd","ne","ng","nl-be","nl-nl","nl","nn","no","nr","ns","nv","ny","oc","oj","om","or","os","pa","pi","pl","ps","pt-pt","qu-bo","qu-ec","qu-pe","qu","rm","rn","ro","rw","sa","sb","sc","sd","se-fi","se-no","se-se","se","sg","sh","si","sk","sl","sm","sn","so","sq","sr-ba","sr-sp","sr","ss","st","su","sv-fi","sv-se","sv","sw","sx","syr","ta","te","tg","th","ti","tk","tl","tn","to","tr","ts","tt","tw","ty","ug","uk","ur","us","uz","ve","vi","vo","wa","wo","xh","yi","yo","za","zh-cn","zh-hk","zh-mo","zh-ro","zh-sg","zh-tw","zh","zu"])
+ public MangaDex(GlobalBase clone) : base(clone, "MangaDex", ["en","pt","pt-br","it","de","ru","aa","ab","ae","af","ak","am","an","ar-ae","ar-bh","ar-dz","ar-eg","ar-iq","ar-jo","ar-kw","ar-lb","ar-ly","ar-ma","ar-om","ar-qa","ar-sa","ar-sy","ar-tn","ar-ye","ar","as","av","ay","az","ba","be","bg","bh","bi","bm","bn","bo","br","bs","ca","ce","ch","co","cr","cs","cu","cv","cy","da","de-at","de-ch","de-de","de-li","de-lu","div","dv","dz","ee","el","en-au","en-bz","en-ca","en-cb","en-gb","en-ie","en-jm","en-nz","en-ph","en-tt","en-us","en-za","en-zw","eo","es-ar","es-bo","es-cl","es-co","es-cr","es-do","es-ec","es-es","es-gt","es-hn","es-la","es-mx","es-ni","es-pa","es-pe","es-pr","es-py","es-sv","es-us","es-uy","es-ve","es","et","eu","fa","ff","fi","fj","fo","fr-be","fr-ca","fr-ch","fr-fr","fr-lu","fr-mc","fr","fy","ga","gd","gl","gn","gu","gv","ha","he","hi","ho","hr-ba","hr-hr","hr","ht","hu","hy","hz","ia","id","ie","ig","ii","ik","in","io","is","it-ch","it-it","iu","iw","ja","ja-ro","ji","jv","jw","ka","kg","ki","kj","kk","kl","km","kn","ko","ko-ro","kr","ks","ku","kv","kw","ky","kz","la","lb","lg","li","ln","lo","ls","lt","lu","lv","mg","mh","mi","mk","ml","mn","mo","mr","ms-bn","ms-my","ms","mt","my","na","nb","nd","ne","ng","nl-be","nl-nl","nl","nn","no","nr","ns","nv","ny","oc","oj","om","or","os","pa","pi","pl","ps","pt-pt","qu-bo","qu-ec","qu-pe","qu","rm","rn","ro","rw","sa","sb","sc","sd","se-fi","se-no","se-se","se","sg","sh","si","sk","sl","sm","sn","so","sq","sr-ba","sr-sp","sr","ss","st","su","sv-fi","sv-se","sv","sw","sx","syr","ta","te","tg","th","ti","tk","tl","tn","to","tr","ts","tt","tw","ty","ug","uk","ur","us","uz","ve","vi","vo","wa","wo","xh","yi","yo","za","zh-cn","zh-hk","zh-mo","zh-ro","zh-sg","zh-tw","zh","zu"], ["mangadex.org"])
{
this.downloadClient = new HttpDownloadClient(clone);
}
@@ -129,10 +129,10 @@ public class MangaDex : MangaConnector
false => null
};
- Manga.ReleaseStatusByte status = Manga.ReleaseStatusByte.Unreleased;
+ Manga.ReleaseStatusByte releaseStatus = Manga.ReleaseStatusByte.Unreleased;
if (attributes.TryGetPropertyValue("status", out JsonNode? statusNode))
{
- status = statusNode?.GetValue().ToLower() switch
+ releaseStatus = statusNode?.GetValue().ToLower() switch
{
"ongoing" => Manga.ReleaseStatusByte.Continuing,
"completed" => Manga.ReleaseStatusByte.Completed,
@@ -176,6 +176,7 @@ public class MangaDex : MangaConnector
}
Manga pub = new(
+ this,
title,
authors,
description,
@@ -187,8 +188,8 @@ public class MangaDex : MangaConnector
year,
originalLanguage,
publicationId,
- status,
- websiteUrl: $"https://mangadex.org/title/{publicationId}"
+ releaseStatus,
+ $"https://mangadex.org/title/{publicationId}"
);
AddMangaToCache(pub);
return pub;
diff --git a/Tranga/MangaConnectors/MangaHere.cs b/Tranga/MangaConnectors/MangaHere.cs
index 18c04d6..d758fb5 100644
--- a/Tranga/MangaConnectors/MangaHere.cs
+++ b/Tranga/MangaConnectors/MangaHere.cs
@@ -7,7 +7,7 @@ namespace Tranga.MangaConnectors;
public class MangaHere : MangaConnector
{
- public MangaHere(GlobalBase clone) : base(clone, "MangaHere", ["en"])
+ public MangaHere(GlobalBase clone) : base(clone, "MangaHere", ["en"], ["www.mangahere.cc"])
{
this.downloadClient = new ChromiumDownloadClient(clone);
}
@@ -101,7 +101,7 @@ public class MangaHere : MangaConnector
.SelectSingleNode("//p[contains(concat(' ',normalize-space(@class),' '),' fullcontent ')]");
string description = descriptionNode.InnerText;
- Manga manga = new(sortName, authors.ToList(), description, altTitles, tags.ToArray(), posterUrl,
+ Manga manga = new(this, sortName, authors.ToList(), description, altTitles, tags.ToArray(), posterUrl,
coverFileNameInCache, links,
null, originalLanguage, publicationId, releaseStatus, websiteUrl: websiteUrl);
AddMangaToCache(manga);
diff --git a/Tranga/MangaConnectors/MangaKatana.cs b/Tranga/MangaConnectors/MangaKatana.cs
index 8cd0c65..a336b1c 100644
--- a/Tranga/MangaConnectors/MangaKatana.cs
+++ b/Tranga/MangaConnectors/MangaKatana.cs
@@ -7,7 +7,7 @@ namespace Tranga.MangaConnectors;
public class MangaKatana : MangaConnector
{
- public MangaKatana(GlobalBase clone) : base(clone, "MangaKatana", ["en"])
+ public MangaKatana(GlobalBase clone) : base(clone, "MangaKatana", ["en"], ["mangakatana.com"])
{
this.downloadClient = new HttpDownloadClient(clone);
}
@@ -141,8 +141,8 @@ public class MangaKatana : MangaConnector
year = Convert.ToInt32(yearString);
}
- Manga manga = new (sortName, authors.ToList(), description, altTitles, tags.ToArray(), posterUrl, coverFileNameInCache, links,
- year, originalLanguage, publicationId, releaseStatus, websiteUrl: websiteUrl);
+ Manga manga = new (this, sortName, authors.ToList(), description, altTitles, tags.ToArray(), posterUrl, coverFileNameInCache, links,
+ year, originalLanguage, publicationId, releaseStatus, websiteUrl);
AddMangaToCache(manga);
return manga;
}
diff --git a/Tranga/MangaConnectors/MangaLife.cs b/Tranga/MangaConnectors/MangaLife.cs
index 66f2d20..9913e22 100644
--- a/Tranga/MangaConnectors/MangaLife.cs
+++ b/Tranga/MangaConnectors/MangaLife.cs
@@ -7,7 +7,7 @@ namespace Tranga.MangaConnectors;
public class MangaLife : MangaConnector
{
- public MangaLife(GlobalBase clone) : base(clone, "Manga4Life", ["en"])
+ public MangaLife(GlobalBase clone) : base(clone, "Manga4Life", ["en"], ["manga4life.com"])
{
this.downloadClient = new ChromiumDownloadClient(clone);
}
@@ -121,8 +121,8 @@ public class MangaLife : MangaConnector
.Descendants("div").First();
string description = descriptionNode.InnerText;
- Manga manga = new(sortName, authors.ToList(), description, altTitles, tags.ToArray(), posterUrl,
- coverFileNameInCache, links, year, originalLanguage, publicationId, releaseStatus, websiteUrl: websiteUrl);
+ Manga manga = new(this, sortName, authors.ToList(), description, altTitles, tags.ToArray(), posterUrl,
+ coverFileNameInCache, links, year, originalLanguage, publicationId, releaseStatus, websiteUrl);
AddMangaToCache(manga);
return manga;
}
diff --git a/Tranga/MangaConnectors/Manganato.cs b/Tranga/MangaConnectors/Manganato.cs
index 7d79414..f05af25 100644
--- a/Tranga/MangaConnectors/Manganato.cs
+++ b/Tranga/MangaConnectors/Manganato.cs
@@ -8,7 +8,7 @@ namespace Tranga.MangaConnectors;
public class Manganato : MangaConnector
{
- public Manganato(GlobalBase clone) : base(clone, "Manganato", ["en"])
+ public Manganato(GlobalBase clone) : base(clone, "Manganato", ["en"], ["manganato.com"])
{
this.downloadClient = new HttpDownloadClient(clone);
}
@@ -139,8 +139,8 @@ public class Manganato : MangaConnector
int year = DateTime.ParseExact(oldestChapter?.GetAttributeValue("title", "Dec 31 2400, 23:59")??"Dec 31 2400, 23:59", pattern,
CultureInfo.InvariantCulture).Year;
- Manga manga = new (sortName, authors.ToList(), description, altTitles, tags.ToArray(), posterUrl, coverFileNameInCache, links,
- year, originalLanguage, publicationId, releaseStatus, websiteUrl: websiteUrl);
+ Manga manga = new (this, sortName, authors.ToList(), description, altTitles, tags.ToArray(), posterUrl, coverFileNameInCache, links,
+ year, originalLanguage, publicationId, releaseStatus, websiteUrl);
AddMangaToCache(manga);
return manga;
}
diff --git a/Tranga/MangaConnectors/Mangasee.cs b/Tranga/MangaConnectors/Mangasee.cs
index f912f6d..f447726 100644
--- a/Tranga/MangaConnectors/Mangasee.cs
+++ b/Tranga/MangaConnectors/Mangasee.cs
@@ -11,7 +11,7 @@ namespace Tranga.MangaConnectors;
public class Mangasee : MangaConnector
{
- public Mangasee(GlobalBase clone) : base(clone, "Mangasee", ["en"])
+ public Mangasee(GlobalBase clone) : base(clone, "Mangasee", ["en"], ["mangasee123.com"])
{
this.downloadClient = new ChromiumDownloadClient(clone);
}
@@ -152,9 +152,8 @@ public class Mangasee : MangaConnector
.Descendants("div").First();
string description = descriptionNode.InnerText;
- Manga manga = new(sortName, authors.ToList(), description, altTitles, tags.ToArray(), posterUrl,
- coverFileNameInCache, links,
- year, originalLanguage, publicationId, releaseStatus, websiteUrl: websiteUrl);
+ Manga manga = new(this, sortName, authors.ToList(), description, altTitles, tags.ToArray(), posterUrl,
+ coverFileNameInCache, links, year, originalLanguage, publicationId, releaseStatus, websiteUrl);
AddMangaToCache(manga);
return manga;
}
diff --git a/Tranga/MangaConnectors/Mangaworld.cs b/Tranga/MangaConnectors/Mangaworld.cs
index f54876c..9772a8c 100644
--- a/Tranga/MangaConnectors/Mangaworld.cs
+++ b/Tranga/MangaConnectors/Mangaworld.cs
@@ -7,7 +7,7 @@ namespace Tranga.MangaConnectors;
public class Mangaworld: MangaConnector
{
- public Mangaworld(GlobalBase clone) : base(clone, "Mangaworld", ["it"])
+ public Mangaworld(GlobalBase clone) : base(clone, "Mangaworld", ["it"], ["www.mangaworld.ac"])
{
this.downloadClient = new ChromiumDownloadClient(clone);
}
@@ -118,8 +118,8 @@ public class Mangaworld: MangaConnector
string yearString = metadata.SelectSingleNode("//span[text()='Anno di uscita: ']/..").SelectNodes("a").First().InnerText;
int year = Convert.ToInt32(yearString);
- Manga manga = new (sortName, authors.ToList(), description, altTitles, tags.ToArray(), posterUrl, coverFileNameInCache, links,
- year, originalLanguage, publicationId, releaseStatus, websiteUrl: websiteUrl);
+ Manga manga = new (this, sortName, authors.ToList(), description, altTitles, tags.ToArray(), posterUrl, coverFileNameInCache, links,
+ year, originalLanguage, publicationId, releaseStatus, websiteUrl);
AddMangaToCache(manga);
return manga;
}
diff --git a/Tranga/MangaConnectors/ManhuaPlus.cs b/Tranga/MangaConnectors/ManhuaPlus.cs
index 28bbad2..40efe16 100644
--- a/Tranga/MangaConnectors/ManhuaPlus.cs
+++ b/Tranga/MangaConnectors/ManhuaPlus.cs
@@ -7,7 +7,7 @@ namespace Tranga.MangaConnectors;
public class ManhuaPlus : MangaConnector
{
- public ManhuaPlus(GlobalBase clone) : base(clone, "ManhuaPlus", ["en"])
+ public ManhuaPlus(GlobalBase clone) : base(clone, "ManhuaPlus", ["en"], ["manhuaplus.org"])
{
this.downloadClient = new ChromiumDownloadClient(clone);
}
@@ -127,7 +127,7 @@ public class ManhuaPlus : MangaConnector
.SelectSingleNode("//div[@id='syn-target']");
string description = descriptionNode.InnerText;
- Manga manga = new(sortName, authors.ToList(), description, altTitles, tags.ToArray(), posterUrl,
+ Manga manga = new(this, sortName, authors.ToList(), description, altTitles, tags.ToArray(), posterUrl,
coverFileNameInCache, links,
year, originalLanguage, publicationId, releaseStatus, websiteUrl: websiteUrl);
AddMangaToCache(manga);
diff --git a/Tranga/Server.cs b/Tranga/Server.cs
deleted file mode 100644
index dbf80a5..0000000
--- a/Tranga/Server.cs
+++ /dev/null
@@ -1,763 +0,0 @@
-using System.Net;
-using System.Runtime.InteropServices;
-using System.Text;
-using System.Text.RegularExpressions;
-using Newtonsoft.Json;
-using Tranga.Jobs;
-using Tranga.LibraryConnectors;
-using Tranga.MangaConnectors;
-using Tranga.NotificationConnectors;
-
-namespace Tranga;
-
-public class Server : GlobalBase
-{
- private readonly HttpListener _listener = new ();
- private readonly Tranga _parent;
-
- public Server(Tranga parent) : base(parent)
- {
- this._parent = parent;
- if(RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
- this._listener.Prefixes.Add($"http://*:{TrangaSettings.apiPortNumber}/");
- else
- this._listener.Prefixes.Add($"http://localhost:{TrangaSettings.apiPortNumber}/");
- Thread listenThread = new (Listen);
- listenThread.Start();
- Thread watchThread = new(WatchRunning);
- watchThread.Start();
- }
-
- private void WatchRunning()
- {
- while(_parent.keepRunning)
- Thread.Sleep(1000);
- this._listener.Close();
- }
-
- private void Listen()
- {
- this._listener.Start();
- foreach(string prefix in this._listener.Prefixes)
- Log($"Listening on {prefix}");
- while (this._listener.IsListening && _parent.keepRunning)
- {
- try
- {
- HttpListenerContext context = this._listener.GetContext();
- //Log($"{context.Request.HttpMethod} {context.Request.Url} {context.Request.UserAgent}");
- Task t = new(() =>
- {
- HandleRequest(context);
- });
- t.Start();
- }
- catch (HttpListenerException)
- {
-
- }
- }
- }
-
- private void HandleRequest(HttpListenerContext context)
- {
- HttpListenerRequest request = context.Request;
- HttpListenerResponse response = context.Response;
- if (request.Url!.LocalPath.Contains("favicon"))
- {
- SendResponse(HttpStatusCode.NoContent, response);
- return;
- }
-
- switch (request.HttpMethod)
- {
- case "GET":
- HandleGet(request, response);
- break;
- case "POST":
- HandlePost(request, response);
- break;
- case "DELETE":
- HandleDelete(request, response);
- break;
- case "OPTIONS":
- SendResponse(HttpStatusCode.OK, context.Response);
- break;
- default:
- SendResponse(HttpStatusCode.BadRequest, response);
- break;
- }
- }
-
- private Dictionary GetRequestVariables(string query)
- {
- Dictionary ret = new();
- Regex queryRex = new (@"\?{1}&?([A-z0-9-=]+=[A-z0-9-=]+)+(&[A-z0-9-=]+=[A-z0-9-=]+)*");
- if (!queryRex.IsMatch(query))
- return ret;
- query = query.Substring(1);
- foreach (string keyValuePair in query.Split('&').Where(str => str.Length >= 3))
- {
- string var = keyValuePair.Split('=')[0];
- string val = Regex.Replace(keyValuePair.Substring(var.Length + 1), "%20", " ");
- val = Regex.Replace(val, "%[0-9]{2}", "");
- ret.Add(var, val);
- }
- return ret;
- }
-
- private void HandleGet(HttpListenerRequest request, HttpListenerResponse response)
- {
- Dictionary requestVariables = GetRequestVariables(request.Url!.Query);
- string? connectorName, jobId, internalId;
- MangaConnector? connector;
- Manga? manga;
- string path = Regex.Match(request.Url!.LocalPath, @"[A-z0-9]+(\/[A-z0-9]+)*").Value;
- switch (path)
- {
- case "Connectors":
- SendResponse(HttpStatusCode.OK, response, _parent.GetConnectors().Select(con => con.name).ToArray());
- break;
- case "Manga/Cover":
- if (!requestVariables.TryGetValue("internalId", out internalId) ||
- !_parent.TryGetPublicationById(internalId, out manga))
- {
- SendResponse(HttpStatusCode.BadRequest, response);
- break;
- }
-
- string filePath = manga?.coverFileNameInCache ?? "";
- if (File.Exists(filePath))
- {
- FileStream coverStream = new(filePath, FileMode.Open);
- SendResponse(HttpStatusCode.OK, response, coverStream);
- }
- else
- {
- SendResponse(HttpStatusCode.NotFound, response);
- }
- break;
- case "Manga/FromConnector":
- requestVariables.TryGetValue("title", out string? title);
- requestVariables.TryGetValue("url", out string? url);
- if (!requestVariables.TryGetValue("connector", out connectorName) ||
- !_parent.TryGetConnector(connectorName, out connector) ||
- (title is null && url is null))
- {
- SendResponse(HttpStatusCode.BadRequest, response);
- break;
- }
-
- if (url is not null)
- {
- HashSet ret = new();
- manga = connector!.GetMangaFromUrl(url);
- if (manga is not null)
- ret.Add((Manga)manga);
- SendResponse(HttpStatusCode.OK, response, ret);
- }else
- SendResponse(HttpStatusCode.OK, response, connector!.GetManga(title!));
- break;
- case "Manga/Chapters":
- if(!requestVariables.TryGetValue("connector", out connectorName) ||
- !requestVariables.TryGetValue("internalId", out internalId) ||
- !_parent.TryGetConnector(connectorName, out connector) ||
- !_parent.TryGetPublicationById(internalId, out manga))
- {
- SendResponse(HttpStatusCode.BadRequest, response);
- break;
- }
- requestVariables.TryGetValue("translatedLanguage", out string? translatedLanguage);
- SendResponse(HttpStatusCode.OK, response, connector!.GetChapters((Manga)manga!, translatedLanguage??"en"));
- break;
- case "Jobs":
- if (!requestVariables.TryGetValue("jobId", out jobId))
- {
- if(!_parent.jobBoss.jobs.Any(jjob => jjob.id == jobId))
- SendResponse(HttpStatusCode.BadRequest, response);
- else
- SendResponse(HttpStatusCode.OK, response, _parent.jobBoss.jobs.First(jjob => jjob.id == jobId));
- break;
- }
- SendResponse(HttpStatusCode.OK, response, _parent.jobBoss.jobs);
- break;
- case "Jobs/Progress":
- if (requestVariables.TryGetValue("jobId", out jobId))
- {
- if(!_parent.jobBoss.jobs.Any(jjob => jjob.id == jobId))
- SendResponse(HttpStatusCode.BadRequest, response);
- else
- SendResponse(HttpStatusCode.OK, response, _parent.jobBoss.jobs.First(jjob => jjob.id == jobId).progressToken);
- break;
- }
- SendResponse(HttpStatusCode.OK, response, _parent.jobBoss.jobs.Select(jjob => jjob.progressToken));
- break;
- case "Jobs/Running":
- SendResponse(HttpStatusCode.OK, response, _parent.jobBoss.jobs.Where(jjob => jjob.progressToken.state is ProgressToken.State.Running));
- break;
- case "Jobs/Waiting":
- SendResponse(HttpStatusCode.OK, response, _parent.jobBoss.jobs.Where(jjob => jjob.progressToken.state is ProgressToken.State.Standby).OrderBy(jjob => jjob.nextExecution));
- break;
- case "Jobs/MonitorJobs":
- SendResponse(HttpStatusCode.OK, response, _parent.jobBoss.jobs.Where(jjob => jjob is DownloadNewChapters).OrderBy(jjob => ((DownloadNewChapters)jjob).manga.sortName));
- break;
- case "Settings":
- SendResponse(HttpStatusCode.OK, response, TrangaSettings.AsJObject());
- break;
- case "Settings/userAgent":
- SendResponse(HttpStatusCode.OK, response, TrangaSettings.userAgent);
- break;
- case "Settings/customRequestLimit":
- SendResponse(HttpStatusCode.OK, response, TrangaSettings.requestLimits);
- break;
- case "Settings/AprilFoolsMode":
- SendResponse(HttpStatusCode.OK, response, TrangaSettings.aprilFoolsMode);
- break;
- case "NotificationConnectors":
- SendResponse(HttpStatusCode.OK, response, notificationConnectors);
- break;
- case "NotificationConnectors/Types":
- SendResponse(HttpStatusCode.OK, response,
- Enum.GetValues().Select(nc => new KeyValuePair((byte)nc, Enum.GetName(nc))));
- break;
- case "LibraryConnectors":
- SendResponse(HttpStatusCode.OK, response, libraryConnectors);
- break;
- case "LibraryConnectors/Types":
- SendResponse(HttpStatusCode.OK, response,
- Enum.GetValues().Select(lc => new KeyValuePair((byte)lc, Enum.GetName(lc))));
- break;
- case "Ping":
- SendResponse(HttpStatusCode.OK, response, "Pong");
- break;
- case "LogMessages":
- if (logger is null || !File.Exists(logger?.logFilePath))
- {
- SendResponse(HttpStatusCode.NotFound, response);
- break;
- }
-
- if (requestVariables.TryGetValue("count", out string? count))
- {
- try
- {
- uint messageCount = uint.Parse(count);
- SendResponse(HttpStatusCode.OK, response, logger.Tail(messageCount));
- }
- catch (FormatException f)
- {
- SendResponse(HttpStatusCode.InternalServerError, response, f);
- }
- }else
- SendResponse(HttpStatusCode.OK, response, logger.GetLog());
- break;
- case "LogFile":
- if (logger is null || !File.Exists(logger?.logFilePath))
- {
- SendResponse(HttpStatusCode.NotFound, response);
- break;
- }
-
- string logDir = new FileInfo(logger.logFilePath).DirectoryName!;
- string tmpFilePath = Path.Join(logDir, "Tranga.log");
- File.Copy(logger.logFilePath, tmpFilePath);
- SendResponse(HttpStatusCode.OK, response, new FileStream(tmpFilePath, FileMode.Open));
- File.Delete(tmpFilePath);
- break;
- default:
- SendResponse(HttpStatusCode.BadRequest, response);
- break;
- }
- }
-
- private void HandlePost(HttpListenerRequest request, HttpListenerResponse response)
- {
- Dictionary requestVariables = GetRequestVariables(request.Url!.Query);
- string? connectorName, internalId, jobId, chapterNumStr, customFolderName, translatedLanguage, notificationConnectorStr, libraryConnectorStr;
- MangaConnector? connector;
- Manga? tmpManga;
- Manga manga;
- Job? job;
- NotificationConnector.NotificationConnectorType notificationConnectorType;
- LibraryConnector.LibraryType libraryConnectorType;
- string path = Regex.Match(request.Url!.LocalPath, @"[A-z0-9]+(\/[A-z0-9]+)*").Value;
- switch (path)
- {
- case "Manga":
- if(!requestVariables.TryGetValue("internalId", out internalId) ||
- !_parent.TryGetPublicationById(internalId, out tmpManga))
- {
- SendResponse(HttpStatusCode.BadRequest, response);
- break;
- }
- manga = (Manga)tmpManga!;
- SendResponse(HttpStatusCode.OK, response, manga);
- break;
- case "Jobs/MonitorManga":
- if(!requestVariables.TryGetValue("connector", out connectorName) ||
- !requestVariables.TryGetValue("internalId", out internalId) ||
- !requestVariables.TryGetValue("interval", out string? intervalStr) ||
- !_parent.TryGetConnector(connectorName, out connector)||
- !_parent.TryGetPublicationById(internalId, out tmpManga) ||
- !TimeSpan.TryParse(intervalStr, out TimeSpan interval))
- {
- SendResponse(HttpStatusCode.BadRequest, response);
- break;
- }
-
- manga = (Manga)tmpManga!;
-
- if (requestVariables.TryGetValue("ignoreBelowChapterNum", out chapterNumStr))
- {
- if (!float.TryParse(chapterNumStr, numberFormatDecimalPoint, out float chapterNum))
- {
- SendResponse(HttpStatusCode.BadRequest, response);
- break;
- }
- manga.ignoreChaptersBelow = chapterNum;
- }
-
- if (requestVariables.TryGetValue("customFolderName", out customFolderName))
- manga.MovePublicationFolder(TrangaSettings.downloadLocation, customFolderName);
- requestVariables.TryGetValue("translatedLanguage", out translatedLanguage);
-
- _parent.jobBoss.AddJob(new DownloadNewChapters(this, connector!, manga, true, interval, translatedLanguage: translatedLanguage??"en"));
- SendResponse(HttpStatusCode.Accepted, response);
- break;
- case "Jobs/DownloadNewChapters":
- if(!requestVariables.TryGetValue("connector", out connectorName) ||
- !requestVariables.TryGetValue("internalId", out internalId) ||
- !_parent.TryGetConnector(connectorName, out connector)||
- !_parent.TryGetPublicationById(internalId, out tmpManga))
- {
- SendResponse(HttpStatusCode.BadRequest, response);
- break;
- }
-
- manga = (Manga)tmpManga!;
-
- if (requestVariables.TryGetValue("ignoreBelowChapterNum", out chapterNumStr))
- {
- if (!float.TryParse(chapterNumStr, numberFormatDecimalPoint, out float chapterNum))
- {
- SendResponse(HttpStatusCode.BadRequest, response);
- break;
- }
- manga.ignoreChaptersBelow = chapterNum;
- }
-
- if (requestVariables.TryGetValue("customFolderName", out customFolderName))
- manga.MovePublicationFolder(TrangaSettings.downloadLocation, customFolderName);
- requestVariables.TryGetValue("translatedLanguage", out translatedLanguage);
-
- _parent.jobBoss.AddJob(new DownloadNewChapters(this, connector!, manga, false, translatedLanguage: translatedLanguage??"en"));
- SendResponse(HttpStatusCode.Accepted, response);
- break;
- case "Jobs/UpdateMetadata":
- if (!requestVariables.TryGetValue("internalId", out internalId))
- {
- foreach (Job pJob in _parent.jobBoss.jobs.Where(possibleDncJob =>
- possibleDncJob.jobType is Job.JobType.DownloadNewChaptersJob).ToArray())//ToArray to avoid modyifying while adding new jobs
- {
- DownloadNewChapters dncJob = pJob as DownloadNewChapters ??
- throw new Exception("Has to be DownloadNewChapters Job");
- _parent.jobBoss.AddJob(new UpdateMetadata(this, dncJob.mangaConnector, dncJob.manga));
- }
- SendResponse(HttpStatusCode.Accepted, response);
- }
- else
- {
- Job[] possibleDncJobs = _parent.jobBoss.GetJobsLike(internalId: internalId).ToArray();
- switch (possibleDncJobs.Length)
- {
- case <1: SendResponse(HttpStatusCode.BadRequest, response, "Could not find matching release"); break;
- case >1: SendResponse(HttpStatusCode.BadRequest, response, "Multiple releases??"); break;
- default:
- DownloadNewChapters dncJob = possibleDncJobs[0] as DownloadNewChapters ??
- throw new Exception("Has to be DownloadNewChapters Job");
- _parent.jobBoss.AddJob(new UpdateMetadata(this, dncJob.mangaConnector, dncJob.manga));
- SendResponse(HttpStatusCode.Accepted, response);
- break;
- }
- }
- break;
- case "Jobs/StartNow":
- if (!requestVariables.TryGetValue("jobId", out jobId) ||
- !_parent.jobBoss.TryGetJobById(jobId, out job))
- {
- SendResponse(HttpStatusCode.BadRequest, response);
- break;
- }
- _parent.jobBoss.AddJobToQueue(job!);
- SendResponse(HttpStatusCode.Accepted, response);
- break;
- case "Jobs/Cancel":
- if (!requestVariables.TryGetValue("jobId", out jobId) ||
- !_parent.jobBoss.TryGetJobById(jobId, out job))
- {
- SendResponse(HttpStatusCode.BadRequest, response);
- break;
- }
- job!.Cancel();
- SendResponse(HttpStatusCode.Accepted, response);
- break;
- case "Settings/UpdateDownloadLocation":
- if (!requestVariables.TryGetValue("downloadLocation", out string? downloadLocation) ||
- !requestVariables.TryGetValue("moveFiles", out string? moveFilesStr) ||
- !bool.TryParse(moveFilesStr, out bool moveFiles))
- {
- SendResponse(HttpStatusCode.BadRequest, response);
- break;
- }
- TrangaSettings.UpdateDownloadLocation(downloadLocation, moveFiles);
- SendResponse(HttpStatusCode.Accepted, response);
- break;
- case "Settings/AprilFoolsMode":
- if (!requestVariables.TryGetValue("enabled", out string? aprilFoolsModeEnabledStr) ||
- !bool.TryParse(aprilFoolsModeEnabledStr, out bool aprilFoolsModeEnabled))
- {
- SendResponse(HttpStatusCode.BadRequest, response);
- break;
- }
- TrangaSettings.UpdateAprilFoolsMode(aprilFoolsModeEnabled);
- SendResponse(HttpStatusCode.Accepted, response);
- break;
- /*case "Settings/UpdateWorkingDirectory":
- if (!requestVariables.TryGetValue("workingDirectory", out string? workingDirectory))
- {
- SendResponse(HttpStatusCode.BadRequest, response);
- break;
- }
- settings.UpdateWorkingDirectory(workingDirectory);
- SendResponse(HttpStatusCode.Accepted, response);
- break;*/
- case "Settings/userAgent":
- if(!requestVariables.TryGetValue("userAgent", out string? customUserAgent))
- {
- SendResponse(HttpStatusCode.BadRequest, response);
- break;
- }
- TrangaSettings.UpdateUserAgent(customUserAgent);
- SendResponse(HttpStatusCode.Accepted, response);
- break;
- case "Settings/userAgent/Reset":
- TrangaSettings.UpdateUserAgent(null);
- SendResponse(HttpStatusCode.Accepted, response);
- break;
- case "Settings/customRequestLimit":
- if (!requestVariables.TryGetValue("requestType", out string? requestTypeStr) ||
- !requestVariables.TryGetValue("requestsPerMinute", out string? requestsPerMinuteStr) ||
- !Enum.TryParse(requestTypeStr, out RequestType requestType) ||
- !int.TryParse(requestsPerMinuteStr, out int requestsPerMinute))
- {
- SendResponse(HttpStatusCode.BadRequest, response);
- break;
- }
-
- TrangaSettings.UpdateRateLimit(requestType, requestsPerMinute);
- SendResponse(HttpStatusCode.Accepted, response);
- break;
- case "Settings/customRequestLimit/Reset":
- TrangaSettings.ResetRateLimits();
- break;
- case "NotificationConnectors/Update":
- if (!requestVariables.TryGetValue("notificationConnector", out notificationConnectorStr) ||
- !Enum.TryParse(notificationConnectorStr, out notificationConnectorType))
- {
- SendResponse(HttpStatusCode.BadRequest, response);
- break;
- }
-
- if (notificationConnectorType is NotificationConnector.NotificationConnectorType.Gotify)
- {
- if (!requestVariables.TryGetValue("gotifyUrl", out string? gotifyUrl) ||
- !requestVariables.TryGetValue("gotifyAppToken", out string? gotifyAppToken))
- {
- SendResponse(HttpStatusCode.BadRequest, response);
- break;
- }
- AddNotificationConnector(new Gotify(this, gotifyUrl, gotifyAppToken));
- SendResponse(HttpStatusCode.Accepted, response);
- }else if (notificationConnectorType is NotificationConnector.NotificationConnectorType.LunaSea)
- {
- if (!requestVariables.TryGetValue("lunaseaWebhook", out string? lunaseaWebhook))
- {
- SendResponse(HttpStatusCode.BadRequest, response);
- break;
- }
- AddNotificationConnector(new LunaSea(this, lunaseaWebhook));
- SendResponse(HttpStatusCode.Accepted, response);
- }else if (notificationConnectorType is NotificationConnector.NotificationConnectorType.Ntfy)
- {
- if (!requestVariables.TryGetValue("ntfyUrl", out string? ntfyUrl) ||
- !requestVariables.TryGetValue("ntfyUser", out string? ntfyUser)||
- !requestVariables.TryGetValue("ntfyPass", out string? ntfyPass))
- {
- SendResponse(HttpStatusCode.BadRequest, response);
- break;
- }
- AddNotificationConnector(new Ntfy(this, ntfyUrl, ntfyUser, ntfyPass, null));
- SendResponse(HttpStatusCode.Accepted, response);
- }
- else
- {
- SendResponse(HttpStatusCode.BadRequest, response);
- }
- break;
- case "NotificationConnectors/Test":
- NotificationConnector notificationConnector;
- if (!requestVariables.TryGetValue("notificationConnector", out notificationConnectorStr) ||
- !Enum.TryParse(notificationConnectorStr, out notificationConnectorType))
- {
- SendResponse(HttpStatusCode.BadRequest, response);
- break;
- }
-
- if (notificationConnectorType is NotificationConnector.NotificationConnectorType.Gotify)
- {
- if (!requestVariables.TryGetValue("gotifyUrl", out string? gotifyUrl) ||
- !requestVariables.TryGetValue("gotifyAppToken", out string? gotifyAppToken))
- {
- SendResponse(HttpStatusCode.BadRequest, response);
- break;
- }
- notificationConnector = new Gotify(this, gotifyUrl, gotifyAppToken);
- }else if (notificationConnectorType is NotificationConnector.NotificationConnectorType.LunaSea)
- {
- if (!requestVariables.TryGetValue("lunaseaWebhook", out string? lunaseaWebhook))
- {
- SendResponse(HttpStatusCode.BadRequest, response);
- break;
- }
- notificationConnector = new LunaSea(this, lunaseaWebhook);
- }else if (notificationConnectorType is NotificationConnector.NotificationConnectorType.Ntfy)
- {
- if (!requestVariables.TryGetValue("ntfyUrl", out string? ntfyUrl) ||
- !requestVariables.TryGetValue("ntfyUser", out string? ntfyUser)||
- !requestVariables.TryGetValue("ntfyPass", out string? ntfyPass))
- {
- SendResponse(HttpStatusCode.BadRequest, response);
- break;
- }
- notificationConnector = new Ntfy(this, ntfyUrl, ntfyUser, ntfyPass, null);
- }
- else
- {
- SendResponse(HttpStatusCode.BadRequest, response);
- break;
- }
-
- notificationConnector.SendNotification("Tranga Test", "This is Test-Notification.");
- SendResponse(HttpStatusCode.Accepted, response);
- break;
- case "NotificationConnectors/Reset":
- if (!requestVariables.TryGetValue("notificationConnector", out notificationConnectorStr) ||
- !Enum.TryParse(notificationConnectorStr, out notificationConnectorType))
- {
- SendResponse(HttpStatusCode.BadRequest, response);
- break;
- }
- DeleteNotificationConnector(notificationConnectorType);
- SendResponse(HttpStatusCode.Accepted, response);
- break;
- case "LibraryConnectors/Update":
- if (!requestVariables.TryGetValue("libraryConnector", out libraryConnectorStr) ||
- !Enum.TryParse(libraryConnectorStr, out libraryConnectorType))
- {
- SendResponse(HttpStatusCode.BadRequest, response);
- break;
- }
-
- if (libraryConnectorType is LibraryConnector.LibraryType.Kavita)
- {
- if (!requestVariables.TryGetValue("kavitaUrl", out string? kavitaUrl) ||
- !requestVariables.TryGetValue("kavitaUsername", out string? kavitaUsername) ||
- !requestVariables.TryGetValue("kavitaPassword", out string? kavitaPassword))
- {
- SendResponse(HttpStatusCode.BadRequest, response);
- break;
- }
- AddLibraryConnector(new Kavita(this, kavitaUrl, kavitaUsername, kavitaPassword));
- SendResponse(HttpStatusCode.Accepted, response);
- }else if (libraryConnectorType is LibraryConnector.LibraryType.Komga)
- {
- if (!requestVariables.TryGetValue("komgaUrl", out string? komgaUrl) ||
- !requestVariables.TryGetValue("komgaAuth", out string? komgaAuth))
- {
- SendResponse(HttpStatusCode.BadRequest, response);
- break;
- }
- AddLibraryConnector(new Komga(this, komgaUrl, komgaAuth));
- SendResponse(HttpStatusCode.Accepted, response);
- }
- else
- {
- SendResponse(HttpStatusCode.BadRequest, response);
- }
- break;
- case "LibraryConnectors/Test":
- LibraryConnector libraryConnector;
- if (!requestVariables.TryGetValue("libraryConnector", out libraryConnectorStr) ||
- !Enum.TryParse(libraryConnectorStr, out libraryConnectorType))
- {
- SendResponse(HttpStatusCode.BadRequest, response);
- break;
- }
-
- if (libraryConnectorType is LibraryConnector.LibraryType.Kavita)
- {
- if (!requestVariables.TryGetValue("kavitaUrl", out string? kavitaUrl) ||
- !requestVariables.TryGetValue("kavitaUsername", out string? kavitaUsername) ||
- !requestVariables.TryGetValue("kavitaPassword", out string? kavitaPassword))
- {
- SendResponse(HttpStatusCode.BadRequest, response);
- break;
- }
- libraryConnector = new Kavita(this, kavitaUrl, kavitaUsername, kavitaPassword);
- }else if (libraryConnectorType is LibraryConnector.LibraryType.Komga)
- {
- if (!requestVariables.TryGetValue("komgaUrl", out string? komgaUrl) ||
- !requestVariables.TryGetValue("komgaAuth", out string? komgaAuth))
- {
- SendResponse(HttpStatusCode.BadRequest, response);
- break;
- }
- libraryConnector = new Komga(this, komgaUrl, komgaAuth);
- }
- else
- {
- SendResponse(HttpStatusCode.BadRequest, response);
- break;
- }
- libraryConnector.UpdateLibrary();
- SendResponse(HttpStatusCode.Accepted, response);
- break;
- case "LibraryConnectors/Reset":
- if (!requestVariables.TryGetValue("libraryConnector", out libraryConnectorStr) ||
- !Enum.TryParse(libraryConnectorStr, out libraryConnectorType))
- {
- SendResponse(HttpStatusCode.BadRequest, response);
- break;
- }
- DeleteLibraryConnector(libraryConnectorType);
- SendResponse(HttpStatusCode.Accepted, response);
- break;
- default:
- SendResponse(HttpStatusCode.BadRequest, response);
- break;
- }
- }
-
- private void HandleDelete(HttpListenerRequest request, HttpListenerResponse response)
- {
- Dictionary requestVariables = GetRequestVariables(request.Url!.Query);
- string? connectorName, internalId;
- MangaConnector connector;
- Manga manga;
- string path = Regex.Match(request.Url!.LocalPath, @"[A-z0-9]+(\/[A-z0-9]+)*").Value;
- switch (path)
- {
- case "Jobs":
- if (!requestVariables.TryGetValue("jobId", out string? jobId) ||
- !_parent.jobBoss.TryGetJobById(jobId, out Job? job))
- {
- SendResponse(HttpStatusCode.BadRequest, response);
- break;
- }
- _parent.jobBoss.RemoveJob(job!);
- SendResponse(HttpStatusCode.Accepted, response);
- break;
- case "Jobs/DownloadNewChapters":
- if(!requestVariables.TryGetValue("connector", out connectorName) ||
- !requestVariables.TryGetValue("internalId", out internalId) ||
- _parent.GetConnector(connectorName) is null ||
- _parent.GetPublicationById(internalId) is null)
- {
- SendResponse(HttpStatusCode.BadRequest, response);
- break;
- }
- connector = _parent.GetConnector(connectorName)!;
- manga = (Manga)_parent.GetPublicationById(internalId)!;
- _parent.jobBoss.RemoveJobs(_parent.jobBoss.GetJobsLike(connector, manga));
- SendResponse(HttpStatusCode.Accepted, response);
- break;
- case "NotificationConnectors":
- if (!requestVariables.TryGetValue("notificationConnector", out string? notificationConnectorStr) ||
- !Enum.TryParse(notificationConnectorStr, out NotificationConnector.NotificationConnectorType notificationConnectorType))
- {
- SendResponse(HttpStatusCode.BadRequest, response);
- break;
- }
- DeleteNotificationConnector(notificationConnectorType);
- SendResponse(HttpStatusCode.Accepted, response);
- break;
- case "LibraryConnectors":
- if (!requestVariables.TryGetValue("libraryConnectors", out string? libraryConnectorStr) ||
- !Enum.TryParse(libraryConnectorStr,
- out LibraryConnector.LibraryType libraryConnectoryType))
- {
- SendResponse(HttpStatusCode.BadRequest, response);
- break;
- }
- DeleteLibraryConnector(libraryConnectoryType);
- SendResponse(HttpStatusCode.Accepted, response);
- break;
- default:
- SendResponse(HttpStatusCode.BadRequest, response);
- break;
- }
- }
-
- private void SendResponse(HttpStatusCode statusCode, HttpListenerResponse response, object? content = null)
- {
- //Log($"Response: {statusCode} {content}");
-
- response.StatusCode = (int)statusCode;
- response.AddHeader("Access-Control-Allow-Headers", "Content-Type, Accept, X-Requested-With");
- response.AddHeader("Access-Control-Allow-Methods", "GET, POST, DELETE");
- response.AddHeader("Access-Control-Max-Age", "1728000");
- response.AppendHeader("Access-Control-Allow-Origin", "*");
- try
- {
-
- if (content is not Stream)
- {
- response.ContentType = "application/json";
- response.AddHeader("Cache-Control", "no-store");
- response.OutputStream.Write(content is not null
- ? Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(content))
- : Array.Empty());
- response.OutputStream.Close();
- }
- else if (content is FileStream stream)
- {
- string contentType = stream.Name.Split('.')[^1];
- response.AddHeader("Cache-Control", "max-age=600");
- switch (contentType.ToLower())
- {
- case "gif":
- response.ContentType = "image/gif";
- break;
- case "png":
- response.ContentType = "image/png";
- break;
- case "jpg":
- case "jpeg":
- response.ContentType = "image/jpeg";
- break;
- case "log":
- response.ContentType = "text/plain";
- break;
- }
-
- stream.CopyTo(response.OutputStream);
- response.OutputStream.Close();
- stream.Close();
- }
- }
- catch (Exception e)
- {
- Log(e.ToString());
- }
- }
-}
\ No newline at end of file
diff --git a/Tranga/Server/RequestPath.cs b/Tranga/Server/RequestPath.cs
new file mode 100644
index 0000000..cbf4d0d
--- /dev/null
+++ b/Tranga/Server/RequestPath.cs
@@ -0,0 +1,19 @@
+using System.Net;
+using System.Text.RegularExpressions;
+
+namespace Tranga.Server;
+
+internal struct RequestPath
+{
+ internal readonly string HttpMethod;
+ internal readonly string RegexStr;
+ internal readonly Func, ValueTuple> Method;
+
+ public RequestPath(string httpHttpMethod, string regexStr,
+ Func, ValueTuple> method)
+ {
+ this.HttpMethod = httpHttpMethod;
+ this.RegexStr = regexStr + "(?:/?)";
+ this.Method = method;
+ }
+}
\ No newline at end of file
diff --git a/Tranga/Server/Server.cs b/Tranga/Server/Server.cs
new file mode 100644
index 0000000..b7ce0f8
--- /dev/null
+++ b/Tranga/Server/Server.cs
@@ -0,0 +1,269 @@
+using System.Net;
+using System.Runtime.InteropServices;
+using System.Text;
+using System.Text.RegularExpressions;
+using Newtonsoft.Json;
+using SixLabors.ImageSharp;
+using SixLabors.ImageSharp.Formats.Png;
+using SixLabors.ImageSharp.Metadata.Profiles.Exif;
+using SixLabors.ImageSharp.Metadata.Profiles.Iptc;
+using ZstdSharp;
+
+namespace Tranga.Server;
+
+public partial class Server : GlobalBase, IDisposable
+{
+ private readonly HttpListener _listener = new();
+ private readonly Tranga _parent;
+ private bool _running = true;
+
+ private readonly List _apiRequestPaths;
+
+ public Server(Tranga parent) : base(parent)
+ {
+ /*
+ * Contains all valid Request Methods, Paths (with Regex Group Matching for specific Parameters) and Handling Methods
+ */
+ _apiRequestPaths = new List
+ {
+ new ("GET", @"/v2/Connector/Types", GetV2ConnectorTypes),
+ new ("GET", @"/v2/Connector/([a-zA-Z]+)/GetManga", GetV2ConnectorConnectorNameGetManga),
+ new ("GET", @"/v2/Mangas", GetV2Mangas),
+ new ("GET", @"/v2/Manga/Search", GetV2MangaSearch),
+ new ("GET", @"/v2/Manga", GetV2Manga),
+ new ("GET", @"/v2/Manga/([-A-Za-z0-9]*={0,3})/Cover", GetV2MangaInternalIdCover),
+ new ("GET", @"/v2/Manga/([-A-Za-z0-9]*={0,3})/Chapters", GetV2MangaInternalIdChapters),
+ new ("GET", @"/v2/Manga/([-A-Za-z0-9]*={0,3})/Chapters/Latest", GetV2MangaInternalIdChaptersLatest),
+ new ("POST", @"/v2/Manga/([-A-Za-z0-9]*={0,3})/ignoreChaptersBelow", PostV2MangaInternalIdIgnoreChaptersBelow),
+ new ("POST", @"/v2/Manga/([-A-Za-z0-9]*={0,3})/moveFolder", PostV2MangaInternalIdMoveFolder),
+ new ("GET", @"/v2/Manga/([-A-Za-z0-9]*={0,3})", GetV2MangaInternalId),
+ new ("DELETE", @"/v2/Manga/([-A-Za-z0-9]*={0,3})", DeleteV2MangaInternalId),
+ new ("GET", @"/v2/Jobs", GetV2Jobs),
+ new ("GET", @"/v2/Jobs/Running", GetV2JobsRunning),
+ new ("GET", @"/v2/Jobs/Waiting", GetV2JobsWaiting),
+ new ("GET", @"/v2/Jobs/Monitoring", GetV2JobsMonitoring),
+ new ("GET", @"/v2/Jobs/Standby", GetV2JobsStandby),
+ new ("GET", @"/v2/Job/Types", GetV2JobTypes),
+ new ("POST", @"/v2/Job/Create/([a-zA-Z]+)", PostV2JobCreateType),
+ new ("GET", @"/v2/Job", GetV2Job),
+ new ("GET", @"/v2/Job/([a-zA-Z\.]+-[-A-Za-z0-9+/]*={0,3}(?:-[0-9]+)?)/Progress", GetV2JobJobIdProgress),
+ new ("POST", @"/v2/Job/([a-zA-Z\.]+-[-A-Za-z0-9+/]*={0,3}(?:-[0-9]+)?)/StartNow", PostV2JobJobIdStartNow),
+ new ("POST", @"/v2/Job/([a-zA-Z\.]+-[-A-Za-z0-9+/]*={0,3}(?:-[0-9]+)?)/Cancel", PostV2JobJobIdCancel),
+ new ("GET", @"/v2/Job/([a-zA-Z\.]+-[-A-Za-z0-9+/]*={0,3}(?:-[0-9]+)?)", GetV2JobJobId),
+ new ("DELETE", @"/v2/Job/([a-zA-Z\.]+-[-A-Za-z0-9+/]*={0,3}(?:-[0-9]+)?)", DeleteV2JobJobId),
+ new ("GET", @"/v2/Settings", GetV2Settings),
+ new ("GET", @"/v2/Settings/UserAgent", GetV2SettingsUserAgent),
+ new ("POST", @"/v2/Settings/UserAgent", PostV2SettingsUserAgent),
+ new ("GET", @"/v2/Settings/RateLimit/Types", GetV2SettingsRateLimitTypes),
+ new ("GET", @"/v2/Settings/RateLimit", GetV2SettingsRateLimit),
+ new ("POST", @"/v2/Settings/RateLimit", PostV2SettingsRateLimit),
+ new ("GET", @"/v2/Settings/RateLimit/([a-zA-Z]+)", GetV2SettingsRateLimitType),
+ new ("POST", @"/v2/Settings/RateLimit/([a-zA-Z]+)", PostV2SettingsRateLimitType),
+ new ("GET", @"/v2/Settings/AprilFoolsMode", GetV2SettingsAprilFoolsMode),
+ new ("POST", @"/v2/Settings/AprilFoolsMode", PostV2SettingsAprilFoolsMode),
+ new ("GET", @"/v2/Settings/CompressImages", GetV2SettingsCompressImages),
+ new ("POST", @"/v2/Settings/CompressImages", PostV2SettingsCompressImages),
+ new ("GET", @"/v2/Settings/BWImages", GetV2SettingsBwImages),
+ new ("POST", @"/v2/Settings/BWImages", PostV2SettingsBwImages),
+ new ("POST", @"/v2/Settings/DownloadLocation", PostV2SettingsDownloadLocation),
+ new ("GET", @"/v2/LibraryConnector", GetV2LibraryConnector),
+ new ("GET", @"/v2/LibraryConnector/Types", GetV2LibraryConnectorTypes),
+ new ("GET", @"/v2/LibraryConnector/([a-zA-Z]+)", GetV2LibraryConnectorType),
+ new ("POST", @"/v2/LibraryConnector/([a-zA-Z]+)", PostV2LibraryConnectorType),
+ new ("POST", @"/v2/LibraryConnector/([a-zA-Z]+)/Test", PostV2LibraryConnectorTypeTest),
+ new ("DELETE", @"/v2/LibraryConnector/([a-zA-Z]+)", DeleteV2LibraryConnectorType),
+ new ("GET", @"/v2/NotificationConnector", GetV2NotificationConnector),
+ new ("GET", @"/v2/NotificationConnector/Types", GetV2NotificationConnectorTypes),
+ new ("GET", @"/v2/NotificationConnector/([a-zA-Z]+)", GetV2NotificationConnectorType),
+ new ("POST", @"/v2/NotificationConnector/([a-zA-Z]+)", PostV2NotificationConnectorType),
+ new ("POST", @"/v2/NotificationConnector/([a-zA-Z]+)/Test", PostV2NotificationConnectorTypeTest),
+ new ("DELETE", @"/v2/NotificationConnector/([a-zA-Z]+)", DeleteV2NotificationConnectorType),
+ new ("GET", @"/v2/LogFile", GetV2LogFile),
+ new ("GET", @"/v2/Ping", GetV2Ping),
+ new ("POST", @"/v2/Ping", PostV2Ping)
+ };
+
+ this._parent = parent;
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
+ this._listener.Prefixes.Add($"http://*:{TrangaSettings.apiPortNumber}/");
+ else
+ this._listener.Prefixes.Add($"http://localhost:{TrangaSettings.apiPortNumber}/");
+ Thread listenThread = new(Listen);
+ listenThread.Start();
+ while(_parent.keepRunning && _running)
+ Thread.Sleep(100);
+ this.Dispose();
+ }
+
+ private void Listen()
+ {
+ this._listener.Start();
+ foreach (string prefix in this._listener.Prefixes)
+ Log($"Listening on {prefix}");
+ while (this._listener.IsListening && _parent.keepRunning)
+ {
+ try
+ {
+ HttpListenerContext context = this._listener.GetContext();
+ //Log($"{context.Request.HttpMethod} {context.Request.Url} {context.Request.UserAgent}");
+ Task t = new(() =>
+ {
+ HandleRequest(context);
+ });
+ t.Start();
+ }
+ catch (HttpListenerException)
+ {
+
+ }
+ }
+ }
+
+ private void HandleRequest(HttpListenerContext context)
+ {
+ HttpListenerRequest request = context.Request;
+ HttpListenerResponse response = context.Response;
+ if (request.HttpMethod == "OPTIONS")
+ {
+ SendResponse(HttpStatusCode.NoContent, response);//Response always contains all valid Request-Methods
+ return;
+ }
+ if (request.Url!.LocalPath.Contains("favicon"))
+ {
+ SendResponse(HttpStatusCode.NoContent, response);
+ return;
+ }
+ string path = Regex.Match(request.Url.LocalPath, @"\/[a-zA-Z0-9\.+/=-]+(\/[a-zA-Z0-9\.+/=-]+)*").Value; //Local Path
+
+ if (!Regex.IsMatch(path, "/v2(/.*)?")) //Use only v2 API
+ {
+ SendResponse(HttpStatusCode.NotFound, response, "Use Version 2 API");
+ return;
+ }
+
+ Dictionary requestVariables = GetRequestVariables(request.Url!.Query); //Variables in the URI
+ Dictionary requestBody = GetRequestBody(request); //Variables in the JSON body
+ Dictionary requestParams = requestVariables.UnionBy(requestBody, v => v.Key)
+ .ToDictionary(kv => kv.Key, kv => kv.Value); //The actual variable used for the API
+
+ ValueTuple responseMessage; //Used to respond to the HttpRequest
+ if (_apiRequestPaths.Any(p => p.HttpMethod == request.HttpMethod && Regex.Match(path, p.RegexStr).Length == path.Length)) //Check if Request-Path is valid
+ {
+ RequestPath requestPath =
+ _apiRequestPaths.First(p => p.HttpMethod == request.HttpMethod && Regex.Match(path, p.RegexStr).Length == path.Length);
+ responseMessage =
+ requestPath.Method.Invoke(Regex.Match(path, requestPath.RegexStr).Groups, requestParams); //Get HttpResponse content
+ }
+ else
+ responseMessage = new ValueTuple(HttpStatusCode.MethodNotAllowed, "Unknown Request Path");
+
+ SendResponse(responseMessage.Item1, response, responseMessage.Item2);
+ }
+
+ private Dictionary GetRequestVariables(string query)
+ {
+ Dictionary ret = new();
+ Regex queryRex = new(@"\?{1}&?([A-z0-9-=]+=[A-z0-9-=]+)+(&[A-z0-9-=]+=[A-z0-9-=]+)*");
+ if (!queryRex.IsMatch(query))
+ return ret;
+ query = query.Substring(1);
+ foreach (string keyValuePair in query.Split('&').Where(str => str.Length >= 3))
+ {
+ string var = keyValuePair.Split('=')[0];
+ string val = Regex.Replace(keyValuePair.Substring(var.Length + 1), "%20", " ");
+ val = Regex.Replace(val, "%[0-9]{2}", "");
+ ret.Add(var, val);
+ }
+ return ret;
+ }
+
+ private Dictionary GetRequestBody(HttpListenerRequest request)
+ {
+ if (!request.HasEntityBody)
+ {
+ //Nospam Log("No request body");
+ return new Dictionary();
+ }
+ Stream body = request.InputStream;
+ Encoding encoding = request.ContentEncoding;
+ using StreamReader streamReader = new (body, encoding);
+ try
+ {
+ Dictionary requestBody =
+ JsonConvert.DeserializeObject>(streamReader.ReadToEnd())
+ ?? new();
+ return requestBody;
+ }
+ catch (JsonException e)
+ {
+ Log(e.Message);
+ }
+ return new Dictionary();
+ }
+
+ private void SendResponse(HttpStatusCode statusCode, HttpListenerResponse response, object? content = null)
+ {
+ //Log($"Response: {statusCode} {content}");
+ response.StatusCode = (int)statusCode;
+ response.AddHeader("Access-Control-Allow-Headers", "Content-Type, Accept, X-Requested-With");
+ response.AddHeader("Access-Control-Allow-Methods", "GET, POST, DELETE");
+ response.AddHeader("Access-Control-Max-Age", "1728000");
+ response.AddHeader("Access-Control-Allow-Origin", "*");
+ response.AddHeader("Content-Encoding", "zstd");
+
+ using CompressionStream compressor = new (response.OutputStream, 5);
+ try
+ {
+ if (content is Stream stream)
+ {
+ response.ContentType = "text/plain";
+ response.AddHeader("Cache-Control", "private, no-store");
+ stream.CopyTo(compressor);
+ stream.Close();
+ }else if (content is Image image)
+ {
+ response.ContentType = image.Metadata.DecodedImageFormat?.DefaultMimeType ?? PngFormat.Instance.DefaultMimeType;
+ response.AddHeader("Cache-Control", "public, max-age=3600");
+ response.AddHeader("Expires", $"{DateTime.Now.AddHours(1):ddd\\,\\ dd\\ MMM\\ yyyy\\ HH\\:mm\\:ss} GMT");
+ string lastModifiedStr = "";
+ if (image.Metadata.IptcProfile is not null)
+ {
+ DateTime date = DateTime.ParseExact(image.Metadata.IptcProfile.GetValues(IptcTag.CreatedDate).First().Value, "yyyyMMdd",null);
+ DateTime time = DateTime.ParseExact(image.Metadata.IptcProfile.GetValues(IptcTag.CreatedTime).First().Value, "HHmmssK",null);
+ lastModifiedStr = $"{date:ddd\\,\\ dd\\ MMM\\ yyyy} {time:HH\\:mm\\:ss} GMT";
+ }else if (image.Metadata.ExifProfile is not null)
+ {
+ DateTime datetime = DateTime.ParseExact(image.Metadata.ExifProfile.Values.FirstOrDefault(value => value.Tag == ExifTag.DateTime)?.ToString() ?? "2000:01:01 01:01:01", "yyyy:MM:dd HH:mm:ss", null);
+ lastModifiedStr = $"{datetime:ddd\\,\\ dd\\ MMM\\ yyyy\\ HH\\:mm\\:ss} GMT";
+ }
+ if(lastModifiedStr.Length>0)
+ response.AddHeader("Last-Modified", lastModifiedStr);
+ image.Save(compressor, image.Metadata.DecodedImageFormat ?? PngFormat.Instance);
+ image.Dispose();
+ }
+ else
+ {
+ response.ContentType = "application/json";
+ response.AddHeader("Cache-Control", "private, no-store");
+ if(content is not null)
+ new MemoryStream(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(content))).CopyTo(compressor);
+ else
+ compressor.Write(Array.Empty());
+ }
+
+ compressor.Flush();
+ response.OutputStream.Close();
+ }
+ catch (HttpListenerException e)
+ {
+ Log(e.ToString());
+ }
+ }
+
+
+ public void Dispose()
+ {
+ _running = false;
+ ((IDisposable)_listener).Dispose();
+ }
+}
\ No newline at end of file
diff --git a/Tranga/Server/v2Connector.cs b/Tranga/Server/v2Connector.cs
new file mode 100644
index 0000000..58fd354
--- /dev/null
+++ b/Tranga/Server/v2Connector.cs
@@ -0,0 +1,31 @@
+using System.Net;
+using System.Text.RegularExpressions;
+using Tranga.MangaConnectors;
+
+namespace Tranga.Server;
+
+public partial class Server
+{
+ private ValueTuple GetV2ConnectorTypes(GroupCollection groups, Dictionary requestParameters)
+ {
+ return new ValueTuple(HttpStatusCode.Accepted, _parent.GetConnectors());
+ }
+
+ private ValueTuple GetV2ConnectorConnectorNameGetManga(GroupCollection groups, Dictionary requestParameters)
+ {
+ if(groups.Count < 1 ||
+ !_parent.GetConnectors().Any(mangaConnector => mangaConnector.name == groups[1].Value)||
+ !_parent.TryGetConnector(groups[1].Value, out MangaConnector? connector) ||
+ connector is null)
+ return new ValueTuple(HttpStatusCode.BadRequest, $"Connector '{groups[1].Value}' does not exist.");
+
+ if (requestParameters.TryGetValue("title", out string? title))
+ {
+ return (HttpStatusCode.OK, connector.GetManga(title));
+ }else if (requestParameters.TryGetValue("url", out string? url))
+ {
+ return (HttpStatusCode.OK, connector.GetMangaFromUrl(url));
+ }else
+ return new ValueTuple(HttpStatusCode.BadRequest, "Parameter 'title' or 'url' has to be set.");
+ }
+}
\ No newline at end of file
diff --git a/Tranga/Server/v2Jobs.cs b/Tranga/Server/v2Jobs.cs
new file mode 100644
index 0000000..a0eb9b4
--- /dev/null
+++ b/Tranga/Server/v2Jobs.cs
@@ -0,0 +1,176 @@
+using System.Net;
+using System.Text.RegularExpressions;
+using Tranga.Jobs;
+using Tranga.MangaConnectors;
+
+namespace Tranga.Server;
+
+public partial class Server
+{
+ private ValueTuple GetV2Jobs(GroupCollection groups, Dictionary requestParameters)
+ {
+ return new ValueTuple(HttpStatusCode.OK, _parent.jobBoss.jobs.Select(job => job.id));
+ }
+
+ private ValueTuple GetV2JobsRunning(GroupCollection groups, Dictionary requestParameters)
+ {
+ return new ValueTuple(HttpStatusCode.OK, _parent.jobBoss.jobs
+ .Where(job => job.progressToken.state is ProgressToken.State.Running)
+ .Select(job => job.id));
+ }
+
+ private ValueTuple GetV2JobsWaiting(GroupCollection groups, Dictionary requestParameters)
+ {
+ return new ValueTuple(HttpStatusCode.OK, _parent.jobBoss.jobs
+ .Where(job => job.progressToken.state is ProgressToken.State.Waiting)
+ .Select(job => job.id));
+ }
+
+ private ValueTuple GetV2JobsStandby(GroupCollection groups, Dictionary requestParameters)
+ {
+ return new ValueTuple(HttpStatusCode.OK, _parent.jobBoss.jobs
+ .Where(job => job.progressToken.state is ProgressToken.State.Standby)
+ .Select(job => job.id));
+ }
+
+ private ValueTuple GetV2JobsMonitoring(GroupCollection groups, Dictionary requestParameters)
+ {
+ return new ValueTuple(HttpStatusCode.OK, _parent.jobBoss.jobs
+ .Where(job => job.jobType is Job.JobType.DownloadNewChaptersJob)
+ .Select(job => job.id));
+ }
+
+ private ValueTuple GetV2JobTypes(GroupCollection groups, Dictionary requestParameters)
+ {
+ return new ValueTuple(HttpStatusCode.OK,
+ Enum.GetValues().ToDictionary(b => (byte)b, b => Enum.GetName(b)));
+ }
+
+ private ValueTuple PostV2JobCreateType(GroupCollection groups, Dictionary requestParameters)
+ {
+ if (groups.Count < 1 ||
+ !Enum.TryParse(groups[1].Value, true, out Job.JobType jobType))
+ {
+ return new ValueTuple(HttpStatusCode.NotFound, $"JobType {groups[1].Value} does not exist.");
+ }
+
+ string? mangaId;
+ Manga? manga;
+ switch (jobType)
+ {
+ case Job.JobType.MonitorManga:
+ if(!requestParameters.TryGetValue("internalId", out mangaId) ||
+ !_parent.TryGetPublicationById(mangaId, out manga) ||
+ manga is null)
+ return new ValueTuple(HttpStatusCode.NotFound, "'internalId' Parameter missing, or is not a valid ID.");
+ if(!requestParameters.TryGetValue("interval", out string? intervalStr) ||
+ !TimeSpan.TryParse(intervalStr, out TimeSpan interval))
+ return new ValueTuple(HttpStatusCode.InternalServerError, "'interval' Parameter missing, or is not in correct format.");
+ requestParameters.TryGetValue("language", out string? language);
+ if (requestParameters.TryGetValue("customFolder", out string? folder))
+ manga.Value.MovePublicationFolder(TrangaSettings.downloadLocation, folder);
+ if (requestParameters.TryGetValue("startChapter", out string? startChapterStr) &&
+ float.TryParse(startChapterStr, out float startChapter))
+ {
+ Manga manga1 = manga.Value;
+ manga1.ignoreChaptersBelow = startChapter;
+ }
+
+ return _parent.jobBoss.AddJob(new DownloadNewChapters(this, ((Manga)manga).mangaConnector,
+ ((Manga)manga).internalId, true, interval, language)) switch
+ {
+ true => new ValueTuple(HttpStatusCode.OK, null),
+ false => new ValueTuple(HttpStatusCode.Conflict, "Job already exists."),
+ };
+ case Job.JobType.UpdateMetaDataJob:
+ if(!requestParameters.TryGetValue("internalId", out mangaId) ||
+ !_parent.TryGetPublicationById(mangaId, out manga) ||
+ manga is null)
+ return new ValueTuple(HttpStatusCode.NotFound, "InternalId Parameter missing, or is not a valid ID.");
+ return _parent.jobBoss.AddJob(new UpdateMetadata(this, ((Manga)manga).internalId)) switch
+ {
+ true => new ValueTuple(HttpStatusCode.OK, null),
+ false => new ValueTuple(HttpStatusCode.Conflict, "Job already exists."),
+ };
+ case Job.JobType.DownloadNewChaptersJob: //TODO
+ case Job.JobType.DownloadChapterJob: //TODO
+ default: return new ValueTuple(HttpStatusCode.MethodNotAllowed, $"JobType {Enum.GetName(jobType)} is not supported.");
+ }
+ }
+
+ private ValueTuple GetV2JobJobId(GroupCollection groups, Dictionary requestParameters)
+ {
+ if (groups.Count < 1 ||
+ !_parent.jobBoss.TryGetJobById(groups[1].Value, out Job? job) ||
+ job is null)
+ {
+ return new ValueTuple(HttpStatusCode.NotFound, $"Job with ID: '{groups[1].Value}' does not exist.");
+ }
+ return new ValueTuple(HttpStatusCode.OK, job);
+ }
+
+ private ValueTuple DeleteV2JobJobId(GroupCollection groups, Dictionary requestParameters)
+ {
+ if (groups.Count < 1 ||
+ !_parent.jobBoss.TryGetJobById(groups[1].Value, out Job? job) ||
+ job is null)
+ {
+ return new ValueTuple(HttpStatusCode.NotFound, $"Job with ID: '{groups[1].Value}' does not exist.");
+ }
+
+ _parent.jobBoss.RemoveJob(job);
+ return new ValueTuple(HttpStatusCode.OK, null);
+ }
+
+ private ValueTuple GetV2JobJobIdProgress(GroupCollection groups, Dictionary requestParameters)
+ {
+
+ if (groups.Count < 1 ||
+ !_parent.jobBoss.TryGetJobById(groups[1].Value, out Job? job) ||
+ job is null)
+ {
+ return new ValueTuple(HttpStatusCode.BadRequest, $"Job with ID: '{groups[1].Value}' does not exist.");
+ }
+ return new ValueTuple(HttpStatusCode.OK, job.progressToken);
+ }
+
+ private ValueTuple PostV2JobJobIdStartNow(GroupCollection groups, Dictionary requestParameters)
+ {
+ if (groups.Count < 1 ||
+ !_parent.jobBoss.TryGetJobById(groups[1].Value, out Job? job) ||
+ job is null)
+ {
+ return new ValueTuple(HttpStatusCode.NotFound, $"Job with ID: '{groups[1].Value}' does not exist.");
+ }
+ _parent.jobBoss.AddJobs(job.ExecuteReturnSubTasks(_parent.jobBoss));
+ return new ValueTuple(HttpStatusCode.OK, null);
+ }
+
+ private ValueTuple PostV2JobJobIdCancel(GroupCollection groups, Dictionary requestParameters)
+ {
+ if (groups.Count < 1 ||
+ !_parent.jobBoss.TryGetJobById(groups[1].Value, out Job? job) ||
+ job is null)
+ {
+ return new ValueTuple(HttpStatusCode.NotFound, $"Job with ID: '{groups[1].Value}' does not exist.");
+ }
+ job.Cancel();
+ return new ValueTuple(HttpStatusCode.OK, null);
+ }
+
+ private ValueTuple GetV2Job(GroupCollection groups, Dictionary requestParameters)
+ {
+ if(!requestParameters.TryGetValue("jobIds", out string? jobIdListStr))
+ return new ValueTuple(HttpStatusCode.BadRequest, "Missing parameter 'jobIds'.");
+ string[] jobIdList = jobIdListStr.Split(',');
+ List ret = new();
+ foreach (string jobId in jobIdList)
+ {
+ if(!_parent.jobBoss.TryGetJobById(jobId, out Job? job) || job is null)
+ return new ValueTuple(HttpStatusCode.NotFound, $"Job with id '{jobId}' not found.");
+ ret.Add(job);
+ }
+
+ return new ValueTuple(HttpStatusCode.OK, ret);
+ }
+}
\ No newline at end of file
diff --git a/Tranga/Server/v2LibraryConnectors.cs b/Tranga/Server/v2LibraryConnectors.cs
new file mode 100644
index 0000000..85c2ee0
--- /dev/null
+++ b/Tranga/Server/v2LibraryConnectors.cs
@@ -0,0 +1,116 @@
+using System.Net;
+using System.Text.RegularExpressions;
+using Tranga.LibraryConnectors;
+
+namespace Tranga.Server;
+
+public partial class Server
+{
+ private ValueTuple GetV2LibraryConnector(GroupCollection groups, Dictionary requestParameters)
+ {
+ return new ValueTuple(HttpStatusCode.OK, libraryConnectors);
+ }
+
+ private ValueTuple GetV2LibraryConnectorTypes(GroupCollection groups, Dictionary requestParameters)
+ {
+ return new ValueTuple(HttpStatusCode.OK,
+ Enum.GetValues().ToDictionary(b => (byte)b, b => Enum.GetName(b)));
+ }
+
+ private ValueTuple GetV2LibraryConnectorType(GroupCollection groups, Dictionary requestParameters)
+ {
+ if (groups.Count < 1 ||
+ !Enum.TryParse(groups[1].Value, true, out LibraryConnector.LibraryType libraryType))
+ {
+ return new ValueTuple(HttpStatusCode.NotFound, $"LibraryType {groups[1].Value} does not exist.");
+ }
+
+ if(libraryConnectors.All(lc => lc.libraryType != libraryType))
+ return new ValueTuple(HttpStatusCode.NotFound, $"LibraryType {Enum.GetName(libraryType)} not configured.");
+ else
+ return new ValueTuple(HttpStatusCode.OK, libraryConnectors.First(lc => lc.libraryType == libraryType));
+ }
+
+ private ValueTuple PostV2LibraryConnectorType(GroupCollection groups, Dictionary requestParameters)
+ {
+ if (groups.Count < 1 ||
+ !Enum.TryParse(groups[1].Value, true, out LibraryConnector.LibraryType libraryType))
+ {
+ return new ValueTuple(HttpStatusCode.NotFound, $"LibraryType {groups[1].Value} does not exist.");
+ }
+
+ if(!requestParameters.TryGetValue("url", out string? url))
+ return new ValueTuple(HttpStatusCode.NotAcceptable, "Parameter 'url' missing.");
+ if(!requestParameters.TryGetValue("username", out string? username))
+ return new ValueTuple(HttpStatusCode.NotAcceptable, "Parameter 'username' missing.");
+ if(!requestParameters.TryGetValue("password", out string? password))
+ return new ValueTuple(HttpStatusCode.NotAcceptable, "Parameter 'password' missing.");
+
+ switch (libraryType)
+ {
+ case LibraryConnector.LibraryType.Kavita:
+ Kavita kavita = new (this, url, username, password);
+ libraryConnectors.RemoveWhere(lc => lc.libraryType == LibraryConnector.LibraryType.Kavita);
+ libraryConnectors.Add(kavita);
+ return new ValueTuple(HttpStatusCode.OK, kavita);
+ case LibraryConnector.LibraryType.Komga:
+ Komga komga = new (this, url, username, password);
+ libraryConnectors.RemoveWhere(lc => lc.libraryType == LibraryConnector.LibraryType.Komga);
+ libraryConnectors.Add(komga);
+ return new ValueTuple(HttpStatusCode.OK, komga);
+ default: return new ValueTuple(HttpStatusCode.MethodNotAllowed, $"LibraryType {Enum.GetName(libraryType)} is not supported.");
+ }
+ }
+
+ private ValueTuple PostV2LibraryConnectorTypeTest(GroupCollection groups, Dictionary requestParameters)
+ {
+ if (groups.Count < 1 ||
+ !Enum.TryParse(groups[1].Value, true, out LibraryConnector.LibraryType libraryType))
+ {
+ return new ValueTuple(HttpStatusCode.NotFound, $"LibraryType {groups[1].Value} does not exist.");
+ }
+
+ if(!requestParameters.TryGetValue("url", out string? url))
+ return new ValueTuple(HttpStatusCode.NotAcceptable, "Parameter 'url' missing.");
+ if(!requestParameters.TryGetValue("username", out string? username))
+ return new ValueTuple(HttpStatusCode.NotAcceptable, "Parameter 'username' missing.");
+ if(!requestParameters.TryGetValue("password", out string? password))
+ return new ValueTuple(HttpStatusCode.NotAcceptable, "Parameter 'password' missing.");
+
+ switch (libraryType)
+ {
+ case LibraryConnector.LibraryType.Kavita:
+ Kavita kavita = new (this, url, username, password);
+ return kavita.Test() switch
+ {
+ true => new ValueTuple(HttpStatusCode.OK, kavita),
+ _ => new ValueTuple(HttpStatusCode.FailedDependency, kavita)
+ };
+ case LibraryConnector.LibraryType.Komga:
+ Komga komga = new (this, url, username, password);
+ return komga.Test() switch
+ {
+ true => new ValueTuple(HttpStatusCode.OK, komga),
+ _ => new ValueTuple(HttpStatusCode.FailedDependency, komga)
+ };
+ default: return new ValueTuple(HttpStatusCode.MethodNotAllowed, $"LibraryType {Enum.GetName(libraryType)} is not supported.");
+ }
+ }
+
+ private ValueTuple DeleteV2LibraryConnectorType(GroupCollection groups, Dictionary requestParameters)
+ {
+ if (groups.Count < 1 ||
+ !Enum.TryParse(groups[1].Value, true, out LibraryConnector.LibraryType libraryType))
+ {
+ return new ValueTuple(HttpStatusCode.NotFound, $"LibraryType {groups[1].Value} does not exist.");
+ }
+
+ if(libraryConnectors.All(lc => lc.libraryType != libraryType))
+ return new ValueTuple(HttpStatusCode.NotFound, $"LibraryType {Enum.GetName(libraryType)} not configured.");
+ else
+ {
+ libraryConnectors.Remove(libraryConnectors.First(lc => lc.libraryType == libraryType));
+ return new ValueTuple(HttpStatusCode.OK, null);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Tranga/Server/v2Manga.cs b/Tranga/Server/v2Manga.cs
new file mode 100644
index 0000000..cad38d6
--- /dev/null
+++ b/Tranga/Server/v2Manga.cs
@@ -0,0 +1,166 @@
+using SixLabors.ImageSharp;
+using SixLabors.ImageSharp.Processing;
+using System.Net;
+using System.Text.RegularExpressions;
+using SixLabors.ImageSharp.Processing.Processors.Transforms;
+using Tranga.Jobs;
+using Tranga.MangaConnectors;
+
+namespace Tranga.Server;
+
+public partial class Server
+{
+ private ValueTuple GetV2Mangas(GroupCollection groups, Dictionary requestParameters)
+ {
+ return new ValueTuple(HttpStatusCode.OK, GetAllCachedManga().Select(m => m.internalId));
+ }
+
+ private ValueTuple GetV2Manga(GroupCollection groups, Dictionary requestParameters)
+ {
+ if(!requestParameters.TryGetValue("mangaIds", out string? mangaIdListStr))
+ return new ValueTuple(HttpStatusCode.BadRequest, "Missing parameter 'mangaIds'.");
+ string[] mangaIdList = mangaIdListStr.Split(',').Distinct().ToArray();
+ List ret = new();
+ foreach (string mangaId in mangaIdList)
+ {
+ if(!_parent.TryGetPublicationById(mangaId, out Manga? manga) || manga is null)
+ return new ValueTuple(HttpStatusCode.NotFound, $"Manga with id '{mangaId}' not found.");
+ ret.Add(manga.Value);
+ }
+
+ return new ValueTuple(HttpStatusCode.OK, ret);
+ }
+
+ private ValueTuple GetV2MangaSearch(GroupCollection groups, Dictionary requestParameters)
+ {
+ if(!requestParameters.TryGetValue("title", out string? title))
+ return new ValueTuple(HttpStatusCode.BadRequest, "Missing parameter 'title'.");
+ List ret = new();
+ List threads = new();
+ foreach (MangaConnector mangaConnector in _connectors)
+ {
+ Thread t = new (() =>
+ {
+ ret.AddRange(mangaConnector.GetManga(title));
+ });
+ t.Start();
+ threads.Add(t);
+ }
+ while(threads.Any(t => t.ThreadState is ThreadState.Running or ThreadState.WaitSleepJoin))
+ Thread.Sleep(10);
+
+ return new ValueTuple(HttpStatusCode.OK, ret);
+ }
+
+ private ValueTuple GetV2MangaInternalId(GroupCollection groups, Dictionary requestParameters)
+ {
+ if(groups.Count < 1 ||
+ !_parent.TryGetPublicationById(groups[1].Value, out Manga? manga) ||
+ manga is null)
+ return new ValueTuple(HttpStatusCode.NotFound, $"Manga with ID '{groups[1].Value} could not be found.'");
+ return new ValueTuple(HttpStatusCode.OK, manga);
+ }
+
+ private ValueTuple DeleteV2MangaInternalId(GroupCollection groups, Dictionary requestParameters)
+ {
+ if(groups.Count < 1 ||
+ !_parent.TryGetPublicationById(groups[1].Value, out Manga? manga) ||
+ manga is null)
+ return new ValueTuple(HttpStatusCode.NotFound, $"Manga with ID '{groups[1].Value} could not be found.'");
+ Job[] jobs = _parent.jobBoss.GetJobsLike(publication: manga).ToArray();
+ _parent.jobBoss.RemoveJobs(jobs);
+ RemoveMangaFromCache(groups[1].Value);
+ return new ValueTuple(HttpStatusCode.OK, null);
+ }
+
+ private ValueTuple GetV2MangaInternalIdCover(GroupCollection groups, Dictionary requestParameters)
+ {
+ if(groups.Count < 1 ||
+ !_parent.TryGetPublicationById(groups[1].Value, out Manga? manga) ||
+ manga is null)
+ return new ValueTuple(HttpStatusCode.NotFound, $"Manga with ID '{groups[1].Value} could not be found.'");
+ string filePath = manga.Value.coverFileNameInCache!;
+ if(!File.Exists(filePath))
+ return new ValueTuple(HttpStatusCode.NotFound, "Cover-File not found.");
+
+ Image image = Image.Load(filePath);
+ if (requestParameters.TryGetValue("dimensions", out string? dimensionsStr))
+ {
+ Regex dimensionsRex = new(@"([0-9]+)x([0-9]+)");
+ if(!dimensionsRex.IsMatch(dimensionsStr))
+ return new ValueTuple(HttpStatusCode.BadRequest, "Requested dimensions not in required format.");
+ Match m = dimensionsRex.Match(dimensionsStr);
+ int width = int.Parse(m.Groups[1].Value);
+ int height = int.Parse(m.Groups[2].Value);
+ double aspectRequested = (double)width / (double)height;
+
+ double aspectCover = (double)image.Width / (double)image.Height;
+
+ Size newSize = aspectRequested > aspectCover
+ ? new Size(width, (width / image.Width) * image.Height)
+ : new Size((height / image.Height) * image.Width, height);
+
+ image.Mutate(x => x.Resize(newSize, CubicResampler.Robidoux, true));
+ }
+ return new ValueTuple(HttpStatusCode.OK, image);
+ }
+
+ private ValueTuple GetV2MangaInternalIdChapters(GroupCollection groups, Dictionary requestParameters)
+ {
+ if(groups.Count < 1 ||
+ !_parent.TryGetPublicationById(groups[1].Value, out Manga? manga) ||
+ manga is null)
+ return new ValueTuple(HttpStatusCode.NotFound, $"Manga with ID '{groups[1].Value} could not be found.'");
+
+ Chapter[] chapters = requestParameters.TryGetValue("language", out string? parameter) switch
+ {
+ true => manga.Value.mangaConnector.GetChapters((Manga)manga, parameter),
+ false => manga.Value.mangaConnector.GetChapters((Manga)manga)
+ };
+ return new ValueTuple(HttpStatusCode.OK, chapters);
+ }
+
+ private ValueTuple GetV2MangaInternalIdChaptersLatest(GroupCollection groups, Dictionary requestParameters)
+ {
+ if(groups.Count < 1 ||
+ !_parent.TryGetPublicationById(groups[1].Value, out Manga? manga) ||
+ manga is null)
+ return new ValueTuple(HttpStatusCode.NotFound, $"Manga with ID '{groups[1].Value} could not be found.'");
+
+ float latest = requestParameters.TryGetValue("language", out string? parameter) switch
+ {
+ true => manga.Value.mangaConnector.GetChapters(manga.Value, parameter).Max().chapterNumber,
+ false => manga.Value.mangaConnector.GetChapters(manga.Value).Max().chapterNumber
+ };
+ return new ValueTuple(HttpStatusCode.OK, latest);
+ }
+
+ private ValueTuple PostV2MangaInternalIdIgnoreChaptersBelow(GroupCollection groups, Dictionary requestParameters)
+ {
+ if(groups.Count < 1 ||
+ !_parent.TryGetPublicationById(groups[1].Value, out Manga? manga) ||
+ manga is null)
+ return new ValueTuple(HttpStatusCode.NotFound, $"Manga with ID '{groups[1].Value} could not be found.'");
+ if (requestParameters.TryGetValue("startChapter", out string? startChapterStr) &&
+ float.TryParse(startChapterStr, out float startChapter))
+ {
+ Manga manga1 = manga.Value;
+ manga1.ignoreChaptersBelow = startChapter;
+ return new ValueTuple(HttpStatusCode.OK, null);
+ }else
+ return new ValueTuple(HttpStatusCode.InternalServerError, "Parameter 'startChapter' missing, or failed to parse.");
+ }
+
+ private ValueTuple PostV2MangaInternalIdMoveFolder(GroupCollection groups, Dictionary requestParameters)
+ {
+
+ if(groups.Count < 1 ||
+ !_parent.TryGetPublicationById(groups[1].Value, out Manga? manga) ||
+ manga is null)
+ return new ValueTuple(HttpStatusCode.NotFound, $"Manga with ID '{groups[1].Value} could not be found.'");
+ if(!requestParameters.TryGetValue("location", out string? newFolder))
+ return new ValueTuple(HttpStatusCode.BadRequest, "Parameter 'location' missing.");
+ manga.Value.MovePublicationFolder(TrangaSettings.downloadLocation, newFolder);
+ return new ValueTuple(HttpStatusCode.OK, null);
+ }
+}
\ No newline at end of file
diff --git a/Tranga/Server/v2Miscellaneous.cs b/Tranga/Server/v2Miscellaneous.cs
new file mode 100644
index 0000000..08ff29f
--- /dev/null
+++ b/Tranga/Server/v2Miscellaneous.cs
@@ -0,0 +1,33 @@
+using System.Net;
+using System.Text.RegularExpressions;
+
+namespace Tranga.Server;
+
+public partial class Server
+{
+ private ValueTuple GetV2LogFile(GroupCollection groups, Dictionary requestParameters)
+ {
+ if (logger is null || !File.Exists(logger?.logFilePath))
+ {
+ return new ValueTuple(HttpStatusCode.NotFound, "Missing Logfile");
+ }
+
+ FileStream logFile = new (logger.logFilePath, FileMode.Open, FileAccess.Read);
+ FileStream content = new(Path.GetTempFileName(), FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.ReadWrite, 0, FileOptions.DeleteOnClose);
+ logFile.Position = 0;
+ logFile.CopyTo(content);
+ content.Position = 0;
+ logFile.Dispose();
+ return new ValueTuple(HttpStatusCode.OK, content);
+ }
+
+ private ValueTuple GetV2Ping(GroupCollection groups, Dictionary requestParameters)
+ {
+ return new ValueTuple(HttpStatusCode.Accepted, "Pong!");
+ }
+
+ private ValueTuple PostV2Ping(GroupCollection groups, Dictionary requestParameters)
+ {
+ return new ValueTuple(HttpStatusCode.Accepted, "Pong!");
+ }
+}
\ No newline at end of file
diff --git a/Tranga/Server/v2NotificationConnectors.cs b/Tranga/Server/v2NotificationConnectors.cs
new file mode 100644
index 0000000..d6917fb
--- /dev/null
+++ b/Tranga/Server/v2NotificationConnectors.cs
@@ -0,0 +1,136 @@
+using System.Net;
+using System.Text.RegularExpressions;
+using Tranga.NotificationConnectors;
+
+namespace Tranga.Server;
+
+public partial class Server
+{
+ private ValueTuple GetV2NotificationConnector(GroupCollection groups, Dictionary requestParameters)
+ {
+ return new ValueTuple(HttpStatusCode.OK, notificationConnectors);
+ }
+
+ private ValueTuple GetV2NotificationConnectorTypes(GroupCollection groups, Dictionary requestParameters)
+ {
+ return new ValueTuple(HttpStatusCode.OK,
+ Enum.GetValues().ToDictionary(b => (byte)b, b => Enum.GetName(b)));
+ }
+
+ private ValueTuple GetV2NotificationConnectorType(GroupCollection groups, Dictionary requestParameters)
+ {
+ if (groups.Count < 1 ||
+ !Enum.TryParse(groups[1].Value, true, out NotificationConnector.NotificationConnectorType notificationConnectorType))
+ {
+ return new ValueTuple(HttpStatusCode.NotFound, $"NotificationType {groups[1].Value} does not exist.");
+ }
+
+ if(notificationConnectors.All(nc => nc.notificationConnectorType != notificationConnectorType))
+ return new ValueTuple