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(HttpStatusCode.NotFound, $"NotificationType {Enum.GetName(notificationConnectorType)} not configured."); + else + return new ValueTuple(HttpStatusCode.OK, notificationConnectors.First(nc => nc.notificationConnectorType != notificationConnectorType)); + } + + private ValueTuple PostV2NotificationConnectorType(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."); + } + + string? url; + switch (notificationConnectorType) + { + case NotificationConnector.NotificationConnectorType.Gotify: + if(!requestParameters.TryGetValue("url", out url)) + return new ValueTuple(HttpStatusCode.NotAcceptable, "Parameter 'url' missing."); + if(!requestParameters.TryGetValue("appToken", out string? appToken)) + return new ValueTuple(HttpStatusCode.NotAcceptable, "Parameter 'appToken' missing."); + Gotify gotify = new (this, url, appToken); + this.notificationConnectors.RemoveWhere(nc => + nc.notificationConnectorType == NotificationConnector.NotificationConnectorType.Gotify); + this.notificationConnectors.Add(gotify); + return new ValueTuple(HttpStatusCode.OK, gotify); + case NotificationConnector.NotificationConnectorType.LunaSea: + if(!requestParameters.TryGetValue("webhook", out string? webhook)) + return new ValueTuple(HttpStatusCode.NotAcceptable, "Parameter 'webhook' missing."); + LunaSea lunaSea = new (this, webhook); + this.notificationConnectors.RemoveWhere(nc => + nc.notificationConnectorType == NotificationConnector.NotificationConnectorType.LunaSea); + this.notificationConnectors.Add(lunaSea); + return new ValueTuple(HttpStatusCode.OK, lunaSea); + case NotificationConnector.NotificationConnectorType.Ntfy: + if(!requestParameters.TryGetValue("url", out 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."); + Ntfy ntfy = new(this, url, username, password, null); + this.notificationConnectors.RemoveWhere(nc => + nc.notificationConnectorType == NotificationConnector.NotificationConnectorType.Ntfy); + this.notificationConnectors.Add(ntfy); + return new ValueTuple(HttpStatusCode.OK, ntfy); + default: + return new ValueTuple(HttpStatusCode.MethodNotAllowed, $"NotificationType {Enum.GetName(notificationConnectorType)} is not supported."); + } + } + + private ValueTuple PostV2NotificationConnectorTypeTest(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."); + } + + string? url; + switch (notificationConnectorType) + { + case NotificationConnector.NotificationConnectorType.Gotify: + if(!requestParameters.TryGetValue("url", out url)) + return new ValueTuple(HttpStatusCode.NotAcceptable, "Parameter 'url' missing."); + if(!requestParameters.TryGetValue("appToken", out string? appToken)) + return new ValueTuple(HttpStatusCode.NotAcceptable, "Parameter 'appToken' missing."); + Gotify gotify = new (this, url, appToken); + gotify.SendNotification("Tranga Test", "It was successful :3"); + return new ValueTuple(HttpStatusCode.OK, gotify); + case NotificationConnector.NotificationConnectorType.LunaSea: + if(!requestParameters.TryGetValue("webhook", out string? webhook)) + return new ValueTuple(HttpStatusCode.NotAcceptable, "Parameter 'webhook' missing."); + LunaSea lunaSea = new (this, webhook); + lunaSea.SendNotification("Tranga Test", "It was successful :3"); + return new ValueTuple(HttpStatusCode.OK, lunaSea); + case NotificationConnector.NotificationConnectorType.Ntfy: + if(!requestParameters.TryGetValue("url", out 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."); + Ntfy ntfy = new(this, url, username, password, null); + ntfy.SendNotification("Tranga Test", "It was successful :3"); + return new ValueTuple(HttpStatusCode.OK, ntfy); + default: + return new ValueTuple(HttpStatusCode.MethodNotAllowed, $"NotificationType {Enum.GetName(notificationConnectorType)} is not supported."); + } + } + + private ValueTuple DeleteV2NotificationConnectorType(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(HttpStatusCode.NotFound, $"NotificationType {Enum.GetName(notificationConnectorType)} not configured."); + else + { + notificationConnectors.Remove(notificationConnectors.First(nc => nc.notificationConnectorType != notificationConnectorType)); + return new ValueTuple(HttpStatusCode.OK, null); + } + } +} \ No newline at end of file diff --git a/Tranga/Server/v2Settings.cs b/Tranga/Server/v2Settings.cs new file mode 100644 index 0000000..4e0212e --- /dev/null +++ b/Tranga/Server/v2Settings.cs @@ -0,0 +1,137 @@ +using System.Net; +using System.Text.RegularExpressions; +using Tranga.MangaConnectors; + +namespace Tranga.Server; + +public partial class Server +{ + private ValueTuple GetV2Settings(GroupCollection groups, Dictionary requestParameters) + { + return new ValueTuple(HttpStatusCode.OK, TrangaSettings.AsJObject()); + } + + private ValueTuple GetV2SettingsUserAgent(GroupCollection groups, Dictionary requestParameters) + { + return new ValueTuple(HttpStatusCode.OK, TrangaSettings.userAgent); + } + + private ValueTuple PostV2SettingsUserAgent(GroupCollection groups, Dictionary requestParameters) + { + if (!requestParameters.TryGetValue("value", out string? userAgent)) + { + TrangaSettings.UpdateUserAgent(null); + return new ValueTuple(HttpStatusCode.Accepted, null); + } + else + { + TrangaSettings.UpdateUserAgent(userAgent); + return new ValueTuple(HttpStatusCode.OK, null); + } + } + + private ValueTuple GetV2SettingsRateLimitTypes(GroupCollection groups, Dictionary requestParameters) + { + return new ValueTuple(HttpStatusCode.OK, Enum.GetValues().ToDictionary(b =>(byte)b, b => Enum.GetName(b)) ); + } + + private ValueTuple GetV2SettingsRateLimit(GroupCollection groups, Dictionary requestParameters) + { + return new ValueTuple(HttpStatusCode.OK, TrangaSettings.requestLimits); + } + + private ValueTuple PostV2SettingsRateLimit(GroupCollection groups, Dictionary requestParameters) + { + foreach (KeyValuePair kv in requestParameters) + { + if(!Enum.TryParse(kv.Key, out RequestType requestType) || + !int.TryParse(kv.Value, out int requestsPerMinute)) + return new ValueTuple(HttpStatusCode.InternalServerError, null); + TrangaSettings.UpdateRateLimit(requestType, requestsPerMinute); + } + return new ValueTuple(HttpStatusCode.OK, TrangaSettings.requestLimits); + } + + private ValueTuple GetV2SettingsRateLimitType(GroupCollection groups, Dictionary requestParameters) + { + if(groups.Count < 1 || + !Enum.TryParse(groups[1].Value, out RequestType requestType)) + return new ValueTuple(HttpStatusCode.NotFound, $"RequestType {groups[1].Value}"); + return new ValueTuple(HttpStatusCode.OK, TrangaSettings.requestLimits[requestType]); + } + + private ValueTuple PostV2SettingsRateLimitType(GroupCollection groups, Dictionary requestParameters) + { + if(groups.Count < 1 || + !Enum.TryParse(groups[1].Value, out RequestType requestType)) + return new ValueTuple(HttpStatusCode.NotFound, $"RequestType {groups[1].Value}"); + if (!requestParameters.TryGetValue("value", out string? requestsPerMinuteStr) || + !int.TryParse(requestsPerMinuteStr, out int requestsPerMinute)) + return new ValueTuple(HttpStatusCode.InternalServerError, "Errors parsing requestsPerMinute"); + TrangaSettings.UpdateRateLimit(requestType, requestsPerMinute); + return new ValueTuple(HttpStatusCode.OK, null); + } + + private ValueTuple GetV2SettingsAprilFoolsMode(GroupCollection groups, Dictionary requestParameters) + { + return new ValueTuple(HttpStatusCode.OK, TrangaSettings.aprilFoolsMode); + } + + private ValueTuple PostV2SettingsAprilFoolsMode(GroupCollection groups, Dictionary requestParameters) + { + if (!requestParameters.TryGetValue("value", out string? trueFalseStr) || + !bool.TryParse(trueFalseStr, out bool trueFalse)) + return new ValueTuple(HttpStatusCode.InternalServerError, "Errors parsing 'value'"); + TrangaSettings.UpdateAprilFoolsMode(trueFalse); + return new ValueTuple(HttpStatusCode.OK, null); + } + + private ValueTuple GetV2SettingsCompressImages(GroupCollection groups, Dictionary requestParameters) + { + return new ValueTuple(HttpStatusCode.OK, TrangaSettings.compression); + } + + private ValueTuple PostV2SettingsCompressImages(GroupCollection groups, Dictionary requestParameters) + { + if (!requestParameters.TryGetValue("value", out string? valueStr) || + !int.TryParse(valueStr, out int value) + || value != int.Clamp(value, 1, 100)) + return new ValueTuple(HttpStatusCode.InternalServerError, "Errors parsing 'value'"); + TrangaSettings.UpdateCompressImages(value); + return new ValueTuple(HttpStatusCode.OK, null); + } + + private ValueTuple GetV2SettingsBwImages(GroupCollection groups, Dictionary requestParameters) + { + return new ValueTuple(HttpStatusCode.OK, TrangaSettings.bwImages); + } + + private ValueTuple PostV2SettingsBwImages(GroupCollection groups, Dictionary requestParameters) + { + if (!requestParameters.TryGetValue("value", out string? trueFalseStr) || + !bool.TryParse(trueFalseStr, out bool trueFalse)) + return new ValueTuple(HttpStatusCode.InternalServerError, "Errors parsing 'value'"); + TrangaSettings.UpdateBwImages(trueFalse); + return new ValueTuple(HttpStatusCode.OK, null); + } + + private ValueTuple PostV2SettingsDownloadLocation(GroupCollection groups, Dictionary requestParameters) + { + if (!requestParameters.TryGetValue("location", out string? folderPath)) + return new ValueTuple(HttpStatusCode.NotFound, "Missing Parameter 'location'"); + try + { + bool moveFiles = requestParameters.TryGetValue("moveFiles", out string? moveFilesStr) switch + { + false => true, + true => bool.Parse(moveFilesStr!) + }; + TrangaSettings.UpdateDownloadLocation(folderPath, moveFiles); + return new ValueTuple(HttpStatusCode.OK, null); + } + catch (FormatException) + { + return new ValueTuple(HttpStatusCode.InternalServerError, "Error Parsing Parameter 'moveFiles'"); + } + } +} \ No newline at end of file diff --git a/Tranga/Tranga.cs b/Tranga/Tranga.cs index 65ea58e..6d897cb 100644 --- a/Tranga/Tranga.cs +++ b/Tranga/Tranga.cs @@ -8,8 +8,7 @@ public partial class Tranga : GlobalBase { public bool keepRunning; public JobBoss jobBoss; - private Server _server; - private HashSet _connectors; + private Server.Server _server; public Tranga(Logger? logger) : base(logger) { @@ -33,7 +32,7 @@ public partial class Tranga : GlobalBase dir.Delete(); jobBoss = new(this, this._connectors); StartJobBoss(); - this._server = new Server(this); + this._server = new Server.Server(this); string[] emojis = { "(•‿•)", "(づ \u25d5‿\u25d5 )づ", "( \u02d8\u25bd\u02d8)っ\u2668", "=\uff3e\u25cf \u22cf \u25cf\uff3e=", "(ΦωΦ)", "(\u272a\u3268\u272a)", "( ノ・o・ )ノ", "(〜^\u2207^ )〜", "~(\u2267ω\u2266)~","૮ \u00b4• ﻌ \u00b4• ა", "(\u02c3ᆺ\u02c2)", "(=\ud83d\udf66 \u0f1d \ud83d\udf66=)"}; SendNotifications("Tranga Started", emojis[Random.Shared.Next(0,emojis.Length-1)]); Log(TrangaSettings.AsJObject().ToString()); @@ -53,9 +52,9 @@ public partial class Tranga : GlobalBase return connector is not null; } - public IEnumerable GetConnectors() + public List GetConnectors() { - return _connectors; + return _connectors.ToList(); } public Manga? GetPublicationById(string internalId) => GetCachedManga(internalId); diff --git a/Tranga/Tranga.csproj b/Tranga/Tranga.csproj index 1ca1c49..e7d5a27 100644 --- a/Tranga/Tranga.csproj +++ b/Tranga/Tranga.csproj @@ -12,8 +12,11 @@ + + + diff --git a/Tranga/TrangaSettings.cs b/Tranga/TrangaSettings.cs index ccfa997..dfa1590 100644 --- a/Tranga/TrangaSettings.cs +++ b/Tranga/TrangaSettings.cs @@ -17,11 +17,14 @@ public static class TrangaSettings public static string userAgent { get; private set; } = DefaultUserAgent; public static bool bufferLibraryUpdates { get; private set; } = false; public static bool bufferNotifications { get; private set; } = false; + public static int compression{ get; private set; } = 40; + public static bool bwImages { get; private set; } = false; [JsonIgnore] public static string settingsFilePath => Path.Join(workingDirectory, "settings.json"); [JsonIgnore] public static string libraryConnectorsFilePath => Path.Join(workingDirectory, "libraryConnectors.json"); [JsonIgnore] public static string notificationConnectorsFilePath => Path.Join(workingDirectory, "notificationConnectors.json"); [JsonIgnore] public static string jobsFolderPath => Path.Join(workingDirectory, "jobs"); [JsonIgnore] public static string coverImageCache => Path.Join(workingDirectory, "imageCache"); + [JsonIgnore] public static string mangaCacheFolderPath => Path.Join(workingDirectory, "mangaCache"); public static ushort? version { get; } = 2; public static bool aprilFoolsMode { get; private set; } = true; [JsonIgnore]internal static readonly Dictionary DefaultRequestLimits = new () @@ -48,7 +51,9 @@ public static class TrangaSettings ExportSettings(); } - public static void CreateOrUpdate(string? downloadDirectory = null, string? pWorkingDirectory = null, int? pApiPortNumber = null, string? pUserAgent = null, bool? pAprilFoolsMode = null, bool? pBufferLibraryUpdates = null, bool? pBufferNotifications = null) + public static void CreateOrUpdate(string? downloadDirectory = null, string? pWorkingDirectory = null, + int? pApiPortNumber = null, string? pUserAgent = null, bool? pAprilFoolsMode = null, + bool? pBufferLibraryUpdates = null, bool? pBufferNotifications = null, int? pCompression = null, bool? pbwImages = null) { if(pWorkingDirectory is null && File.Exists(settingsFilePath)) LoadFromWorkingDirectory(workingDirectory); @@ -59,6 +64,8 @@ public static class TrangaSettings aprilFoolsMode = pAprilFoolsMode ?? aprilFoolsMode; bufferLibraryUpdates = pBufferLibraryUpdates ?? bufferLibraryUpdates; bufferNotifications = pBufferNotifications ?? bufferNotifications; + compression = pCompression ?? compression; + bwImages = pbwImages ?? bwImages; Directory.CreateDirectory(downloadLocation); Directory.CreateDirectory(workingDirectory); ExportSettings(); @@ -98,33 +105,67 @@ public static class TrangaSettings ExportSettings(); } + public static void UpdateCompressImages(int value) + { + compression = int.Clamp(value, 1, 100); + ExportSettings(); + } + + public static void UpdateBwImages(bool enabled) + { + bwImages = enabled; + ExportSettings(); + } + public static void UpdateDownloadLocation(string newPath, bool moveFiles = true) { if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) - Directory.CreateDirectory(newPath, - GroupRead | GroupWrite | None | OtherRead | OtherWrite | UserRead | UserWrite); + Directory.CreateDirectory(newPath, GroupRead | GroupWrite | None | OtherRead | OtherWrite | UserRead | UserWrite); else Directory.CreateDirectory(newPath); - if (moveFiles && Directory.Exists(downloadLocation)) - Directory.Move(downloadLocation, newPath); - - downloadLocation = newPath; + if (moveFiles) + MoveContentsOfDirectoryTo(TrangaSettings.downloadLocation, newPath); + + TrangaSettings.downloadLocation = newPath; ExportSettings(); } public static void UpdateWorkingDirectory(string newPath) { if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) - Directory.CreateDirectory(newPath, - GroupRead | GroupWrite | None | OtherRead | OtherWrite | UserRead | UserWrite); + Directory.CreateDirectory(newPath, GroupRead | GroupWrite | None | OtherRead | OtherWrite | UserRead | UserWrite); else Directory.CreateDirectory(newPath); - Directory.Move(workingDirectory, newPath); - workingDirectory = newPath; + + MoveContentsOfDirectoryTo(TrangaSettings.workingDirectory, newPath); + + TrangaSettings.workingDirectory = newPath; ExportSettings(); } + private static void MoveContentsOfDirectoryTo(string oldDir, string newDir) + { + string[] directoryPaths = Directory.GetDirectories(oldDir); + string[] filePaths = Directory.GetFiles(oldDir); + foreach (string file in filePaths) + { + string newPath = Path.Join(newDir, Path.GetFileName(file)); + File.Move(file, newPath, true); + } + foreach(string directory in directoryPaths) + { + string? dirName = Path.GetDirectoryName(directory); + if(dirName is null) + continue; + string newPath = Path.Join(newDir, dirName); + if(Directory.Exists(newPath)) + MoveContentsOfDirectoryTo(directory, newPath); + else + Directory.Move(directory, newPath); + } + } + public static void UpdateUserAgent(string? customUserAgent) { userAgent = customUserAgent ?? DefaultUserAgent; @@ -167,6 +208,8 @@ public static class TrangaSettings jobj.Add("requestLimits", JToken.FromObject(requestLimits)); jobj.Add("bufferLibraryUpdates", JToken.FromObject(bufferLibraryUpdates)); jobj.Add("bufferNotifications", JToken.FromObject(bufferNotifications)); + jobj.Add("compression", JToken.FromObject(compression)); + jobj.Add("bwImages", JToken.FromObject(bwImages)); return jobj; } @@ -191,5 +234,9 @@ public static class TrangaSettings bufferLibraryUpdates = blu.Value()!; if (jobj.TryGetValue("bufferNotifications", out JToken? bn)) bufferNotifications = bn.Value()!; + if (jobj.TryGetValue("compression", out JToken? ci)) + compression = ci.Value()!; + if (jobj.TryGetValue("bwImages", out JToken? bwi)) + bwImages = bwi.Value()!; } } \ No newline at end of file diff --git a/docs/API_Calls.md b/docs/API_Calls.md new file mode 100644 index 0000000..59e10e6 --- /dev/null +++ b/docs/API_Calls.md @@ -0,0 +1,383 @@ +## Tranga API Calls +This document serves to outline all of the different HTTP API calls that Tranga accepts. Tranga expects specific HTTP methods for its calls and therefore careful attention must be paid when making them. +In the examples below, `{apiUri}` refers to your `http(s)://TRANGA.FRONTEND.URI/api`. Parameters are included in the HTTP request URI and the request body is in JSON format. Tranga responses are always +in the JSON format within the Response Body. + +#### [GET] /Connectors +Retrieves the available manga sites (connectors) that Tranga is currently able to download manga from. + +- Parameters: + None + +- Request Body: + None + +#### [GET] /Jobs +Retrieves all jobs that Tranga is keeping track of, includes Running Jobs, Waiting Jobs, Manga Tracking (Monitoring) Jobs. + +- Parameters: + None + +- Request Body: + None + +#### [DELETE] /Jobs +Removes the specified job given by the job ID + +- Request Variables: + - None + +- Request Body: + ``` + { + jobId: ${Tranga Job ID} + } + ``` + +#### [POST] /Jobs/Cancel +Cancels a running job or prevents a queued job from running. + +- Parameters: + None + +- Request Body: + ``` + { + jobId: ${Tranga Job ID} + } + ``` + +#### [POST] /Jobs/DownloadNewChapters +Manually adds a Job to Tranga's queue to check for and download new chapters for a specified manga + +- Parameters: + None + +- Request Body: + ``` + { + connector: ${Manga Connector to Download From} + internalId: ${Tranga Manga ID} + translatedLanguage: ${Manga Language} + } + ``` + +#### [GET] /Jobs/Running +Retrieves all currently running jobs. + +- Parameters: + None + +- Request Body: + None + +#### [POST] /Jobs/StartNow +Manually starts a configured job +- Parameters: + None + +- Request Body: + ``` + { + jobId: ${Tranga Job ID} + } + ``` + +#### [GET]/Jobs/Waiting +Retrieves all currently queued jobs. + +- Parameters: + None + +- Request Body: + None + +#### [GET] /Jobs/MonitorJobs +Retrieves all jobs for Mangas that Tranga is currently tracking. + +- Parameters: + None + +- Request Body: + None + +#### [POST] /Jobs/MonitorManga +Adds a new manga for Tranga to monitor + +- Parameters: + None + +- Request Body: + ``` + { + connector: ${Manga Connector to download from} + internalId: ${Tranga Manga ID} + interval: ${Interval at which to run job, in the HH:MM:SS format} + translatedLanguage: ${Supported language code} + ignoreBelowChapterNum: ${Chapter number to start downloading from} + customFolderName: ${Folder Name to save Manga to} + } + ``` + +#### [GET] /Jobs/Progress +Retrieves the current completion progress of a running or waiting job. Tranga's ID for the Job is returned with each of the `GET /Job/` API calls. + +- Parameters: + - `{jobId}`: Tranga Job ID + +- Request Body: + None + +#### [POST] /Jobs/UpdateMetadata +Updates the metadata for all monitored mangas + +- Parameters: + None + +- Request Body: + None + +#### [GET] /LibraryConnectors +Retrieves the currently configured library servers + +- Parameters: + None + +- Request Body: + None + +#### [DELETE] /LibraryConnectors/Reset +Resets or clears a configured library connector + +- Parameters: + None + +- Request Body: + ``` + { + libraryConnector: Komga/Kavita + } + ``` + +#### [POST] /LibraryConnectors/Test +Verifies the behavior of a library connector before saving it. The connector must be checked to verify that the connection is active. + + +- Parameters: + None + +- Request Body: + ``` + { + libraryConnector: Komga/Kavita + libraryURL: ${Library URL} + komgaAuth: Only for when libraryConnector = Komga + kavitaUsername: Only for when libraryConnector = Kavita + kavitaPassword: Only for when libraryConnector = Kavita + } + ``` + +#### [GET] /LibraryConnectors/Types +Retrives Key-Value pairs for all of Tranga's currently supported library servers. + +- Parameters: + None + +- Request Body: + None + +#### [POST] /LibraryConnectors/Update +Updates or Adds a Library Connector to Tranga + +- Parameters: None + +- Request Body: + ``` + { + libraryConnector: Komga/Kavita + libraryURL: ${Library URL} + komgaAuth: Only for when libraryConnector = Komga + kavitaUsername: Only for when libraryConnector = Kavita + kavitaPassword: Only for when libraryConnector = Kavita + } + ``` + +#### [GET] /LogFile +Retrieves the log file from the running Tranga instance + +- Parameters: + None + +- Request Body: + None + +#### [GET] /Manga/FromConnector +Retrieves the details about a specified manga from a specific connector. If the manga title returned by Tranga is a URL (determined by the presence of `http` in the title, the API call should use the second +call with the `url` rather than the `title`. + +- Parameters: + - `{connector}`: Manga Connector + - `{url/title}`: Manga URL/Title + +- Request Body: + None + +#### [GET] /Manga/Chapters +Retrieves the currently available chapters for a specified manga from a connector. The `{internalId}` is how Tranga uniquely recognizes and distinguishes different Manga. + +- Parameters: + - `{connector}`: Manga Connector + - `{internalId}`: Tranga Manga ID + - `{translatedLanguage}`: Translated Language + +- Request Body: + None + +#### [GET] /Manga/Cover +Retrives the URL of the cover image for a specific manga that Tranga is tracking. + +- Parameters: + - `{internalId}`: Tranga Manga ID + +- Request Body: + None + +#### [GET] /NotificationConnectors +Retrieves the currently configured notification providers + +- Parameters: + None + +- Request Body: + None + +#### [DELETE] /NotificationConnectors/Reset +Resets or clears a configured notification connector + +- Parameters: + None + +- Request Body: + ``` + { + notificationConnector: Gotify/Ntfy/LunaSea + } + ``` + +#### [POST] /NotificationConnectors/Test +Tests a notification connector with the currently input settings. The connector behavior must be checked to verify that the input settings are correct. + +- Parameters: + None + +- Request Body: + ``` + { + notificationConnector: Gotify/Ntfy/LunaSea + + gotifyUrl: + gotifyAppToken: + + lunaseaWebhook: + + ntfyUrl: + ntfyAuth: + } + ``` + +#### [POST] /NotificationConnectors/Update +Updates or Adds a notification connector to Tranga + +- Parameters: + None + +- Request Body: + ``` + { + notificationConnector: Gotify/Ntfy/LunaSea + + gotifyUrl: + gotifyAppToken: + + lunaseaWebhook: + + ntfyUrl: + ntfyAuth: + } + ``` + +#### [GET] /NotificationConnectors/Types +Retrives Key-Value pairs for all of Tranga's currently supported notification providers. + +- Parameters: + None + +- Request Body: + None + +#### [GET] /Ping +This call is used periodically by the web frontend to establish that connection to the server is active. + +- Parameters: + None + +- Request Body: + None + +#### [GET] /Settings +Retrieves the content of Tranga's `settings.json` + +- Parameters: + None + +- Request Body: + None + +#### [GET] /Settings/customRequestLimit +Retrieves the configured rate limits for different types of manga connector requests. + +- Parameters: + None + +- Request Body: + None + +#### [POST] /Settings/customRequestLimit +Sets the rate limits for different types of manga connector requests. + +- Parameters: + None + +- Request Body: + ``` + { + requestType: {Request Byte} + requestsPerMinute: {Rate Limit in Requests Per Minute} + } + ``` + +#### [POST] /Settings/UpdateDownloadLocation +Updates the root directory of where Tranga downloads manga + +- Parameters: + None + +- Request Body: + ``` + { + downloadLocation: {New Root Directory} + moveFiles: "true"/"false" + } + ``` +#### [POST] /Settings/userAgent +Updates the user agent that Tranga uses when scraping the web + +- Parameters + +- Request Body: + ``` + { + userAgent: {User Agent String} + } + ``` + diff --git a/docs/API_Calls_v2.md b/docs/API_Calls_v2.md new file mode 100644 index 0000000..e47bcd4 --- /dev/null +++ b/docs/API_Calls_v2.md @@ -0,0 +1,1104 @@ + +# Tranga API Calls v2 +This document outlines all different HTTP API calls that Tranga accepts. +Tranga expects specific HTTP methods for its calls and therefore careful attention must be paid when making them. + +`apiUri` refers to your `http(s)://TRANGA.FRONTEND.URI/api`. + +Parameters are included in the HTTP request URI and/or the request body. +The request Body is in JSON key-value-pair format, with all values as strings. +Tranga responses are always in the JSON format within the Response Body. + +Parameters in *italics* are optional + + + + + +### Quick Entry + +* [Connectors](#connectors-top) +* [Manga](#manga-top) +* [Jobs](#jobs-top) +* [Settings](#settings-top) +* [Library Connectors](#library-connectors-top) +* [Notification Connectors](#notification-connectors-top) +* [Miscellaneous](#miscellaneous-top) + +## Connectors [^top](#top) + +### ![GET](https://img.shields.io/badge/GET-0f0) `/v2/Connector/Types` + +Returns available Manga Connectors (Scanlation sites) + +
+ Returns + + List of [Connectors](Types.md#Connector) +
+ +### ![GET](https://img.shields.io/badge/GET-0f0) `/v2/Connector//GetManga` + +Returns the Manga from the specified Manga Connector. + +
+ Request + + `ConnectorName` is returned in the response of [GET /v2/Connector/Types](#-v2connectortypes) + + Use either `title` or `url` Parameter. + + | Parameter | Value | + |-----------|-------------------------------------------------| + | title | Search Term | + | url | Direct link (URL) to the Manga on the used Site | +
+ +
+ Returns + + List of [Manga](Types.md#Manga) + + | StatusCode | Meaning | + |------------|--------------------------| + | 400 | Connector does not exist | + | 404 | URL/Connector Mismatch | +
+ +## Manga [^top](#top) + +### ![GET](https://img.shields.io/badge/GET-0f0) `/v2/Mangas` + +Returns all known Manga. + +
+ Returns + + List of internalIds. +
+ +### ![GET](https://img.shields.io/badge/GET-0f0) `/v2/Manga/Search` + +Initiates a search for a Manga on all Connectors. + +
+ Request + + + | Parameter | Value | + |-----------|-------------------------------------------------| + | title | Search Term | +
+ +
+ Returns + + List of [Manga](Types.md#Manga) + + | StatusCode | Meaning | + |------------|-------------------| + | 400 | Parameter missing | +
+ +### ![GET](https://img.shields.io/badge/GET-0f0) `/v2/Manga/` + +Returns the specified Manga. + +
+ Request + + `internalId` is returned in the response of + * [GET /v2/Manga](#-v2manga) + * [GET /v2/Connector/*ConnectorName*/GetManga](#-v2connectorconnectornamegetmanga) + * [GET /v2/Job/*jobId*](#-v2jobjobid) +
+ +
+ Returns + + [Manga](Types.md#manga) + + | StatusCode | Meaning | + |------------|--------------------------------------------| + | 404 | Manga with `internalId` could not be found | +
+ +### ![GET](https://img.shields.io/badge/GET-0f0) `/v2/Manga/` + +Returns the list of Mangas requested. + +
+ Request + + | Parameter | Value | + |-----------|--------------------------------------| + | mangaIds | Comma-Seperated list of `internalId` | + + `internalId` is returned in the response of + * [GET /v2/Manga](#-v2manga) + * [GET /v2/Connector/*ConnectorName*/GetManga](#-v2connectorconnectornamegetmanga) + * [GET /v2/Job/*jobId*](#-v2jobjobid) +
+ +
+ Returns + + List of [Manga](Types.md#manga) + + | StatusCode | Meaning | + |------------|--------------------------------------------| + | 400 | Missing Parameter | + | 404 | Manga with `internalId` could not be found | +
+ +### ![DELETE](https://img.shields.io/badge/DELETE-f00) `/v2/Manga/` + +Deletes all associated Jobs for the specified Manga + +
+ Request + + `internalId` is returned in the response of + * [GET /v2/Manga](#-v2manga) + * [GET /v2/Connector/*ConnectorName*/GetManga](#-v2connectorconnectornamegetmanga) + * [GET /v2/Job/*jobId*](#-v2jobjobid) +
+ +
+ Returns + + [Manga](Types.md#manga) + + | StatusCode | Meaning | + |------------|--------------------------------------------| + | 200 | Manga was deleted | + | 404 | Manga with `internalId` could not be found | +
+ +### ![GET](https://img.shields.io/badge/GET-0f0) `/v2/Manga//Cover` + +Returns the URL for the Cover of the specified Manga. + +
+ Request + + `internalId` is returned in the response of + * [GET /v2/Manga](#-v2manga) + * [GET /v2/Connector/*ConnectorName*/GetManga](#-v2connectorconnectornamegetmanga) + * [GET /v2/Job/*jobId*](#-v2jobjobid) + + Optional: `dimensions=x` replace width and height with requested dimensions in pixels. + Fitting will cover requested area. +
+ +
+ Returns + +String with the url. +If `dimensions=x` was not requested, returns full sized image. + + | StatusCode | Meaning | + |------------|--------------------------------------------| + | 404 | Manga with `internalId` could not be found | +
+ +### ![GET](https://img.shields.io/badge/GET-0f0) `/v2/Manga//Chapters` + +Returns the Chapter-list for the specified Manga. + +
+ Request + + `internalId` is returned in the response of + * [GET /v2/Manga](#-v2manga) + * [GET /v2/Connector/*ConnectorName*/GetManga](#-v2connectorconnectornamegetmanga) + * [GET /v2/Job/*jobId*](#-v2jobjobid) + + | Parameter | Value | + |------------|------------------------| + | *language* | Language to search for | +
+ +
+ Returns + + List of [Chapters](Types.md/#chapter) + + | StatusCode | Meaning | + |------------|--------------------------------------------| + | 404 | Manga with `internalId` could not be found | +
+ +### ![GET](https://img.shields.io/badge/GET-0f0) `/v2/Manga//Chapters/latest` + +Returns the latest Chapter of the specified Manga. + +
+ Request + + `internalId` is returned in the response of + * [GET /v2/Manga](#-v2manga) + * [GET /v2/Connector/*ConnectorName*/GetManga](#-v2connectorconnectornamegetmanga) + * [GET /v2/Job/*jobId*](#-v2jobjobid) +
+ +
+ Returns + + [Chapter](Types.md/#chapter) + + | StatusCode | Meaning | + |------------|--------------------------------------------| + | 404 | Manga with `internalId` could not be found | +
+ +### ![POST](https://img.shields.io/badge/POST-00f) `/v2/Manga//ignoreChaptersBelow` + +
+ Request + + `internalId` is returned in the response of + * [GET /v2/Manga](#-v2manga) + * [GET /v2/Connector/*ConnectorName*/GetManga](#-v2connectorconnectornamegetmanga) + * [GET /v2/Job/*jobId*](#-v2jobjobid) + + | Parameter | Value | + |--------------|----------------------------| + | startChapter | Chapter-number to start at | +
+ +
+ Returns + + | StatusCode | Meaning | + |------------|--------------------------------------------| + | 404 | Manga with `internalId` could not be found | + | 500 | Parsing Error | +
+ +### ![POST](https://img.shields.io/badge/POST-00f) `/v2/Manga//moveFolder` + +
+ Request + + `internalId` is returned in the response of + * [GET /v2/Manga](#-v2manga) + * [GET /v2/Connector/*ConnectorName*/GetManga](#-v2connectorconnectornamegetmanga) + * [GET /v2/Job/*jobId*](#-v2jobjobid) + + | Parameter | Value | + |-----------|-------------------------------------| + | location | New location (relative to root dir) | + +
+ +
+ Returns + + | StatusCode | Meaning | + |------------|--------------------------------------------| + | 400 | Parameter missing | + | 404 | Manga with `internalId` could not be found | +
+ +## Jobs [^top](#top) + +### ![GET](https://img.shields.io/badge/GET-0f0) `/v2/Jobs` + +Returns all configured Jobs as IDs. + +
+ Returns + + List of JobIds. +
+ +### ![GET](https://img.shields.io/badge/GET-0f0) `/v2/Jobs/Running` + +Returns all Running Jobs as IDs. + +
+ Returns + + List of JobIds. +
+ +### ![GET](https://img.shields.io/badge/GET-0f0) `/v2/Jobs/Waiting` + +Returns all Waiting Jobs as IDs. + +
+ Returns + + List of JobIds. +
+ +### ![GET](https://img.shields.io/badge/GET-0f0) `/v2/Jobs/Standby` + +Returns all Standby Jobs as IDs. + +
+ Returns + +List of JobIds. +
+ +### ![GET](https://img.shields.io/badge/GET-0f0) `/v2/Jobs/Monitoring` + +Returns all Monitoring Jobs as IDs. + +
+ Returns + + List of JobIds. +
+ +### ![GET](https://img.shields.io/badge/GET-0f0) `/v2/Job/Types` + +Returns the valid Job-Types. + +
+ Returns + + List of strings. +
+ +### ![POST](https://img.shields.io/badge/POST-00f) `/v2/Job/Create/` + +Creates a Job. + +
+ Request + + `Type` is returned in the response of [GET /v2/Job/Types](#-v2jobtypes) + + | Parameter | Value | + |----------------|---------------------------------------------------------------------------------------------------| + | internalId | Manga ID | + | *customFolder* | Custom folder location
Only for MonitorManga, DownloadNewChapters and DownloadChapter | + | *startChapter* | Chapter to start downloading at
Only for MonitorManga, DownloadNewChapters | + | *interval* | Interval at which the Job is re-run in HH:MM:SS format
Only for MonitorManga, UpdateMetadata | + | *language* | Translated language
Only for MonitorManga, DownloadNewChapters and DownloadChapter | + + + `internalId` is returned in the response of + * [GET /v2/Manga](#-v2manga) + * [GET /v2/Connector/*ConnectorName*/GetManga](#-v2connectorconnectornamegetmanga) + * [GET /v2/Job/*jobId*](#-v2jobjobid) +
+ +
+ Returns + + [Job](Types.md#job) + + | StatusCode | Meaning | + |------------|------------------------------------------| + | 200 | Job created. | + | 404 | Parameter missing or could not be found. | + | 409 | Job already exists | + | 500 | Error parsing interval | +
+ +### ![GET](https://img.shields.io/badge/GET-0f0) `/v2/Job/` + +Returns the list of Jobs requested. + +
+ Request + + | Parameter | Value | + |------------|--------------------------------| + | jobIds | Comma-Seperated list of jobIds | + + `jobId` is returned in the response of + * [GET /v2/Jobs](#-v2jobs) + * [GET /v2/Jobs/Running](#-v2jobsrunning) + * [GET /v2/Jobs/Waiting](#-v2jobswaiting) + * [GET /v2/Jobs/Monitoring](#-v2jobsmonitoring) +
+ +
+ Returns + + List of [Jobs](Types.md#job) + + | StatusCode | Meaning | + |------------|-------------------------------------| + | 400 | Missing Parameter | + | 404 | Job with `jobId` could not be found | +
+ +### ![GET](https://img.shields.io/badge/GET-0f0) `/v2/Job/` + +Returns the specified Job. + +
+ Request + + `jobId` is returned in the response of + * [GET /v2/Jobs](#-v2jobs) + * [GET /v2/Jobs/Running](#-v2jobsrunning) + * [GET /v2/Jobs/Waiting](#-v2jobswaiting) + * [GET /v2/Jobs/Monitoring](#-v2jobsmonitoring) +
+ +
+ Returns + + [Job](Types.md#job) + + | StatusCode | Meaning | + |------------|---------------------------------------| + | 404 | Manga with `jobId` could not be found | +
+ +### ![DELETE](https://img.shields.io/badge/DELETE-f00) `/v2/Job/` + +Deletes the specified Job and all descendants. + +
+ Request + + `jobId` is returned in the response of + * [GET /v2/Jobs](#-v2jobs) + * [GET /v2/Jobs/Running](#-v2jobsrunning) + * [GET /v2/Jobs/Waiting](#-v2jobswaiting) + * [GET /v2/Jobs/Monitoring](#-v2jobsmonitoring) +
+ +
+ Returns + + | StatusCode | Meaning | + |------------|---------------------------------------| + | 200 | Job deleted | + | 404 | Manga with `jobId` could not be found | +
+ +### ![GET](https://img.shields.io/badge/GET-0f0) `/v2/Job//Progress` + +Returns the progress the of the specified Job. + +
+ Request + + `jobId` is returned in the response of + * [GET /v2/Jobs](#-v2jobs) + * [GET /v2/Jobs/Running](#-v2jobsrunning) + * [GET /v2/Jobs/Waiting](#-v2jobswaiting) + * [GET /v2/Jobs/Monitoring](#-v2jobsmonitoring) +
+ +
+ Returns + + [ProgressToken](Types.md#progresstoken) + + | StatusCode | Meaning | + |------------|---------------------------------------| + | 404 | Manga with `jobId` could not be found | +
+ +### ![POST](https://img.shields.io/badge/POST-00f) `/v2/Job//StartNow` + +Starts the specified Job. + +
+ Request + + `jobId` is returned in the response of + * [GET /v2/Jobs](#-v2jobs) + * [GET /v2/Jobs/Running](#-v2jobsrunning) + * [GET /v2/Jobs/Waiting](#-v2jobswaiting) + * [GET /v2/Jobs/Monitoring](#-v2jobsmonitoring) +
+ +
+ Returns + + | StatusCode | Meaning | + |------------|---------------------------------------| + | 200 | Job started | + | 404 | Manga with `jobId` could not be found | +
+ +### ![POST](https://img.shields.io/badge/POST-00f) `/v2/Job//Cancel` + +Cancels the specified Job, or dequeues it. + +
+ Request + + `jobId` is returned in the response of + * [GET /v2/Jobs](#-v2jobs) + * [GET /v2/Jobs/Running](#-v2jobsrunning) + * [GET /v2/Jobs/Waiting](#-v2jobswaiting) + * [GET /v2/Jobs/Monitoring](#-v2jobsmonitoring) +
+ +
+ Returns + + | StatusCode | Meaning | + |------------|---------------------------------------| + | 200 | Job cancelled | + | 404 | Manga with `jobId` could not be found | +
+ +### ![POST](https://img.shields.io/badge/POST-00f) `/v2/Job//SetInterval` NOT YET IMPLEMENTED + +Edits the specified Job. + +
+ Request + + `jobId` is returned in the response of + * [GET /v2/Jobs](#-v2jobs) + * [GET /v2/Jobs/Running](#-v2jobsrunning) + * [GET /v2/Jobs/Waiting](#-v2jobswaiting) + * [GET /v2/Jobs/Monitoring](#-v2jobsmonitoring) +
+ +
+ Returns + + | StatusCode | Meaning | + |------------|---------------------------------------| + | 404 | Manga with `jobId` could not be found | +
+ +## Settings [^top](#top) + +### ![GET](https://img.shields.io/badge/GET-0f0) `/v2/Settings` + +Returns the `settings.json` file. + +
+ Returns + + [Settings](Types.md#settings) +
+ +### ![GET](https://img.shields.io/badge/GET-0f0) `/v2/Settings/UserAgent` + +Returns the current User Agent used for Requests. + +
+ Returns + + [UserAgent](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/User-Agent) +
+ +### ![POST](https://img.shields.io/badge/POST-00f) `/v2/Settings/UserAgent` + +Sets the User Agent. If left empty, User Agent is reset to default. + +
+ Request + +| Parameter | Value | +|-----------|----------------------------------------------------------------------------------------| +| value | New [UserAgent](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/User-Agent) | +
+ +
+ Returns + +| StatusCode | Meaning | +|------------|-------------------| +| 202 | UserAgent Reset | +| 201 | UserAgent Updated | + +
+ +### ![GET](https://img.shields.io/badge/GET-0f0) `/v2/Settings/RateLimit/Types` + +Returns the configurable Rate-Limits. + +
+ Returns + + Key-Value-Pairs of Values and RateLimit-Names. +
+ +### ![GET](https://img.shields.io/badge/GET-0f0) `/v2/Settings/RateLimit` + +Returns the current configuration of Rate-Limits for Requests. + +
+ Returns + + Dictionary of `Rate-Limits` and `Requests per Minute` +
+ +### ![POST](https://img.shields.io/badge/POST-00f) `/v2/Settings/RateLimit` + +Sets the Rate-Limits for all Requests. If left empty, resets to default Rate-Limits. + +
+ Request + + For each Rate-Limit set as follows: + + | Parameter | Value | + |--------------------------------------|-----------------------| + | [Type](#-v2settingsratelimittypes) | Requests per Minute | + + `Type` is returned by [GET /v2/Settings/RateLimit/Types](#-v2settingsratelimittypes) and should be supplied as string +
+ +
+ Returns + + | StatusCode | Meaning | + |------------|------------------------------------------------| + | 500 | Error parsing RequestType or RequestsPerMinute | +
+ +### ![GET](https://img.shields.io/badge/GET-0f0) `/v2/Settings/RateLimit/` + +Returns the current Rate-Limit for the Request-Type. + +
+ Request + + `Type` is returned by [GET /v2/Settings/RateLimit/Types](#-v2settingsratelimittypes) +
+ +
+ Returns + + Integer with Requests per Minute. + +| StatusCode | Meaning | +|------------|-----------------------------------------------| +| 404 | Error parsing RequestType | +
+ +### ![POST](https://img.shields.io/badge/POST-00f) `/v2/Settings/RateLimit/` + +Sets the Rate-Limit for the Request-Type in Requests per Minute. + +
+ Request + + `Type` is returned by [GET /v2/Settings/RateLimit/Types](#-v2settingsratelimittypes) + + | Parameter | Value | + |-----------|---------------------| + | value | Requests per Minute | +
+
+ Returns + + | StatusCode | Meaning | + |------------|--------------------------------| + | 404 | Rate-Limit-Name does not exist | + | 500 | Parsing Error | +
+ +### ![GET](https://img.shields.io/badge/GET-0f0) `/v2/Settings/AprilFoolsMode` + +Returns the current state of the April-Fools-Mode setting. + +
+ Returns + + Boolean +
+ +### ![POST](https://img.shields.io/badge/POST-00f) `/v2/Settings/AprilFoolsMode` + +Enables/Disables April-Fools-Mode. + +
+ Request + + | Parameter | Value | + |-----------|------------| + | value | true/false | +
+ +
+ Returns + + | StatusCode | Meaning | + |------------|--------------------------------| + | 500 | Parsing Error | +
+ +### ![GET](https://img.shields.io/badge/GET-0f0) `/v2/Settings/CompressImages` + +Returns the current state of the Image-compression setting. + +
+ Returns + + number +
+ +### ![POST](https://img.shields.io/badge/POST-00f) `/v2/Settings/CompressImages` + +Set the quality of the compression. + +
+ Request + + | Parameter | Value | + |-----------|-------| + | value | 1-100 | +
+ +
+ Returns + + | StatusCode | Meaning | + |------------|--------------------------------| + | 500 | Parsing Error | +
+ +### ![GET](https://img.shields.io/badge/GET-0f0) `/v2/Settings/BWImages` + +Returns the current state of the Black/White Image setting. + +
+ Returns + +Boolean +
+ +### ![POST](https://img.shields.io/badge/POST-00f) `/v2/Settings/BWImages` + +Enables/Disables converting Images to Black and White. + +
+ Request + + | Parameter | Value | + |-----------|------------| + | value | true/false | +
+ +
+ Returns + +| StatusCode | Meaning | + |------------|--------------------------------| +| 500 | Parsing Error | +
+ + +### ![POST](https://img.shields.io/badge/POST-00f) `/v2/Settings/DownloadLocation` + +Updates the default Download-Location. + +
+ Request + + | Parameter | Value | + |-------------|------------------| + | location | New Folder-Path | + | *moveFiles* | __*true*__/false | +
+ +
+ Returns + + + | StatusCode | Meaning | + |------------|---------------------------------| + | 200 | Successfully changed | + | 404 | Parameter 'location' is missing | + | 500 | Parsing Error | +
+ +## Library Connectors [^top](#top) + +### ![GET](https://img.shields.io/badge/GET-0f0) `/v2/LibraryConnector` + +Returns the configured Library-Connectors. + +
+ Returns + + List of [LibraryConnectors](Types.md#libraryconnector) +
+ +### ![GET](https://img.shields.io/badge/GET-0f0) `/v2/LibraryConnector/Types` + +Returns the available Library-Connector types. + +
+ Returns + + List of String of Names. +
+ +### ![GET](https://img.shields.io/badge/GET-0f0) `/v2/LibraryConnector/` + +Returns the Library-Connector for the specified Type. + +
+ Request + + `Type` is returned by [GET /v2/LibraryConnector/Types](#-v2libraryconnectortypes) +
+ +
+ Returns + + [LibraryConnector](Types.md#libraryconnector) + + | StatusCode | Meaning | + |------------|----------------------------------------------------| + | 404 | Library Connector of specified Type does not exist | +
+ +### ![POST](https://img.shields.io/badge/POST-00f) `/v2/LibraryConnector/` + +Creates a Library-Connector of the specified Type. + +
+ Request + + `Type` is returned by [GET /v2/LibraryConnector/Types](#-v2libraryconnectortypes) + + | Parameter | Value | + |-----------|--------------------| + | url | URL of the Library | + | username | Username | + | password | Password | +
+ +
+ Returns + + [LibraryConnector](Types.md#libraryconnector) + + | StatusCode | Meaning | + |------------|----------------------------------| + | 404 | Library Connector does not exist | + | 406 | Missing Parameter | + | 500 | Parsing Error | +
+ +### ![POST](https://img.shields.io/badge/POST-00f) `/v2/LibraryConnector//Test` + +Tests a Library-Connector of the specified Type. + +
+ Request + + `Type` is returned by [GET /v2/LibraryConnector/Types](#-v2libraryconnectortypes) + + | Parameter | Value | + |-----------|--------------------| + | url | URL of the Library | + | username | Username | + | password | Password | +
+ +
+ Returns + + | StatusCode | Meaning | + |------------|------------------------------------| + | 200 | Test successful | + | 404 | Library Connector does not exist | + | 406 | Missing Parameter | + | 424 | Test failed | + | 500 | Parsing Error | +
+ +### ![DELETE](https://img.shields.io/badge/DELETE-f00) `/v2/LibraryConnector/` + +Deletes the Library-Connector of the specified Type. + +
+ Request + + `Type` is returned by [GET /v2/LibraryConnector/Types](#-v2libraryconnectortypes) +
+ +
+ Returns + + | StatusCode | Meaning | + |------------|---------------------------------------| + | 200 | Deleted | + | 404 | Library Connector Type does not exist | +
+ +## Notification Connectors [^top](#top) + +### ![GET](https://img.shields.io/badge/GET-0f0) `/v2/NotificationConnector` + +Returns the configured Notification-Connectors. + +
+ Returns + + List of [NotificationConnectors](Types.md#notificationconnector) +
+ +### ![GET](https://img.shields.io/badge/GET-0f0) `/v2/NotificationConnector/Types` + +Returns the available Notification-Connectors. + +
+ Returns + + List of String of Names. +
+ +### ![GET](https://img.shields.io/badge/GET-0f0) `/v2/NotificationConnector/` + +Returns the configured Notification-Connector of the specified Type. + +
+ Request + + `Type` is returned by [GET /v2/NotificationConnector/Types](#-v2notificationconnectortypes) +
+ +
+ Returns + + [Notification Connector](Types.md#notificationconnector) + + | StatusCode | Meaning | + |------------|---------------------------------------| + | 404 | Library Connector Type does not exist | + | 500 | Parsing Error | +
+ +### ![POST](https://img.shields.io/badge/POST-00f) `/v2/NotificationConnector/` + +Creates a Notification-Connector of the specified Type. + +
+ Request + + `Type` is returned by [GET /v2/NotificationConnector/Types](-v2notificationconnectortypes) + + #### Type specific Parameters (must be included for each) + * Gotify + + | Parameter | Value | + |-----------|---------------------------------------| + | url | URL of the Gotify Instance | + | appToken | AppToken of the configured Gotify App | + + * LunaSea + + | Parameter | Value | + |-----------|-----------------| + | webhook | LunaSea Webhook | + + * Nty + + | Parameter | Value | + |-----------|--------------------------| + | url | URL of the Ntfy Instance | + | username | Username | + | password | Password | +
+ +
+ Returns + + [NotificationConnector](Types.md#notificationconnector) + + | StatusCode | Meaning | + |------------|--------------------------------------------| + | 404 | Notification Connector Type does not exist | + | 406 | Missing Parameter | + | 500 | Parsing Error | +
+ +### ![POST](https://img.shields.io/badge/POST-00f) `/v2/NotificationConnector//Test` + +Tests a Notification-Connector of the specified Type. + +
+ Request + + `Type` is returned by [GET /v2/NotificationConnector/Types](#-v2notificationconnectortypes) + + #### Type specific Parameters (must be included for each) + * Gotify + + | Parameter | Value | + |-----------|---------------------------------------| + | url | URL of the Gotify Instance | + | appToken | AppToken of the configured Gotify App | + + * LunaSea + + | Parameter | Value | + |-----------|-----------------| + | webhook | LunaSea Webhook | + + * Ntfy + + | Parameter | Value | + |-----------|--------------------------| + | url | URL of the Ntfy Instance | + | username | Username | + | password | Password | +
+ +
+ Returns + + | StatusCode | Meaning | + |------------|--------------------------------------------| + | 200 | Test successful | + | 404 | Notification Connector Type does not exist | + | 406 | Missing Parameter | + | 500 | Parsing Error | +
+ +### ![DELETE](https://img.shields.io/badge/DELETE-f00) `/v2/NotificationConnector/` + +Deletes the Notification-Connector of the specified Type. + +
+ Request + + `Type` is returned by [GET /v2/NotificationConnector/Types](#-v2notificationconnectortypes) +
+ +
+ Returns + + | StatusCode | Meaning | + |------------|--------------------------------------------| + | 200 | Deleted | + | 404 | Notification Connector Type does not exist | +
+ +## Miscellaneous [^top](#top) + +### ![GET](https://img.shields.io/badge/GET-0f0) `/v2/LogFile` + +Returns the current log-file. + +
+ Returns + + The Logfile as Stream. + +| StatusCode | Meaning | +|------------|------------| +| 404 | No Logfile | +
+ +### ![GET](https://img.shields.io/badge/GET-0f0) `/v2/Ping` + +Pong! + +### ![POST](https://img.shields.io/badge/POST-00f) `/v2/Ping` + +Pong! diff --git a/docs/Types.md b/docs/Types.md new file mode 100644 index 0000000..5543fed --- /dev/null +++ b/docs/Types.md @@ -0,0 +1,173 @@ +## Connector + +``` +{ + "name": string, + "SupportedLanguages": string[], + "BaseUris": string[] +} +``` + +## Manga +``` +{ + "sortName": string, + "authors": string[], + "altTitles": string[][], + "description": string, + "tags": string[], + "coverUrl": string, + "coverFileNameInCache": string, + "links": string[][], + "year": int, + "originalLanguage": string, + "releaseStatus": ReleaseStatus, see ReleaseStatus + "folderName": string, + "publicationId": string, + "internalId": string, + "ignoreChaptersBelow": number, + "latestChapterDownloaded": number, + "latestChapterAvailable": number, + "websiteUrl": string, + "mangaConnector": Connector +} +``` + +## Chapter +``` +{ + "parentManga": IManga, + "name": string | undefined, + "volumeNumber": string, + "chapterNumber": string, + "url": string, + "fileName": string, + "id": string? +} +``` + +### ReleaseStatus +``` +{ + Continuing = 0, + Completed = 1, + OnHiatus = 2, + Cancelled = 3, + Unreleased = 4 +} +``` + +## Job +``` +{ + "progressToken": IProgressToken, + "recurring": boolean, + "recurrenceTime": string, + "lastExecution": Date, + "nextExecution": Date, + "id": string, + "jobType": number, //see JobType + "parentJobId": string | null, + "mangaConnector": IMangaConnector, + "mangaInternalId": string | undefined, //only on DownloadNewChapters + "translatedLanguage": string | undefined, //only on DownloadNewChapters + "chapter": IChapter | undefined, //only on DownloadChapter +} +``` + +### JobType +``` +{ + DownloadChapterJob = 0, + DownloadNewChaptersJob = 1, + UpdateMetaDataJob = 2, + MonitorManga = 3 +} +``` + +## ProgressToken +``` +{ + "cancellationRequested": boolean, + "increments": number, + "incrementsCompleted": number, + "progress": number, + "lastUpdate": Date, + "executionStarted": Date, + "timeRemaining": Date, + "state": number //see ProgressState +} +``` + +### ProgressState +``` +{ + Running = 0, + Complete = 1, + Standby = 2, + Cancelled = 3, + Waiting = 4 +} +``` + +## Settings +``` +{ + "downloadLocation": string, + "workingDirectory": string, + "apiPortNumber": number, + "userAgent": string, + "bufferLibraryUpdates": boolean, + "bufferNotifications": boolean, + "version": number, + "aprilFoolsMode": boolean, + "compressImages": boolean, + "bwImages": boolean, + "requestLimits": { + "MangaInfo": number, + "MangaDexFeed": number, + "MangaDexImage": number, + "MangaImage": number, + "MangaCover": number, + "Default": number + } +} +``` + +## LibraryConnector +``` +{ + "libraryType": number, //see LibraryType + "baseUrl": string, + "auth": string +} +``` + +### LibraryType +``` +{ + Komga = 0, + Kavita = 1 +} +``` + +## NotificationConnector +``` +{ + "notificationConnectorType": number, //see NotificationConnectorType + "endpoint": string, //only on Ntfy, Gotify + "appToken": string, //only on Gotify + "auth": string, //only on Ntfy + "topic": string, //only on Ntfy + "id": string, //only on LunaSea +} +``` + +### NotificationConnectorType +``` +{ + Gotify = 0, + LunaSea = 1, + Ntfy = 2 +} +``` \ No newline at end of file