No longer require connector name to create job

This commit is contained in:
Glax 2024-04-22 04:21:30 +02:00
parent 64482931a3
commit 03e90eccd3
14 changed files with 62 additions and 86 deletions

View File

@ -7,8 +7,8 @@ public class DownloadNewChapters : Job
public Manga manga { get; set; } public Manga manga { get; set; }
public string translatedLanguage { get; init; } public string translatedLanguage { get; init; }
public DownloadNewChapters(GlobalBase clone, MangaConnector connector, Manga manga, DateTime lastExecution, public DownloadNewChapters(GlobalBase clone, Manga manga, DateTime lastExecution,
bool recurring = false, TimeSpan? recurrence = null, string? parentJobId = null, string translatedLanguage = "en") : base(clone, JobType.DownloadNewChaptersJob, connector, lastExecution, recurring, bool recurring = false, TimeSpan? recurrence = null, string? parentJobId = null, string translatedLanguage = "en") : base(clone, JobType.DownloadNewChaptersJob, manga.mangaConnector, lastExecution, recurring,
recurrence, parentJobId) recurrence, parentJobId)
{ {
this.manga = manga; this.manga = manga;
@ -43,7 +43,7 @@ public class DownloadNewChapters : Job
DownloadChapter downloadChapterJob = new(this, this.mangaConnector, chapter, parentJobId: this.id); DownloadChapter downloadChapterJob = new(this, this.mangaConnector, chapter, parentJobId: this.id);
jobs.Add(downloadChapterJob); jobs.Add(downloadChapterJob);
} }
UpdateMetadata updateMetadataJob = new(this, this.mangaConnector, this.manga, parentJobId: this.id); UpdateMetadata updateMetadataJob = new(this, this.manga, parentJobId: this.id);
jobs.Add(updateMetadataJob); jobs.Add(updateMetadataJob);
progressToken.Complete(); progressToken.Complete();
return jobs; return jobs;

View File

@ -24,52 +24,41 @@ public class JobJsonConverter : JsonConverter
{ {
JObject jo = JObject.Load(reader); JObject jo = JObject.Load(reader);
if (jo.ContainsKey("jobType") && jo["jobType"]!.Value<byte>() == (byte)Job.JobType.UpdateMetaDataJob) if(!jo.ContainsKey("jobType"))
throw new Exception();
return Enum.Parse<Job.JobType>(jo["jobType"]!.Value<byte>().ToString()) switch
{ {
return new UpdateMetadata(this._clone, Job.JobType.UpdateMetaDataJob => new UpdateMetadata(_clone,
jo.GetValue("manga")!.ToObject<Manga>(JsonSerializer.Create(new JsonSerializerSettings()
{
Converters = { this._mangaConnectorJsonConverter }
})),
jo.GetValue("parentJobId")!.Value<string?>()),
Job.JobType.DownloadChapterJob => new DownloadChapter(this._clone,
jo.GetValue("mangaConnector")!.ToObject<MangaConnector>(JsonSerializer.Create(new JsonSerializerSettings() jo.GetValue("mangaConnector")!.ToObject<MangaConnector>(JsonSerializer.Create(new JsonSerializerSettings()
{ {
Converters = Converters = { this._mangaConnectorJsonConverter }
{
this._mangaConnectorJsonConverter
}
}))!, }))!,
jo.GetValue("manga")!.ToObject<Manga>(), jo.GetValue("chapter")!.ToObject<Chapter>(JsonSerializer.Create(new JsonSerializerSettings()
jo.GetValue("parentJobId")!.Value<string?>());
}else if ((jo.ContainsKey("jobType") && jo["jobType"]!.Value<byte>() == (byte)Job.JobType.DownloadNewChaptersJob) || jo.ContainsKey("translatedLanguage"))//TODO change to jobType
{ {
DateTime lastExecution = jo.GetValue("lastExecution") is {} le Converters = { this._mangaConnectorJsonConverter }
})),
DateTime.UnixEpoch,
jo.GetValue("parentJobId")!.Value<string?>()),
Job.JobType.DownloadNewChaptersJob => new DownloadNewChapters(this._clone,
jo.GetValue("manga")!.ToObject<Manga>(JsonSerializer.Create(new JsonSerializerSettings()
{
Converters = { this._mangaConnectorJsonConverter }
})),
jo.GetValue("lastExecution") is {} le
? le.ToObject<DateTime>() ? le.ToObject<DateTime>()
: DateTime.UnixEpoch; //TODO do null checks on all variables : DateTime.UnixEpoch,
return new DownloadNewChapters(this._clone,
jo.GetValue("mangaConnector")!.ToObject<MangaConnector>(JsonSerializer.Create(new JsonSerializerSettings()
{
Converters =
{
this._mangaConnectorJsonConverter
}
}))!,
jo.GetValue("manga")!.ToObject<Manga>(),
lastExecution,
jo.GetValue("recurring")!.Value<bool>(), jo.GetValue("recurring")!.Value<bool>(),
jo.GetValue("recurrenceTime")!.ToObject<TimeSpan?>(), jo.GetValue("recurrenceTime")!.ToObject<TimeSpan?>(),
jo.GetValue("parentJobId")!.Value<string?>()); jo.GetValue("parentJobId")!.Value<string?>()),
}else if ((jo.ContainsKey("jobType") && jo["jobType"]!.Value<byte>() == (byte)Job.JobType.DownloadChapterJob) || jo.ContainsKey("chapter"))//TODO change to jobType _ => throw new Exception()
{ };
return new DownloadChapter(this._clone,
jo.GetValue("mangaConnector")!.ToObject<MangaConnector>(JsonSerializer.Create(new JsonSerializerSettings()
{
Converters =
{
this._mangaConnectorJsonConverter
}
}))!,
jo.GetValue("chapter")!.ToObject<Chapter>(),
DateTime.UnixEpoch,
jo.GetValue("parentJobId")!.Value<string?>());
}
throw new Exception();
} }
public override bool CanWrite => false; public override bool CanWrite => false;

View File

@ -6,7 +6,7 @@ public class UpdateMetadata : Job
{ {
public Manga manga { get; set; } public Manga manga { get; set; }
public UpdateMetadata(GlobalBase clone, MangaConnector connector, Manga manga, string? parentJobId = null) : base(clone, JobType.UpdateMetaDataJob, connector, parentJobId: parentJobId) public UpdateMetadata(GlobalBase clone, Manga manga, string? parentJobId = null) : base(clone, JobType.UpdateMetaDataJob, manga.mangaConnector, parentJobId: parentJobId)
{ {
this.manga = manga; this.manga = manga;
} }

View File

@ -2,6 +2,7 @@
using System.Text; using System.Text;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using Newtonsoft.Json; using Newtonsoft.Json;
using Tranga.MangaConnectors;
using static System.IO.UnixFileMode; using static System.IO.UnixFileMode;
namespace Tranga; namespace Tranga;
@ -26,8 +27,6 @@ public struct Manga
// ReSharper disable once MemberCanBePrivate.Global // ReSharper disable once MemberCanBePrivate.Global
public int? year { get; private set; } public int? year { get; private set; }
public string? originalLanguage { get; } public string? originalLanguage { get; }
// ReSharper disable twice MemberCanBePrivate.Global
public string status { get; private set; }
public ReleaseStatusByte releaseStatus { get; private set; } public ReleaseStatusByte releaseStatus { get; private set; }
public enum ReleaseStatusByte : byte public enum ReleaseStatusByte : byte
{ {
@ -43,14 +42,15 @@ public struct Manga
public float ignoreChaptersBelow { get; set; } public float ignoreChaptersBelow { get; set; }
public float latestChapterDownloaded { get; set; } public float latestChapterDownloaded { get; set; }
public float latestChapterAvailable { 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 \.\-,'\'\)\(~!\+]*"); private static readonly Regex LegalCharacters = new (@"[A-Za-zÀ-ÖØ-öø-ÿ0-9 \.\-,'\'\)\(~!\+]*");
[JsonConstructor] [JsonConstructor]
public Manga(string sortName, List<string> authors, string? description, Dictionary<string,string> altTitles, string[] tags, string? coverUrl, string? coverFileNameInCache, Dictionary<string,string>? links, int? year, string? originalLanguage, string publicationId, ReleaseStatusByte releaseStatus, string? websiteUrl, string? folderName = null, float? ignoreChaptersBelow = 0) public Manga(MangaConnector mangaConnector, string sortName, List<string> authors, string? description, Dictionary<string,string> altTitles, string[] tags, string? coverUrl, string? coverFileNameInCache, Dictionary<string,string>? links, int? year, string? originalLanguage, string publicationId, ReleaseStatusByte releaseStatus, string? websiteUrl, string? folderName = null, float? ignoreChaptersBelow = 0)
{ {
this.mangaConnector = mangaConnector;
this.sortName = sortName; this.sortName = sortName;
this.authors = authors; this.authors = authors;
this.description = description; this.description = description;
@ -82,7 +82,6 @@ public struct Manga
this.authors = authors.Union(newManga.authors).ToList(); this.authors = authors.Union(newManga.authors).ToList();
this.altTitles = altTitles.UnionBy(newManga.altTitles, kv => kv.Key).ToDictionary(x => x.Key, x => x.Value); this.altTitles = altTitles.UnionBy(newManga.altTitles, kv => kv.Key).ToDictionary(x => x.Key, x => x.Value);
this.tags = tags.Union(newManga.tags).ToArray(); this.tags = tags.Union(newManga.tags).ToArray();
this.status = newManga.status;
this.releaseStatus = newManga.releaseStatus; this.releaseStatus = newManga.releaseStatus;
this.year = newManga.year; this.year = newManga.year;
} }
@ -93,7 +92,6 @@ public struct Manga
return false; return false;
return this.description == compareManga.description && return this.description == compareManga.description &&
this.year == compareManga.year && this.year == compareManga.year &&
this.status == compareManga.status &&
this.releaseStatus == compareManga.releaseStatus && this.releaseStatus == compareManga.releaseStatus &&
this.sortName == compareManga.sortName && this.sortName == compareManga.sortName &&
this.latestChapterAvailable.Equals(compareManga.latestChapterAvailable) && this.latestChapterAvailable.Equals(compareManga.latestChapterAvailable) &&

View File

@ -114,8 +114,8 @@ public class Bato : MangaConnector
case "pending": releaseStatus = Manga.ReleaseStatusByte.Unreleased; break; case "pending": releaseStatus = Manga.ReleaseStatusByte.Unreleased; break;
} }
Manga manga = new (sortName, authors, description, altTitles, tags, posterUrl, coverFileNameInCache, new Dictionary<string, string>(), Manga manga = new (this, sortName, authors, description, altTitles, tags, posterUrl, coverFileNameInCache, new Dictionary<string, string>(),
year, originalLanguage, publicationId, releaseStatus, websiteUrl: websiteUrl); year, originalLanguage, publicationId, releaseStatus, websiteUrl);
cachedPublications.Add(manga); cachedPublications.Add(manga);
return manga; return manga;
} }

View File

@ -126,10 +126,10 @@ public class MangaDex : MangaConnector
false => null false => null
}; };
Manga.ReleaseStatusByte status = Manga.ReleaseStatusByte.Unreleased; Manga.ReleaseStatusByte releaseStatus = Manga.ReleaseStatusByte.Unreleased;
if (attributes.TryGetPropertyValue("status", out JsonNode? statusNode)) if (attributes.TryGetPropertyValue("status", out JsonNode? statusNode))
{ {
status = statusNode?.GetValue<string>().ToLower() switch releaseStatus = statusNode?.GetValue<string>().ToLower() switch
{ {
"ongoing" => Manga.ReleaseStatusByte.Continuing, "ongoing" => Manga.ReleaseStatusByte.Continuing,
"completed" => Manga.ReleaseStatusByte.Completed, "completed" => Manga.ReleaseStatusByte.Completed,
@ -173,6 +173,7 @@ public class MangaDex : MangaConnector
} }
Manga pub = new( Manga pub = new(
this,
title, title,
authors, authors,
description, description,
@ -184,8 +185,8 @@ public class MangaDex : MangaConnector
year, year,
originalLanguage, originalLanguage,
publicationId, publicationId,
status, releaseStatus,
websiteUrl: $"https://mangadex.org/title/{publicationId}" $"https://mangadex.org/title/{publicationId}"
); );
cachedPublications.Add(pub); cachedPublications.Add(pub);
return pub; return pub;

View File

@ -141,8 +141,8 @@ public class MangaKatana : MangaConnector
year = Convert.ToInt32(yearString); year = Convert.ToInt32(yearString);
} }
Manga manga = new (sortName, authors.ToList(), description, altTitles, tags.ToArray(), posterUrl, coverFileNameInCache, links, Manga manga = new (this, sortName, authors.ToList(), description, altTitles, tags.ToArray(), posterUrl, coverFileNameInCache, links,
year, originalLanguage, publicationId, releaseStatus, websiteUrl: websiteUrl); year, originalLanguage, publicationId, releaseStatus, websiteUrl);
cachedPublications.Add(manga); cachedPublications.Add(manga);
return manga; return manga;
} }

View File

@ -121,8 +121,8 @@ public class MangaLife : MangaConnector
.Descendants("div").First(); .Descendants("div").First();
string description = descriptionNode.InnerText; 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); coverFileNameInCache, links, year, originalLanguage, publicationId, releaseStatus, websiteUrl);
cachedPublications.Add(manga); cachedPublications.Add(manga);
return manga; return manga;
} }

View File

@ -127,8 +127,8 @@ public class Manganato : MangaConnector
.First(s => s.HasClass("chapter-time")).InnerText; .First(s => s.HasClass("chapter-time")).InnerText;
int year = Convert.ToInt32(yearString.Split(',')[^1]) + 2000; int year = Convert.ToInt32(yearString.Split(',')[^1]) + 2000;
Manga manga = new (sortName, authors.ToList(), description, altTitles, tags.ToArray(), posterUrl, coverFileNameInCache, links, Manga manga = new (this, sortName, authors.ToList(), description, altTitles, tags.ToArray(), posterUrl, coverFileNameInCache, links,
year, originalLanguage, publicationId, releaseStatus, websiteUrl: websiteUrl); year, originalLanguage, publicationId, releaseStatus, websiteUrl);
cachedPublications.Add(manga); cachedPublications.Add(manga);
return manga; return manga;
} }

View File

@ -176,9 +176,8 @@ public class Mangasee : MangaConnector
.Descendants("div").First(); .Descendants("div").First();
string description = descriptionNode.InnerText; 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, coverFileNameInCache, links, year, originalLanguage, publicationId, releaseStatus, websiteUrl);
year, originalLanguage, publicationId, releaseStatus, websiteUrl: websiteUrl);
cachedPublications.Add(manga); cachedPublications.Add(manga);
return manga; return manga;
} }

View File

@ -118,8 +118,8 @@ public class Mangaworld: MangaConnector
string yearString = metadata.SelectSingleNode("//span[text()='Anno di uscita: ']/..").SelectNodes("a").First().InnerText; string yearString = metadata.SelectSingleNode("//span[text()='Anno di uscita: ']/..").SelectNodes("a").First().InnerText;
int year = Convert.ToInt32(yearString); int year = Convert.ToInt32(yearString);
Manga manga = new (sortName, authors.ToList(), description, altTitles, tags.ToArray(), posterUrl, coverFileNameInCache, links, Manga manga = new (this, sortName, authors.ToList(), description, altTitles, tags.ToArray(), posterUrl, coverFileNameInCache, links,
year, originalLanguage, publicationId, releaseStatus, websiteUrl: websiteUrl); year, originalLanguage, publicationId, releaseStatus, websiteUrl);
cachedPublications.Add(manga); cachedPublications.Add(manga);
return manga; return manga;
} }

View File

@ -32,7 +32,7 @@ public partial class Server : GlobalBase, IDisposable
new ("GET", @"/v2/Jobs/Waiting", GetV2JobsWaiting), new ("GET", @"/v2/Jobs/Waiting", GetV2JobsWaiting),
new ("GET", @"/v2/Jobs/Monitoring", GetV2JobsMonitoring), new ("GET", @"/v2/Jobs/Monitoring", GetV2JobsMonitoring),
new ("Get", @"/v2/Job/Types", GetV2JobTypes), new ("Get", @"/v2/Job/Types", GetV2JobTypes),
new ("POST", @"/v2/Job/Create/([a-zA-Z]+)>", PostV2JobsCreateType), new ("POST", @"/v2/Job/Create/([a-zA-Z]+)", PostV2JobsCreateType),
new ("GET", @"/v2/Job/([a-zA-Z\.]+-[-A-Za-z0-9+/]*={0,3}(?:-[0-9]+)?)", GetV2JobJobId), 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 ("DELETE", @"/v2/Job/([a-zA-Z\.]+-[-A-Za-z0-9+/]*={0,3}(?:-[0-9]+)?)", DeleteV2JobJobId),
new ("GET", @"/v2/Job/([a-zA-Z\.]+-[-A-Za-z0-9+/]*={0,3}(?:-[0-9]+)?)/Progress", GetV2JobJobIdProgress), new ("GET", @"/v2/Job/([a-zA-Z\.]+-[-A-Za-z0-9+/]*={0,3}(?:-[0-9]+)?)/Progress", GetV2JobJobIdProgress),
@ -124,7 +124,7 @@ public partial class Server : GlobalBase, IDisposable
.ToDictionary(kv => kv.Key, kv => kv.Value); //The actual variable used for the API .ToDictionary(kv => kv.Key, kv => kv.Value); //The actual variable used for the API
ValueTuple<HttpStatusCode, object?> responseMessage; //Used to respond to the HttpRequest ValueTuple<HttpStatusCode, object?> responseMessage; //Used to respond to the HttpRequest
if (_apiRequestPaths.Any(p => p.HttpMethod == request.HttpMethod && Regex.IsMatch(path, p.RegexStr))) //Check if Request-Path is valid if (_apiRequestPaths.Any(p => p.HttpMethod == request.HttpMethod && Regex.Match(path, p.RegexStr).Length == path.Length)) //Check if Request-Path is valid
{ {
RequestPath requestPath = RequestPath requestPath =
_apiRequestPaths.First(p => p.HttpMethod == request.HttpMethod && Regex.Match(path, p.RegexStr).Length == path.Length); _apiRequestPaths.First(p => p.HttpMethod == request.HttpMethod && Regex.Match(path, p.RegexStr).Length == path.Length);

View File

@ -46,42 +46,32 @@ public partial class Server
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.NotFound, $"Manga with ID: '{groups[1].Value}' does not exist."); return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.NotFound, $"Manga with ID: '{groups[1].Value}' does not exist.");
} }
string? connectorStr, mangaId; string? mangaId;
MangaConnector? connector;
Manga? manga; Manga? manga;
switch (jobType) switch (jobType)
{ {
case Job.JobType.MonitorManga: case Job.JobType.MonitorManga:
if(!requestParameters.TryGetValue("connector", out connectorStr) ||
!_parent.TryGetConnector(connectorStr, out connector) ||
connector is null)
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.NotFound, "Connector Parameter missing, or is not a valid connector.");
if(!requestParameters.TryGetValue("internalId", out mangaId) || if(!requestParameters.TryGetValue("internalId", out mangaId) ||
!_parent.TryGetPublicationById(mangaId, out manga) || !_parent.TryGetPublicationById(mangaId, out manga) ||
manga is null) manga is null)
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.NotFound, "InternalId Parameter missing, or is not a valid ID."); return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.NotFound, "'internalId' Parameter missing, or is not a valid ID.");
if(!requestParameters.TryGetValue("interval", out string? intervalStr) || if(!requestParameters.TryGetValue("interval", out string? intervalStr) ||
!TimeSpan.TryParse(intervalStr, out TimeSpan interval)) !TimeSpan.TryParse(intervalStr, out TimeSpan interval))
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.InternalServerError, "Interval Parameter missing, or is not in correct format."); return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.InternalServerError, "'interval' Parameter missing, or is not in correct format.");
requestParameters.TryGetValue("language", out string? language); requestParameters.TryGetValue("language", out string? language);
_parent.jobBoss.AddJob(new DownloadNewChapters(this, connector, (Manga)manga, true, interval, language)); _parent.jobBoss.AddJob(new DownloadNewChapters(this, ((Manga)manga).mangaConnector, (Manga)manga, true, interval, language));
break; return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.OK, null);
case Job.JobType.UpdateMetaDataJob: case Job.JobType.UpdateMetaDataJob:
if(!requestParameters.TryGetValue("connector", out connectorStr) ||
!_parent.TryGetConnector(connectorStr, out connector) ||
connector is null)
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.NotFound, "Connector Parameter missing, or is not a valid connector.");
if(!requestParameters.TryGetValue("internalId", out mangaId) || if(!requestParameters.TryGetValue("internalId", out mangaId) ||
!_parent.TryGetPublicationById(mangaId, out manga) || !_parent.TryGetPublicationById(mangaId, out manga) ||
manga is null) manga is null)
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.NotFound, "InternalId Parameter missing, or is not a valid ID."); return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.NotFound, "InternalId Parameter missing, or is not a valid ID.");
_parent.jobBoss.AddJob(new UpdateMetadata(this, connector, (Manga)manga)); _parent.jobBoss.AddJob(new UpdateMetadata(this, (Manga)manga));
break; return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.OK, null);
case Job.JobType.DownloadNewChaptersJob: //TODO case Job.JobType.DownloadNewChaptersJob: //TODO
case Job.JobType.DownloadChapterJob: //TODO case Job.JobType.DownloadChapterJob: //TODO
default: return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.MethodNotAllowed, $"JobType {Enum.GetName(jobType)} is not supported."); default: return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.MethodNotAllowed, $"JobType {Enum.GetName(jobType)} is not supported.");
} }
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.NotImplemented, "How'd you get here.");
} }
private ValueTuple<HttpStatusCode, object?> GetV2JobJobId(GroupCollection groups, Dictionary<string, string> requestParameters) private ValueTuple<HttpStatusCode, object?> GetV2JobJobId(GroupCollection groups, Dictionary<string, string> requestParameters)

View File

@ -239,7 +239,6 @@ Creates a Job.
| Parameter | Value | | Parameter | Value |
|------------|---------------------------------------------------------------------------------------------------| |------------|---------------------------------------------------------------------------------------------------|
| connector | Name of the connector to use |
| internalId | Manga ID | | internalId | Manga ID |
| *interval* | Interval at which the Job is re-run in HH:MM:SS format<br />Only for MonitorManga, UpdateMetadata | | *interval* | Interval at which the Job is re-run in HH:MM:SS format<br />Only for MonitorManga, UpdateMetadata |
| *language* | Translated language<br />Only for MonitorManga, DownloadNewChapters and DownloadChapter | | *language* | Translated language<br />Only for MonitorManga, DownloadNewChapters and DownloadChapter |