2
0

Compare commits

..

No commits in common. "ba029b71f5322a02c34807fae33d9947a488313c" and "f4966b034804b9cb0fb23ea3d29222d7796221d8" have entirely different histories.

30 changed files with 318 additions and 528 deletions

View File

@ -15,12 +15,12 @@ jobs:
# https://github.com/docker/setup-qemu-action#usage # https://github.com/docker/setup-qemu-action#usage
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v3.2.0 uses: docker/setup-qemu-action@v2.2.0
# https://github.com/marketplace/actions/docker-setup-buildx # https://github.com/marketplace/actions/docker-setup-buildx
- name: Set up Docker Buildx - name: Set up Docker Buildx
id: buildx id: buildx
uses: docker/setup-buildx-action@v3.6.1 uses: docker/setup-buildx-action@v3.3.0
# https://github.com/docker/login-action#docker-hub # https://github.com/docker/login-action#docker-hub
- name: Login to Docker Hub - name: Login to Docker Hub
@ -31,7 +31,7 @@ jobs:
# https://github.com/docker/build-push-action#multi-platform-image # https://github.com/docker/build-push-action#multi-platform-image
- name: Build and push base - name: Build and push base
uses: docker/build-push-action@v6.6.1 uses: docker/build-push-action@v5.3.0
with: with:
context: ./ context: ./
file: ./Dockerfile-base file: ./Dockerfile-base

View File

@ -17,12 +17,12 @@ jobs:
# https://github.com/docker/setup-qemu-action#usage # https://github.com/docker/setup-qemu-action#usage
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v3.2.0 uses: docker/setup-qemu-action@v2.2.0
# https://github.com/marketplace/actions/docker-setup-buildx # https://github.com/marketplace/actions/docker-setup-buildx
- name: Set up Docker Buildx - name: Set up Docker Buildx
id: buildx id: buildx
uses: docker/setup-buildx-action@v3.6.1 uses: docker/setup-buildx-action@v3.3.0
# https://github.com/docker/login-action#docker-hub # https://github.com/docker/login-action#docker-hub
- name: Login to Docker Hub - name: Login to Docker Hub
@ -33,7 +33,7 @@ jobs:
# https://github.com/docker/build-push-action#multi-platform-image # https://github.com/docker/build-push-action#multi-platform-image
- name: Build and push API - name: Build and push API
uses: docker/build-push-action@v6.6.1 uses: docker/build-push-action@v5.3.0
with: with:
context: ./ context: ./
file: ./Dockerfile file: ./Dockerfile

View File

@ -17,12 +17,12 @@ jobs:
# https://github.com/docker/setup-qemu-action#usage # https://github.com/docker/setup-qemu-action#usage
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v3.2.0 uses: docker/setup-qemu-action@v2.2.0
# https://github.com/marketplace/actions/docker-setup-buildx # https://github.com/marketplace/actions/docker-setup-buildx
- name: Set up Docker Buildx - name: Set up Docker Buildx
id: buildx id: buildx
uses: docker/setup-buildx-action@v3.6.1 uses: docker/setup-buildx-action@v3.3.0
# https://github.com/docker/login-action#docker-hub # https://github.com/docker/login-action#docker-hub
- name: Login to Docker Hub - name: Login to Docker Hub
@ -33,7 +33,7 @@ jobs:
# https://github.com/docker/build-push-action#multi-platform-image # https://github.com/docker/build-push-action#multi-platform-image
- name: Build and push API - name: Build and push API
uses: docker/build-push-action@v6.6.1 uses: docker/build-push-action@v5.3.0
with: with:
context: ./ context: ./
file: ./Dockerfile file: ./Dockerfile

View File

@ -17,12 +17,12 @@ jobs:
# https://github.com/docker/setup-qemu-action#usage # https://github.com/docker/setup-qemu-action#usage
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v3.2.0 uses: docker/setup-qemu-action@v2.2.0
# https://github.com/marketplace/actions/docker-setup-buildx # https://github.com/marketplace/actions/docker-setup-buildx
- name: Set up Docker Buildx - name: Set up Docker Buildx
id: buildx id: buildx
uses: docker/setup-buildx-action@v3.6.1 uses: docker/setup-buildx-action@v3.3.0
# https://github.com/docker/login-action#docker-hub # https://github.com/docker/login-action#docker-hub
- name: Login to Docker Hub - name: Login to Docker Hub
@ -33,7 +33,7 @@ jobs:
# https://github.com/docker/build-push-action#multi-platform-image # https://github.com/docker/build-push-action#multi-platform-image
- name: Build and push API - name: Build and push API
uses: docker/build-push-action@v6.6.1 uses: docker/build-push-action@v5.3.0
with: with:
context: ./ context: ./
file: ./Dockerfile file: ./Dockerfile

View File

@ -17,12 +17,12 @@ jobs:
# https://github.com/docker/setup-qemu-action#usage # https://github.com/docker/setup-qemu-action#usage
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v3.2.0 uses: docker/setup-qemu-action@v2.2.0
# https://github.com/marketplace/actions/docker-setup-buildx # https://github.com/marketplace/actions/docker-setup-buildx
- name: Set up Docker Buildx - name: Set up Docker Buildx
id: buildx id: buildx
uses: docker/setup-buildx-action@v3.6.1 uses: docker/setup-buildx-action@v3.3.0
# https://github.com/docker/login-action#docker-hub # https://github.com/docker/login-action#docker-hub
- name: Login to Docker Hub - name: Login to Docker Hub
@ -33,7 +33,7 @@ jobs:
# https://github.com/docker/build-push-action#multi-platform-image # https://github.com/docker/build-push-action#multi-platform-image
- name: Build and push API - name: Build and push API
uses: docker/build-push-action@v6.6.1 uses: docker/build-push-action@v5.3.0
with: with:
context: ./ context: ./
file: ./Dockerfile file: ./Dockerfile

View File

@ -59,6 +59,7 @@ Notifications can be sent to your devices using [Gotify](https://gotify.net/) an
Tranga (this git-repo) will open a port (standard 6531) and listen for requests to add Jobs to Monitor and/or download specific Manga. 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.
The frontend in this repo is **CLI**-based.
_**For a web-frontend use [tranga-website](https://github.com/C9Glax/tranga-website).**_ _**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). 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).
@ -85,7 +86,6 @@ That is why I wanted to create my own project, in a language I understand, and t
- Newtonsoft.JSON - Newtonsoft.JSON
- [PuppeteerSharp](https://www.puppeteersharp.com/) - [PuppeteerSharp](https://www.puppeteersharp.com/)
- [Html Agility Pack (HAP)](https://html-agility-pack.net/) - [Html Agility Pack (HAP)](https://html-agility-pack.net/)
- [Soenneker.Utils.String.NeedlemanWunsch](https://github.com/soenneker/soenneker.utils.string.needlemanwunsch)
- 💙 Blåhaj 🦈 - 💙 Blåhaj 🦈
<p align="right">(<a href="#readme-top">back to top</a>)</p> <p align="right">(<a href="#readme-top">back to top</a>)</p>

View File

@ -91,22 +91,18 @@ public readonly struct Chapter : IComparable
{ {
if (!Directory.Exists(Path.Join(downloadLocation, parentManga.folderName))) if (!Directory.Exists(Path.Join(downloadLocation, parentManga.folderName)))
return false; return false;
FileInfo[] archives = new DirectoryInfo(Path.Join(downloadLocation, parentManga.folderName)).GetFiles().Where(file => file.Name.Split('.')[^1] == "cbz").ToArray(); FileInfo[] archives = new DirectoryInfo(Path.Join(downloadLocation, parentManga.folderName)).GetFiles();
Regex volChRex = new(@"(?:Vol(?:ume)?\.([0-9]+)\D*)?Ch(?:apter)?\.([0-9]+(?:\.[0-9]+)*)"); Regex volChRex = new(@"(?:Vol(?:ume)?\.([0-9]+)\D*)?Ch(?:apter)?\.([0-9]+(?:\.[0-9]+)*)");
Chapter t = this; Chapter t = this;
string thisPath = GetArchiveFilePath(downloadLocation); return archives.Select(archive => archive.Name).Any(archiveFileName =>
FileInfo? archive = archives.FirstOrDefault(archive =>
{ {
Match m = volChRex.Match(archive.Name); Match m = volChRex.Match(archiveFileName);
string archiveVolNum = m.Groups[1].Success ? m.Groups[1].Value : "0"; string archiveVolNum = m.Groups[1].Success ? m.Groups[1].Value : "0";
string archiveChNum = m.Groups[2].Value; string archiveChNum = m.Groups[2].Value;
return archiveVolNum == t.volumeNumber && archiveChNum == t.chapterNumber || return archiveVolNum == t.volumeNumber &&
archiveVolNum == "0" && archiveChNum == t.chapterNumber; archiveChNum == t.chapterNumber;
}); });
if(archive is not null && thisPath != archive.FullName)
archive.MoveTo(thisPath, true);
return archive is not null;
} }
/// <summary> /// <summary>
/// Creates full file path of chapter-archive /// Creates full file path of chapter-archive

View File

@ -14,7 +14,7 @@ public abstract class GlobalBase
protected TrangaSettings settings { get; init; } protected TrangaSettings settings { get; init; }
protected HashSet<NotificationConnector> notificationConnectors { get; init; } protected HashSet<NotificationConnector> notificationConnectors { get; init; }
protected HashSet<LibraryConnector> libraryConnectors { get; init; } protected HashSet<LibraryConnector> libraryConnectors { get; init; }
private Dictionary<string, Manga> cachedPublications { get; init; } protected List<Manga> cachedPublications { get; init; }
public static readonly NumberFormatInfo numberFormatDecimalPoint = new (){ NumberDecimalSeparator = "." }; public static readonly NumberFormatInfo numberFormatDecimalPoint = new (){ NumberDecimalSeparator = "." };
protected static readonly Regex baseUrlRex = new(@"https?:\/\/[0-9A-z\.-]+(:[0-9]+)?"); protected static readonly Regex baseUrlRex = new(@"https?:\/\/[0-9A-z\.-]+(:[0-9]+)?");
@ -36,29 +36,6 @@ public abstract class GlobalBase
this.cachedPublications = new(); 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;
}
}
protected Manga? GetCachedManga(string internalId)
{
return cachedPublications.TryGetValue(internalId, out Manga manga) switch
{
true => manga,
_ => null
};
}
protected IEnumerable<Manga> GetAllCachedManga()
{
return cachedPublications.Values;
}
protected void Log(string message) protected void Log(string message)
{ {
logger?.WriteLine(this.GetType().Name, message); logger?.WriteLine(this.GetType().Name, message);

View File

@ -150,53 +150,39 @@ public class JobBoss : GlobalBase
//Load json-job-files //Load json-job-files
foreach (FileInfo file in new DirectoryInfo(settings.jobsFolderPath).EnumerateFiles().Where(fileInfo => idRex.IsMatch(fileInfo.Name))) foreach (FileInfo file in new DirectoryInfo(settings.jobsFolderPath).EnumerateFiles().Where(fileInfo => idRex.IsMatch(fileInfo.Name)))
{ {
Log($"Adding {file.Name}"); Job job = JsonConvert.DeserializeObject<Job>(File.ReadAllText(file.FullName),
Job? job = JsonConvert.DeserializeObject<Job>(File.ReadAllText(file.FullName), new JobJsonConverter(this, new MangaConnectorJsonConverter(this, connectors)))!;
new JobJsonConverter(this, new MangaConnectorJsonConverter(this, connectors))); this.jobs.Add(job);
if (job is null)
{
string newName = file.FullName + ".failed";
Log($"Failed loading file {file.Name}.\nMoving to {newName}");
File.Move(file.FullName, newName);
}
else
{
Log($"Adding Job {job}");
this.jobs.Add(job);
}
} }
//Connect jobs to parent-jobs and add Publications to cache //Connect jobs to parent-jobs and add Publications to cache
foreach (Job job in this.jobs) foreach (Job job in this.jobs)
{ {
Log($"Loading Job {job}"); this.jobs.FirstOrDefault(jjob => jjob.id == job.parentJobId)?.AddSubJob(job);
Job? parentJob = this.jobs.FirstOrDefault(jjob => jjob.id == job.parentJobId);
if (parentJob is not null)
{
parentJob.AddSubJob(job);
Log($"Parent Job {parentJob}");
}
if (job is DownloadNewChapters dncJob) if (job is DownloadNewChapters dncJob)
AddMangaToCache(dncJob.manga); cachedPublications.Add(dncJob.manga);
} }
string[] coverFiles = Directory.GetFiles(settings.coverImageCache); HashSet<string> coverFileNames = cachedPublications.Select(manga => manga.coverFileNameInCache!).ToHashSet();
foreach(string fileName in coverFiles.Where(fileName => !GetAllCachedManga().Any(manga => manga.coverFileNameInCache == fileName))) foreach (string fileName in Directory.GetFiles(settings.coverImageCache)) //Cleanup Unused Covers
{
if(!coverFileNames.Any(existingManga => fileName.Contains(existingManga)))
File.Delete(fileName); File.Delete(fileName);
}
} }
internal void UpdateJobFile(Job job, string? oldFile = null) private void UpdateJobFile(Job job)
{ {
string newJobFilePath = Path.Join(settings.jobsFolderPath, $"{job.id}.json"); string jobFilePath = Path.Join(settings.jobsFolderPath, $"{job.id}.json");
if (!this.jobs.Any(jjob => jjob.id == job.id)) if (!this.jobs.Any(jjob => jjob.id == job.id))
{ {
try try
{ {
Log($"Deleting Job-file {newJobFilePath}"); Log($"Deleting Job-file {jobFilePath}");
while(IsFileInUse(newJobFilePath)) while(IsFileInUse(jobFilePath))
Thread.Sleep(10); Thread.Sleep(10);
File.Delete(newJobFilePath); File.Delete(jobFilePath);
} }
catch (Exception e) catch (Exception e)
{ {
@ -205,25 +191,12 @@ public class JobBoss : GlobalBase
} }
else else
{ {
Log($"Exporting Job {newJobFilePath}"); Log($"Exporting Job {jobFilePath}");
string jobStr = JsonConvert.SerializeObject(job, Formatting.Indented); string jobStr = JsonConvert.SerializeObject(job);
while(IsFileInUse(newJobFilePath)) while(IsFileInUse(jobFilePath))
Thread.Sleep(10); Thread.Sleep(10);
File.WriteAllText(newJobFilePath, jobStr); File.WriteAllText(jobFilePath, jobStr);
} }
if(oldFile is not null)
try
{
Log($"Deleting old Job-file {oldFile}");
while(IsFileInUse(oldFile))
Thread.Sleep(10);
File.Delete(oldFile);
}
catch (Exception e)
{
Log(e.ToString());
}
} }
private void UpdateAllJobFiles() private void UpdateAllJobFiles()
@ -272,9 +245,7 @@ public class JobBoss : GlobalBase
Log($"Next job in {jobs.MinBy(job => job.nextExecution)?.nextExecution.Subtract(DateTime.Now)} {jobs.MinBy(job => job.nextExecution)?.id}"); Log($"Next job in {jobs.MinBy(job => job.nextExecution)?.nextExecution.Subtract(DateTime.Now)} {jobs.MinBy(job => job.nextExecution)?.id}");
}else if (queueHead.progressToken.state is ProgressToken.State.Standby) }else if (queueHead.progressToken.state is ProgressToken.State.Standby)
{ {
Job eJob = jobQueue.Peek(); Job[] subJobs = jobQueue.Peek().ExecuteReturnSubTasks(this).ToArray();
Job[] subJobs = eJob.ExecuteReturnSubTasks(this).ToArray();
UpdateJobFile(eJob);
AddJobs(subJobs); AddJobs(subJobs);
AddJobsToQueue(subJobs); AddJobsToQueue(subJobs);
}else if (queueHead.progressToken.state is ProgressToken.State.Running && DateTime.Now.Subtract(queueHead.progressToken.lastUpdate) > TimeSpan.FromMinutes(5)) }else if (queueHead.progressToken.state is ProgressToken.State.Running && DateTime.Now.Subtract(queueHead.progressToken.lastUpdate) > TimeSpan.FromMinutes(5))

View File

@ -33,26 +33,8 @@ public class UpdateMetadata : Job
return Array.Empty<Job>(); return Array.Empty<Job>();
} }
this.manga = manga.WithMetadata(updatedManga); this.manga.UpdateMetadata(updatedManga);
this.manga.SaveSeriesInfoJson(settings.downloadLocation, true); this.manga.SaveSeriesInfoJson(settings.downloadLocation, 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);
}
this.progressToken.Complete(); this.progressToken.Complete();
} }
else else

View File

@ -67,15 +67,7 @@ public class Kavita : LibraryConnector
foreach (KavitaLibrary lib in GetLibraries()) foreach (KavitaLibrary lib in GetLibraries())
NetClient.MakePost($"{baseUrl}/api/Library/scan?libraryId={lib.id}", "Bearer", auth, logger); NetClient.MakePost($"{baseUrl}/api/Library/scan?libraryId={lib.id}", "Bearer", auth, logger);
} }
internal override bool Test()
{
foreach (KavitaLibrary lib in GetLibraries())
if (NetClient.MakePost($"{baseUrl}/api/Library/scan?libraryId={lib.id}", "Bearer", auth, logger))
return true;
return false;
}
/// <summary> /// <summary>
/// Fetches all libraries available to the user /// Fetches all libraries available to the user
/// </summary> /// </summary>
@ -83,7 +75,7 @@ public class Kavita : LibraryConnector
private IEnumerable<KavitaLibrary> GetLibraries() private IEnumerable<KavitaLibrary> GetLibraries()
{ {
Log("Getting libraries."); Log("Getting libraries.");
Stream data = NetClient.MakeRequest($"{baseUrl}/api/Library/libraries", "Bearer", auth, logger); Stream data = NetClient.MakeRequest($"{baseUrl}/api/Library", "Bearer", auth, logger);
if (data == Stream.Null) if (data == Stream.Null)
{ {
Log("No libraries returned"); Log("No libraries returned");
@ -96,13 +88,11 @@ public class Kavita : LibraryConnector
return Array.Empty<KavitaLibrary>(); return Array.Empty<KavitaLibrary>();
} }
List<KavitaLibrary> ret = new(); HashSet<KavitaLibrary> ret = new();
foreach (JsonNode? jsonNode in result) foreach (JsonNode? jsonNode in result)
{ {
JsonObject? jObject = (JsonObject?)jsonNode; var jObject = (JsonObject?)jsonNode;
if(jObject is null)
continue;
int libraryId = jObject!["id"]!.GetValue<int>(); int libraryId = jObject!["id"]!.GetValue<int>();
string libraryName = jObject["name"]!.GetValue<string>(); string libraryName = jObject["name"]!.GetValue<string>();
ret.Add(new KavitaLibrary(libraryId, libraryName)); ret.Add(new KavitaLibrary(libraryId, libraryName));

View File

@ -32,14 +32,6 @@ public class Komga : LibraryConnector
NetClient.MakePost($"{baseUrl}/api/v1/libraries/{lib.id}/scan", "Basic", auth, logger); NetClient.MakePost($"{baseUrl}/api/v1/libraries/{lib.id}/scan", "Basic", auth, logger);
} }
internal override bool Test()
{
foreach (KomgaLibrary lib in GetLibraries())
if (NetClient.MakePost($"{baseUrl}/api/v1/libraries/{lib.id}/scan", "Basic", auth, logger))
return true;
return false;
}
/// <summary> /// <summary>
/// Fetches all libraries available to the user /// Fetches all libraries available to the user
/// </summary> /// </summary>

View File

@ -30,7 +30,6 @@ public abstract class LibraryConnector : GlobalBase
this.libraryType = libraryType; this.libraryType = libraryType;
} }
public abstract void UpdateLibrary(); public abstract void UpdateLibrary();
internal abstract bool Test();
protected static class NetClient protected static class NetClient
{ {

View File

@ -1,7 +1,6 @@
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Text; using System.Text;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Web;
using Newtonsoft.Json; using Newtonsoft.Json;
using static System.IO.UnixFileMode; using static System.IO.UnixFileMode;
@ -13,15 +12,15 @@ namespace Tranga;
public struct Manga public struct Manga
{ {
public string sortName { get; private set; } public string sortName { get; private set; }
public List<string> authors { get; private set; } public List<string> authors { get; }
// ReSharper disable once UnusedAutoPropertyAccessor.Global // ReSharper disable once UnusedAutoPropertyAccessor.Global
public Dictionary<string,string> altTitles { get; private set; } public Dictionary<string,string> altTitles { get; }
// ReSharper disable once MemberCanBePrivate.Global // ReSharper disable once MemberCanBePrivate.Global
public string? description { get; private set; } public string? description { get; private set; }
public string[] tags { get; private set; } public string[] tags { get; }
// ReSharper disable once UnusedAutoPropertyAccessor.Global // ReSharper disable once UnusedAutoPropertyAccessor.Global
public string? coverUrl { get; private set; } public string? coverUrl { get; }
public string? coverFileNameInCache { get; private set; } public string? coverFileNameInCache { get; }
// ReSharper disable once UnusedAutoPropertyAccessor.Global // ReSharper disable once UnusedAutoPropertyAccessor.Global
public Dictionary<string,string> links { get; } public Dictionary<string,string> links { get; }
// ReSharper disable once MemberCanBePrivate.Global // ReSharper disable once MemberCanBePrivate.Global
@ -29,7 +28,7 @@ public struct Manga
public string? originalLanguage { get; } public string? originalLanguage { get; }
// ReSharper disable twice MemberCanBePrivate.Global // ReSharper disable twice MemberCanBePrivate.Global
public string status { get; private set; } public string status { get; private set; }
public ReleaseStatusByte releaseStatus { get; private set; } public ReleaseStatusByte releaseStatus { get; }
public enum ReleaseStatusByte : byte public enum ReleaseStatusByte : byte
{ {
Continuing = 0, Continuing = 0,
@ -44,26 +43,25 @@ 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; }
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 = null, string? folderName = null, float? ignoreChaptersBelow = 0) 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 status, string publicationId, ReleaseStatusByte releaseStatus = 0, string? websiteUrl = null, string? folderName = null, float? ignoreChaptersBelow = 0)
{ {
this.sortName = HttpUtility.HtmlDecode(sortName); this.sortName = sortName;
this.authors = authors.Select(HttpUtility.HtmlDecode).ToList()!; this.authors = authors;
this.description = HttpUtility.HtmlDecode(description); this.description = description;
this.altTitles = altTitles.ToDictionary(a => HttpUtility.HtmlDecode(a.Key), a => HttpUtility.HtmlDecode(a.Value)); this.altTitles = altTitles;
this.tags = tags.Select(HttpUtility.HtmlDecode).ToArray()!; this.tags = tags;
this.coverFileNameInCache = coverFileNameInCache; this.coverFileNameInCache = coverFileNameInCache;
this.coverUrl = coverUrl; this.coverUrl = coverUrl;
this.links = links ?? new Dictionary<string, string>(); this.links = links ?? new Dictionary<string, string>();
this.year = year; this.year = year;
this.originalLanguage = originalLanguage; this.originalLanguage = originalLanguage;
this.status = status;
this.publicationId = publicationId; this.publicationId = publicationId;
this.folderName = folderName ?? string.Concat(LegalCharacters.Matches(HttpUtility.HtmlDecode(sortName))); this.folderName = folderName ?? string.Concat(LegalCharacters.Matches(sortName));
while (this.folderName.EndsWith('.')) while (this.folderName.EndsWith('.'))
this.folderName = this.folderName.Substring(0, this.folderName.Length - 1); this.folderName = this.folderName.Substring(0, this.folderName.Length - 1);
string onlyLowerLetters = string.Concat(this.sortName.ToLower().Where(Char.IsLetter)); string onlyLowerLetters = string.Concat(this.sortName.ToLower().Where(Char.IsLetter));
@ -72,26 +70,17 @@ public struct Manga
this.latestChapterDownloaded = 0; this.latestChapterDownloaded = 0;
this.latestChapterAvailable = 0; this.latestChapterAvailable = 0;
this.releaseStatus = releaseStatus; this.releaseStatus = releaseStatus;
this.status = Enum.GetName(releaseStatus) ?? "";
this.websiteUrl = websiteUrl;
} }
public Manga WithMetadata(Manga newManga) public void UpdateMetadata(Manga newManga)
{ {
return this with this.sortName = newManga.sortName;
{ this.description = newManga.description;
sortName = newManga.sortName, foreach (string author in newManga.authors)
description = newManga.description, if(!this.authors.Contains(author))
coverUrl = newManga.coverUrl, this.authors.Add(author);
authors = authors.Union(newManga.authors).ToList(), this.status = newManga.status;
altTitles = altTitles.UnionBy(newManga.altTitles, kv => kv.Key).ToDictionary(x => x.Key, x => x.Value), this.year = newManga.year;
tags = tags.Union(newManga.tags).ToArray(),
status = newManga.status,
releaseStatus = newManga.releaseStatus,
websiteUrl = newManga.websiteUrl,
year = newManga.year,
coverFileNameInCache = newManga.coverFileNameInCache
};
} }
public override bool Equals(object? obj) public override bool Equals(object? obj)
@ -104,10 +93,7 @@ public struct Manga
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) &&
this.authors.All(a => compareManga.authors.Contains(a)) && this.tags.SequenceEqual(compareManga.tags);
(this.coverFileNameInCache??"").Equals(compareManga.coverFileNameInCache) &&
(this.websiteUrl??"").Equals(compareManga.websiteUrl) &&
this.tags.All(t => compareManga.tags.Contains(t));
} }
public override string ToString() public override string ToString()
@ -182,22 +168,38 @@ public struct Manga
[JsonRequired]public string year { get; } [JsonRequired]public string year { get; }
[JsonRequired]public string status { get; } [JsonRequired]public string status { get; }
[JsonRequired]public string description_text { get; } [JsonRequired]public string description_text { get; }
[JsonIgnore] public static string[] continuing = new[]
{
"ongoing",
"hiatus",
"in corso",
"in pausa"
};
[JsonIgnore] public static string[] ended = new[]
{
"completed",
"cancelled",
"discontinued",
"finito",
"cancellato",
"droppato"
};
public Metadata(Manga manga) : this(manga.sortName, manga.year.ToString() ?? string.Empty, manga.releaseStatus, manga.description ?? "") public Metadata(Manga manga) : this(manga.sortName, manga.year.ToString() ?? string.Empty, manga.status, manga.description ?? "")
{ {
} }
public Metadata(string name, string year, ReleaseStatusByte status, string description_text) public Metadata(string name, string year, string status, string description_text)
{ {
this.name = name; this.name = name;
this.year = year; this.year = year;
this.status = status switch if(continuing.Contains(status.ToLower()))
{ this.status = "Continuing";
ReleaseStatusByte.Continuing => "Continuing", else if(ended.Contains(status.ToLower()))
ReleaseStatusByte.Completed => "Ended", this.status = "Ended";
_ => Enum.GetName(status) ?? "Ended" else
}; this.status = status;
this.description_text = description_text; this.description_text = description_text;
//kill it with fire, but otherwise Komga will not parse //kill it with fire, but otherwise Komga will not parse

View File

@ -49,7 +49,7 @@ public class Bato : MangaConnector
Log($"Failed to retrieve site"); Log($"Failed to retrieve site");
return null; return null;
} }
return ParseSinglePublicationFromHtml(requestResult.htmlDocument, url.Split('/')[^1], url); return ParseSinglePublicationFromHtml(requestResult.htmlDocument, url.Split('/')[^1]);
} }
private Manga[] ParsePublicationsFromHtml(HtmlDocument document) private Manga[] ParsePublicationsFromHtml(HtmlDocument document)
@ -72,7 +72,7 @@ public class Bato : MangaConnector
return ret.ToArray(); return ret.ToArray();
} }
private Manga ParseSinglePublicationFromHtml(HtmlDocument document, string publicationId, string websiteUrl) private Manga ParseSinglePublicationFromHtml(HtmlDocument document, string publicationId)
{ {
HtmlNode infoNode = document.DocumentNode.SelectSingleNode("/html/body/div/main/div[1]/div[2]"); HtmlNode infoNode = document.DocumentNode.SelectSingleNode("/html/body/div/main/div[1]/div[2]");
@ -86,7 +86,7 @@ public class Bato : MangaConnector
string posterUrl = document.DocumentNode.SelectNodes("//img") string posterUrl = document.DocumentNode.SelectNodes("//img")
.First(child => child.GetAttributeValue("data-hk", "") == "0-1-0").GetAttributeValue("src", "").Replace("&amp;", "&"); .First(child => child.GetAttributeValue("data-hk", "") == "0-1-0").GetAttributeValue("src", "").Replace("&amp;", "&");
string coverFileNameInCache = SaveCoverImageToCache(posterUrl, publicationId, RequestType.MangaCover); string coverFileNameInCache = SaveCoverImageToCache(posterUrl, RequestType.MangaCover);
List<HtmlNode> genreNodes = document.DocumentNode.SelectSingleNode("//b[text()='Genres:']/..").SelectNodes("span").ToList(); List<HtmlNode> genreNodes = document.DocumentNode.SelectSingleNode("//b[text()='Genres:']/..").SelectNodes("span").ToList();
string[] tags = genreNodes.Select(node => node.FirstChild.InnerText).ToArray(); string[] tags = genreNodes.Select(node => node.FirstChild.InnerText).ToArray();
@ -115,8 +115,8 @@ public class Bato : MangaConnector
} }
Manga manga = new (sortName, authors, description, altTitles, tags, posterUrl, coverFileNameInCache, new Dictionary<string, string>(), Manga manga = new (sortName, authors, description, altTitles, tags, posterUrl, coverFileNameInCache, new Dictionary<string, string>(),
year, originalLanguage, publicationId, releaseStatus, websiteUrl: websiteUrl); year, originalLanguage, status, publicationId, releaseStatus);
AddMangaToCache(manga); cachedPublications.Add(manga);
return manga; return manga;
} }

View File

@ -175,14 +175,14 @@ public abstract class MangaConnector : GlobalBase
return; return;
} }
string? fileInCache = manga.coverFileNameInCache; string fileInCache = Path.Join(settings.coverImageCache, manga.coverFileNameInCache);
if (fileInCache is null || !File.Exists(fileInCache)) if (!File.Exists(fileInCache))
{ {
Log($"Cloning cover failed: File missing {fileInCache}."); Log($"Cloning cover failed: File missing {fileInCache}.");
if (retries > 0 && manga.coverUrl is not null) if (retries > 0 && manga.coverUrl is not null)
{ {
Log($"Trying {retries} more times"); Log($"Trying {retries} more times");
SaveCoverImageToCache(manga.coverUrl, manga.internalId, 0); SaveCoverImageToCache(manga.coverUrl, 0);
CopyCoverFromCacheToDownloadLocation(manga, --retries); CopyCoverFromCacheToDownloadLocation(manga, --retries);
} }
@ -285,23 +285,20 @@ public abstract class MangaConnector : GlobalBase
return HttpStatusCode.OK; return HttpStatusCode.OK;
} }
protected string SaveCoverImageToCache(string url, string mangaInternalId, RequestType requestType) protected string SaveCoverImageToCache(string url, RequestType requestType)
{ {
Regex urlRex = new (@"https?:\/\/((?:[a-zA-Z0-9-]+\.)+[a-zA-Z0-9]+)\/(?:.+\/)*(.+\.([a-zA-Z]+))"); string filetype = url.Split('/')[^1].Split('?')[0].Split('.')[^1];
//https?:\/\/[a-zA-Z0-9-]+\.([a-zA-Z0-9-]+\.[a-zA-Z0-9]+)\/(?:.+\/)*(.+\.([a-zA-Z]+)) for only second level domains string filename = $"{DateTime.Now.Ticks.ToString()}.{filetype}";
Match match = urlRex.Match(url);
string filename = $"{match.Groups[1].Value}-{mangaInternalId}.{match.Groups[3].Value}";
string saveImagePath = Path.Join(settings.coverImageCache, filename); string saveImagePath = Path.Join(settings.coverImageCache, filename);
if (File.Exists(saveImagePath)) if (File.Exists(saveImagePath))
return saveImagePath; return filename;
RequestResult coverResult = downloadClient.MakeRequest(url, requestType); RequestResult coverResult = downloadClient.MakeRequest(url, requestType);
using MemoryStream ms = new(); using MemoryStream ms = new();
coverResult.result.CopyTo(ms); coverResult.result.CopyTo(ms);
Directory.CreateDirectory(settings.coverImageCache);
File.WriteAllBytes(saveImagePath, ms.ToArray()); File.WriteAllBytes(saveImagePath, ms.ToArray());
Log($"Saving cover to {saveImagePath}"); Log($"Saving cover to {saveImagePath}");
return saveImagePath; return filename;
} }
} }

View File

@ -38,8 +38,6 @@ public class MangaConnectorJsonConverter : JsonConverter
return this._connectors.First(c => c is Bato); return this._connectors.First(c => c is Bato);
case "Manga4Life": case "Manga4Life":
return this._connectors.First(c => c is MangaLife); return this._connectors.First(c => c is MangaLife);
case "ManhuaPlus":
return this._connectors.First(c => c is ManhuaPlus);
} }
throw new Exception(); throw new Exception();

View File

@ -115,8 +115,8 @@ public class MangaDex : MangaConnector
}; };
Dictionary<string, string> linksDict = new(); Dictionary<string, string> linksDict = new();
if (attributes.TryGetPropertyValue("links", out JsonNode? linksNode) && linksNode is not null) if (attributes.TryGetPropertyValue("links", out JsonNode? linksNode))
foreach (KeyValuePair<string, JsonNode?> linkKv in linksNode!.AsObject()) foreach (KeyValuePair<string, JsonNode> linkKv in linksNode!.AsObject())
linksDict.TryAdd(linkKv.Key, linkKv.Value.GetValue<string>()); linksDict.TryAdd(linkKv.Key, linkKv.Value.GetValue<string>());
string? originalLanguage = string? originalLanguage =
@ -160,7 +160,7 @@ public class MangaDex : MangaConnector
return null; return null;
string fileName = coverNode["attributes"]!["fileName"]!.GetValue<string>(); string fileName = coverNode["attributes"]!["fileName"]!.GetValue<string>();
string coverUrl = $"https://uploads.mangadex.org/covers/{publicationId}/{fileName}"; string coverUrl = $"https://uploads.mangadex.org/covers/{publicationId}/{fileName}";
string coverCacheName = SaveCoverImageToCache(coverUrl, publicationId, RequestType.MangaCover); string coverCacheName = SaveCoverImageToCache(coverUrl, RequestType.MangaCover);
List<string> authors = new(); List<string> authors = new();
JsonNode?[] authorNodes = relationshipsNode.AsArray() JsonNode?[] authorNodes = relationshipsNode.AsArray()
@ -183,11 +183,11 @@ public class MangaDex : MangaConnector
linksDict, linksDict,
year, year,
originalLanguage, originalLanguage,
Enum.GetName(status) ?? "",
publicationId, publicationId,
status, status
websiteUrl: $"https://mangadex.org/title/{publicationId}"
); );
AddMangaToCache(pub); cachedPublications.Add(pub);
return pub; return pub;
} }

View File

@ -28,7 +28,7 @@ public class MangaKatana : MangaConnector
&& requestResult.redirectedToUrl is not null && requestResult.redirectedToUrl is not null
&& requestResult.redirectedToUrl.Contains("mangakatana.com/manga")) && requestResult.redirectedToUrl.Contains("mangakatana.com/manga"))
{ {
return new [] { ParseSinglePublicationFromHtml(requestResult.result, requestResult.redirectedToUrl.Split('/')[^1], requestResult.redirectedToUrl) }; return new [] { ParseSinglePublicationFromHtml(requestResult.result, requestResult.redirectedToUrl.Split('/')[^1]) };
} }
Manga[] publications = ParsePublicationsFromHtml(requestResult.result); Manga[] publications = ParsePublicationsFromHtml(requestResult.result);
@ -47,7 +47,7 @@ public class MangaKatana : MangaConnector
downloadClient.MakeRequest(url, RequestType.MangaInfo); downloadClient.MakeRequest(url, RequestType.MangaInfo);
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300) if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
return null; return null;
return ParseSinglePublicationFromHtml(requestResult.result, url.Split('/')[^1], url); return ParseSinglePublicationFromHtml(requestResult.result, url.Split('/')[^1]);
} }
private Manga[] ParsePublicationsFromHtml(Stream html) private Manga[] ParsePublicationsFromHtml(Stream html)
@ -77,12 +77,13 @@ public class MangaKatana : MangaConnector
return ret.ToArray(); return ret.ToArray();
} }
private Manga ParseSinglePublicationFromHtml(Stream html, string publicationId, string websiteUrl) private Manga ParseSinglePublicationFromHtml(Stream html, string publicationId)
{ {
StreamReader reader = new(html); StreamReader reader = new(html);
string htmlString = reader.ReadToEnd(); string htmlString = reader.ReadToEnd();
HtmlDocument document = new(); HtmlDocument document = new();
document.LoadHtml(htmlString); document.LoadHtml(htmlString);
string status = "";
Dictionary<string, string> altTitles = new(); Dictionary<string, string> altTitles = new();
Dictionary<string, string>? links = null; Dictionary<string, string>? links = null;
HashSet<string> tags = new(); HashSet<string> tags = new();
@ -111,7 +112,8 @@ public class MangaKatana : MangaConnector
authors = value.Split(','); authors = value.Split(',');
break; break;
case "status": case "status":
switch (value.ToLower()) status = value;
switch (status.ToLower())
{ {
case "ongoing": releaseStatus = Manga.ReleaseStatusByte.Continuing; break; case "ongoing": releaseStatus = Manga.ReleaseStatusByte.Continuing; break;
case "completed": releaseStatus = Manga.ReleaseStatusByte.Completed; break; case "completed": releaseStatus = Manga.ReleaseStatusByte.Completed; break;
@ -126,7 +128,7 @@ public class MangaKatana : MangaConnector
string posterUrl = document.DocumentNode.SelectSingleNode("//*[@id='single_book']/div[1]/div").Descendants("img").First() string posterUrl = document.DocumentNode.SelectSingleNode("//*[@id='single_book']/div[1]/div").Descendants("img").First()
.GetAttributes().First(a => a.Name == "src").Value; .GetAttributes().First(a => a.Name == "src").Value;
string coverFileNameInCache = SaveCoverImageToCache(posterUrl, publicationId, RequestType.MangaCover); string coverFileNameInCache = SaveCoverImageToCache(posterUrl, RequestType.MangaCover);
string description = document.DocumentNode.SelectSingleNode("//*[@id='single_book']/div[3]/p").InnerText; string description = document.DocumentNode.SelectSingleNode("//*[@id='single_book']/div[3]/p").InnerText;
while (description.StartsWith('\n')) while (description.StartsWith('\n'))
@ -142,8 +144,8 @@ public class MangaKatana : MangaConnector
} }
Manga manga = new (sortName, authors.ToList(), description, altTitles, tags.ToArray(), posterUrl, coverFileNameInCache, links, Manga manga = new (sortName, authors.ToList(), description, altTitles, tags.ToArray(), posterUrl, coverFileNameInCache, links,
year, originalLanguage, publicationId, releaseStatus, websiteUrl: websiteUrl); year, originalLanguage, status, publicationId, releaseStatus);
AddMangaToCache(manga); cachedPublications.Add(manga);
return manga; return manga;
} }

View File

@ -41,7 +41,7 @@ public class MangaLife : MangaConnector
RequestResult requestResult = this.downloadClient.MakeRequest(url, RequestType.MangaInfo); RequestResult requestResult = this.downloadClient.MakeRequest(url, RequestType.MangaInfo);
if(requestResult.htmlDocument is not null) if(requestResult.htmlDocument is not null)
return ParseSinglePublicationFromHtml(requestResult.htmlDocument, publicationId, url); return ParseSinglePublicationFromHtml(requestResult.htmlDocument, publicationId);
return null; return null;
} }
@ -69,7 +69,7 @@ public class MangaLife : MangaConnector
} }
private Manga ParseSinglePublicationFromHtml(HtmlDocument document, string publicationId, string websiteUrl) private Manga ParseSinglePublicationFromHtml(HtmlDocument document, string publicationId)
{ {
string originalLanguage = "", status = ""; string originalLanguage = "", status = "";
Dictionary<string, string> altTitles = new(), links = new(); Dictionary<string, string> altTitles = new(), links = new();
@ -78,7 +78,7 @@ public class MangaLife : MangaConnector
HtmlNode posterNode = document.DocumentNode.SelectSingleNode("//div[@class='BoxBody']//div[@class='row']//img"); HtmlNode posterNode = document.DocumentNode.SelectSingleNode("//div[@class='BoxBody']//div[@class='row']//img");
string posterUrl = posterNode.GetAttributeValue("src", ""); string posterUrl = posterNode.GetAttributeValue("src", "");
string coverFileNameInCache = SaveCoverImageToCache(posterUrl, publicationId, RequestType.MangaCover); string coverFileNameInCache = SaveCoverImageToCache(posterUrl, RequestType.MangaCover);
HtmlNode titleNode = document.DocumentNode.SelectSingleNode("//div[@class='BoxBody']//div[@class='row']//h1"); HtmlNode titleNode = document.DocumentNode.SelectSingleNode("//div[@class='BoxBody']//div[@class='row']//h1");
string sortName = titleNode.InnerText; string sortName = titleNode.InnerText;
@ -122,8 +122,8 @@ public class MangaLife : MangaConnector
string description = descriptionNode.InnerText; string description = descriptionNode.InnerText;
Manga manga = new(sortName, authors.ToList(), description, altTitles, tags.ToArray(), posterUrl, Manga manga = new(sortName, authors.ToList(), description, altTitles, tags.ToArray(), posterUrl,
coverFileNameInCache, links, year, originalLanguage, publicationId, releaseStatus, websiteUrl: websiteUrl); coverFileNameInCache, links, year, originalLanguage, status, publicationId, releaseStatus);
AddMangaToCache(manga); cachedPublications.Add(manga);
return manga; return manga;
} }

View File

@ -65,11 +65,12 @@ public class Manganato : MangaConnector
if (requestResult.htmlDocument is null) if (requestResult.htmlDocument is null)
return null; return null;
return ParseSinglePublicationFromHtml(requestResult.htmlDocument, url.Split('/')[^1], url); return ParseSinglePublicationFromHtml(requestResult.htmlDocument, url.Split('/')[^1]);
} }
private Manga ParseSinglePublicationFromHtml(HtmlDocument document, string publicationId, string websiteUrl) private Manga ParseSinglePublicationFromHtml(HtmlDocument document, string publicationId)
{ {
string status = "";
Dictionary<string, string> altTitles = new(); Dictionary<string, string> altTitles = new();
Dictionary<string, string>? links = null; Dictionary<string, string>? links = null;
HashSet<string> tags = new(); HashSet<string> tags = new();
@ -98,11 +99,10 @@ public class Manganato : MangaConnector
break; break;
case "authors": case "authors":
authors = value.Split('-'); authors = value.Split('-');
for (int i = 0; i < authors.Length; i++)
authors[i] = authors[i].Replace("\r\n", "");
break; break;
case "status": case "status":
switch (value.ToLower()) status = value;
switch (status.ToLower())
{ {
case "ongoing": releaseStatus = Manga.ReleaseStatusByte.Continuing; break; case "ongoing": releaseStatus = Manga.ReleaseStatusByte.Continuing; break;
case "completed": releaseStatus = Manga.ReleaseStatusByte.Completed; break; case "completed": releaseStatus = Manga.ReleaseStatusByte.Completed; break;
@ -110,8 +110,6 @@ public class Manganato : MangaConnector
break; break;
case "genres": case "genres":
string[] genres = value.Split(" - "); string[] genres = value.Split(" - ");
for (int i = 0; i < genres.Length; i++)
genres[i] = genres[i].Replace("\r\n", "");
tags = genres.ToHashSet(); tags = genres.ToHashSet();
break; break;
} }
@ -120,7 +118,7 @@ public class Manganato : MangaConnector
string posterUrl = document.DocumentNode.Descendants("span").First(s => s.HasClass("info-image")).Descendants("img").First() string posterUrl = document.DocumentNode.Descendants("span").First(s => s.HasClass("info-image")).Descendants("img").First()
.GetAttributes().First(a => a.Name == "src").Value; .GetAttributes().First(a => a.Name == "src").Value;
string coverFileNameInCache = SaveCoverImageToCache(posterUrl, publicationId, RequestType.MangaCover); string coverFileNameInCache = SaveCoverImageToCache(posterUrl, RequestType.MangaCover);
string description = document.DocumentNode.Descendants("div").First(d => d.HasClass("panel-story-info-description")) string description = document.DocumentNode.Descendants("div").First(d => d.HasClass("panel-story-info-description"))
.InnerText.Replace("Description :", ""); .InnerText.Replace("Description :", "");
@ -132,8 +130,8 @@ public class Manganato : MangaConnector
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 (sortName, authors.ToList(), description, altTitles, tags.ToArray(), posterUrl, coverFileNameInCache, links,
year, originalLanguage, publicationId, releaseStatus, websiteUrl: websiteUrl); year, originalLanguage, status, publicationId, releaseStatus);
AddMangaToCache(manga); cachedPublications.Add(manga);
return manga; return manga;
} }

View File

@ -4,7 +4,6 @@ using System.Text.RegularExpressions;
using System.Xml.Linq; using System.Xml.Linq;
using HtmlAgilityPack; using HtmlAgilityPack;
using Newtonsoft.Json; using Newtonsoft.Json;
using Soenneker.Utils.String.NeedlemanWunsch;
using Tranga.Jobs; using Tranga.Jobs;
namespace Tranga.MangaConnectors; namespace Tranga.MangaConnectors;
@ -42,6 +41,14 @@ public class Mangasee : MangaConnector
SearchResult[] filteredResults = FilteredResults(publicationTitle, searchResults); SearchResult[] filteredResults = FilteredResults(publicationTitle, searchResults);
Log($"Total available manga: {searchResults.Length} Filtered down to: {filteredResults.Length}"); Log($"Total available manga: {searchResults.Length} Filtered down to: {filteredResults.Length}");
/*
Dictionary<SearchResult, int> levenshteinRelation = filteredResults.ToDictionary(result => result,
result =>
{
Log($"Levenshtein {result.s}");
return LevenshteinDistance(publicationTitle.Replace(" ", "").ToLower(), result.s.Replace(" ", "").ToLower());
});
Log($"After levenshtein: {levenshteinRelation.Count}");*/
string[] urls = filteredResults.Select(result => $"https://mangasee123.com/manga/{result.i}").ToArray(); string[] urls = filteredResults.Select(result => $"https://mangasee123.com/manga/{result.i}").ToArray();
List<Manga> searchResultManga = new(); List<Manga> searchResultManga = new();
@ -63,19 +70,42 @@ public class Mangasee : MangaConnector
private SearchResult[] FilteredResults(string publicationTitle, SearchResult[] unfilteredSearchResults) private SearchResult[] FilteredResults(string publicationTitle, SearchResult[] unfilteredSearchResults)
{ {
Dictionary<SearchResult, int> similarity = new(); string[] bannedStrings = {"a", "the", "of", "as", "to", "no", "for", "on", "with", "be", "and", "in", "wa", "at"};
foreach (SearchResult sr in unfilteredSearchResults) string[] cleanSplitPublicationTitle = publicationTitle.Split(' ')
.Where(part => part.Length > 0 && !bannedStrings.Contains(part.ToLower())).ToArray();
return unfilteredSearchResults.Where(usr =>
{ {
List<int> scores = new(); string cleanSearchResultString = string.Join(' ', usr.s.Split(' ').Where(part => part.Length > 0 && !bannedStrings.Contains(part.ToLower())));
foreach (string se in sr.a) foreach(string splitPublicationTitlePart in cleanSplitPublicationTitle)
scores.Add(NeedlemanWunschStringUtil.CalculateSimilarity(se.ToLower(), publicationTitle.ToLower())); if (cleanSearchResultString.Contains(splitPublicationTitlePart, StringComparison.InvariantCultureIgnoreCase) ||
scores.Add(NeedlemanWunschStringUtil.CalculateSimilarity(sr.s.ToLower(), publicationTitle.ToLower())); cleanSearchResultString.Contains(splitPublicationTitlePart, StringComparison.InvariantCultureIgnoreCase))
similarity.Add(sr, scores.Sum() / scores.Count); return true;
return false;
}).ToArray();
}
private int LevenshteinDistance(string a, string b)
{
if (b.Length == 0)
return a.Length;
if (a.Length == 0)
return b.Length;
if (a[0] == b[0])
return LevenshteinDistance(a[1..], b[1..]);
int case1 = LevenshteinDistance(a, b[1..]);
int case2 = LevenshteinDistance(a[1..], b[1..]);
int case3 = LevenshteinDistance(a[1..], b);
if (case1 < case2)
{
return 1 + (case1 < case3 ? case1 : case3);
}
else
{
return 1 + (case2 < case3 ? case2 : case3);
} }
SearchResult[] similarity90 = similarity.Where(s => s.Value < 10).Select(s => s.Key).ToArray();
return similarity90;
} }
public override Manga? GetMangaFromId(string publicationId) public override Manga? GetMangaFromId(string publicationId)
@ -90,11 +120,11 @@ public class Mangasee : MangaConnector
RequestResult requestResult = this.downloadClient.MakeRequest(url, RequestType.MangaInfo); RequestResult requestResult = this.downloadClient.MakeRequest(url, RequestType.MangaInfo);
if((int)requestResult.statusCode < 300 && (int)requestResult.statusCode >= 200 && requestResult.htmlDocument is not null) if((int)requestResult.statusCode < 300 && (int)requestResult.statusCode >= 200 && requestResult.htmlDocument is not null)
return ParseSinglePublicationFromHtml(requestResult.htmlDocument, publicationId, url); return ParseSinglePublicationFromHtml(requestResult.htmlDocument, publicationId);
return null; return null;
} }
private Manga ParseSinglePublicationFromHtml(HtmlDocument document, string publicationId, string websiteUrl) private Manga ParseSinglePublicationFromHtml(HtmlDocument document, string publicationId)
{ {
string originalLanguage = "", status = ""; string originalLanguage = "", status = "";
Dictionary<string, string> altTitles = new(), links = new(); Dictionary<string, string> altTitles = new(), links = new();
@ -103,7 +133,7 @@ public class Mangasee : MangaConnector
HtmlNode posterNode = document.DocumentNode.SelectSingleNode("//div[@class='BoxBody']//div[@class='row']//img"); HtmlNode posterNode = document.DocumentNode.SelectSingleNode("//div[@class='BoxBody']//div[@class='row']//img");
string posterUrl = posterNode.GetAttributeValue("src", ""); string posterUrl = posterNode.GetAttributeValue("src", "");
string coverFileNameInCache = SaveCoverImageToCache(posterUrl, publicationId, RequestType.MangaCover); string coverFileNameInCache = SaveCoverImageToCache(posterUrl, RequestType.MangaCover);
HtmlNode titleNode = document.DocumentNode.SelectSingleNode("//div[@class='BoxBody']//div[@class='row']//h1"); HtmlNode titleNode = document.DocumentNode.SelectSingleNode("//div[@class='BoxBody']//div[@class='row']//h1");
string sortName = titleNode.InnerText; string sortName = titleNode.InnerText;
@ -148,8 +178,8 @@ public class Mangasee : MangaConnector
Manga manga = new(sortName, authors.ToList(), description, altTitles, tags.ToArray(), posterUrl, Manga manga = new(sortName, authors.ToList(), description, altTitles, tags.ToArray(), posterUrl,
coverFileNameInCache, links, coverFileNameInCache, links,
year, originalLanguage, publicationId, releaseStatus, websiteUrl: websiteUrl); year, originalLanguage, status, publicationId, releaseStatus);
AddMangaToCache(manga); cachedPublications.Add(manga);
return manga; return manga;
} }

View File

@ -68,10 +68,10 @@ public class Mangaworld: MangaConnector
Regex idRex = new (@"https:\/\/www\.mangaworld\.[a-z]{0,63}\/manga\/([0-9]+\/[0-9A-z\-]+).*"); Regex idRex = new (@"https:\/\/www\.mangaworld\.[a-z]{0,63}\/manga\/([0-9]+\/[0-9A-z\-]+).*");
string id = idRex.Match(url).Groups[1].Value; string id = idRex.Match(url).Groups[1].Value;
return ParseSinglePublicationFromHtml(requestResult.htmlDocument, id, url); return ParseSinglePublicationFromHtml(requestResult.htmlDocument, id);
} }
private Manga ParseSinglePublicationFromHtml(HtmlDocument document, string publicationId, string websiteUrl) private Manga ParseSinglePublicationFromHtml(HtmlDocument document, string publicationId)
{ {
Dictionary<string, string> altTitles = new(); Dictionary<string, string> altTitles = new();
Dictionary<string, string>? links = null; Dictionary<string, string>? links = null;
@ -111,7 +111,7 @@ public class Mangaworld: MangaConnector
string posterUrl = document.DocumentNode.SelectSingleNode("//img[@class='rounded']").GetAttributeValue("src", ""); string posterUrl = document.DocumentNode.SelectSingleNode("//img[@class='rounded']").GetAttributeValue("src", "");
string coverFileNameInCache = SaveCoverImageToCache(posterUrl, publicationId.Replace('/', '-'), RequestType.MangaCover); string coverFileNameInCache = SaveCoverImageToCache(posterUrl, RequestType.MangaCover);
string description = document.DocumentNode.SelectSingleNode("//div[@id='noidungm']").InnerText; string description = document.DocumentNode.SelectSingleNode("//div[@id='noidungm']").InnerText;
@ -119,8 +119,8 @@ public class Mangaworld: MangaConnector
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 (sortName, authors.ToList(), description, altTitles, tags.ToArray(), posterUrl, coverFileNameInCache, links,
year, originalLanguage, publicationId, releaseStatus, websiteUrl: websiteUrl); year, originalLanguage, status, publicationId, releaseStatus);
AddMangaToCache(manga); cachedPublications.Add(manga);
return manga; return manga;
} }
@ -153,13 +153,10 @@ public class Mangaworld: MangaConnector
{ {
foreach (HtmlNode volNode in document.DocumentNode.SelectNodes("//div[contains(concat(' ',normalize-space(@class),' '),'volume-element')]")) foreach (HtmlNode volNode in document.DocumentNode.SelectNodes("//div[contains(concat(' ',normalize-space(@class),' '),'volume-element')]"))
{ {
string volume = Regex.Match(volNode.SelectNodes("div").First(node => node.HasClass("volume")).SelectSingleNode("p").InnerText, string volume = volNode.SelectNodes("div").First(node => node.HasClass("volume")).SelectSingleNode("p").InnerText.Split(' ')[^1];
@"[Vv]olume ([0-9]+).*").Groups[1].Value;
foreach (HtmlNode chNode in volNode.SelectNodes("div").First(node => node.HasClass("volume-chapters")).SelectNodes("div")) foreach (HtmlNode chNode in volNode.SelectNodes("div").First(node => node.HasClass("volume-chapters")).SelectNodes("div"))
{ {
string number = chNode.SelectSingleNode("a").SelectSingleNode("span").InnerText.Split(" ")[^1];
string number = Regex.Match(chNode.SelectSingleNode("a").SelectSingleNode("span").InnerText,
@"[Cc]apitolo ([0-9]+).*").Groups[1].Value;
string url = chNode.SelectSingleNode("a").GetAttributeValue("href", ""); string url = chNode.SelectSingleNode("a").GetAttributeValue("href", "");
ret.Add(new Chapter(manga, null, volume, number, url)); ret.Add(new Chapter(manga, null, volume, number, url));
} }

View File

@ -1,184 +0,0 @@
using System.Net;
using System.Text.RegularExpressions;
using HtmlAgilityPack;
using Tranga.Jobs;
namespace Tranga.MangaConnectors;
public class ManhuaPlus : MangaConnector
{
public ManhuaPlus(GlobalBase clone) : base(clone, "ManhuaPlus")
{
this.downloadClient = new ChromiumDownloadClient(clone);
}
public override Manga[] GetManga(string publicationTitle = "")
{
Log($"Searching Publications. Term=\"{publicationTitle}\"");
string sanitizedTitle = string.Join(' ', Regex.Matches(publicationTitle, "[A-z]*").Where(str => str.Length > 0)).ToLower();
string requestUrl = $"https://manhuaplus.org/search?keyword={sanitizedTitle}";
RequestResult requestResult =
downloadClient.MakeRequest(requestUrl, RequestType.Default);
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
return Array.Empty<Manga>();
if (requestResult.htmlDocument is null)
return Array.Empty<Manga>();
Manga[] publications = ParsePublicationsFromHtml(requestResult.htmlDocument);
Log($"Retrieved {publications.Length} publications. Term=\"{publicationTitle}\"");
return publications;
}
private Manga[] ParsePublicationsFromHtml(HtmlDocument document)
{
if (document.DocumentNode.SelectSingleNode("//h1/../..").ChildNodes//I already want to not.
.Any(node => node.InnerText.Contains("No manga found")))
return Array.Empty<Manga>();
List<string> urls = document.DocumentNode
.SelectNodes("//h1/../..//a[contains(@href, 'https://manhuaplus.org/manga/') and contains(concat(' ',normalize-space(@class),' '),' clamp ') and not(contains(@href, '/chapter'))]")
.Select(mangaNode => mangaNode.GetAttributeValue("href", "")).ToList();
logger?.WriteLine($"Got {urls.Count} urls.");
HashSet<Manga> ret = new();
foreach (string url in urls)
{
Manga? manga = GetMangaFromUrl(url);
if (manga is not null)
ret.Add((Manga)manga);
}
return ret.ToArray();
}
public override Manga? GetMangaFromId(string publicationId)
{
return GetMangaFromUrl($"https://manhuaplus.org/manga/{publicationId}");
}
public override Manga? GetMangaFromUrl(string url)
{
Regex publicationIdRex = new(@"https:\/\/manhuaplus.org\/manga\/(.*)(\/.*)*");
string publicationId = publicationIdRex.Match(url).Groups[1].Value;
RequestResult requestResult = this.downloadClient.MakeRequest(url, RequestType.MangaInfo);
if((int)requestResult.statusCode < 300 && (int)requestResult.statusCode >= 200 && requestResult.htmlDocument is not null && requestResult.redirectedToUrl != "https://manhuaplus.org/home") //When manga doesnt exists it redirects to home
return ParseSinglePublicationFromHtml(requestResult.htmlDocument, publicationId, url);
return null;
}
private Manga ParseSinglePublicationFromHtml(HtmlDocument document, string publicationId, string websiteUrl)
{
string originalLanguage = "", status = "";
Dictionary<string, string> altTitles = new(), links = new();
HashSet<string> tags = new();
Manga.ReleaseStatusByte releaseStatus = Manga.ReleaseStatusByte.Unreleased;
HtmlNode posterNode = document.DocumentNode.SelectSingleNode("/html/body/main/div/div/div[2]/div[1]/figure/a/img");//BRUH
Regex posterRex = new(@".*(\/uploads/covers/[a-zA-Z0-9\-\._\~\!\$\&\'\(\)\*\+\,\;\=\:\@]+).*");
string posterUrl = $"https://manhuaplus.org/{posterRex.Match(posterNode.GetAttributeValue("src", "")).Groups[1].Value}";
string coverFileNameInCache = SaveCoverImageToCache(posterUrl, publicationId, RequestType.MangaCover);
HtmlNode titleNode = document.DocumentNode.SelectSingleNode("//h1");
string sortName = titleNode.InnerText.Replace("\n", "");
HtmlNode[] authorsNodes = document.DocumentNode
.SelectNodes("//a[contains(@href, 'https://manhuaplus.org/authors/')]")
.ToArray();
List<string> authors = new();
foreach (HtmlNode authorNode in authorsNodes)
authors.Add(authorNode.InnerText);
HtmlNode[] genreNodes = document.DocumentNode
.SelectNodes("//a[contains(@href, 'https://manhuaplus.org/genres/')]").ToArray();
foreach (HtmlNode genreNode in genreNodes)
tags.Add(genreNode.InnerText.Replace("\n", ""));
string yearNodeStr = document.DocumentNode
.SelectSingleNode("//aside//i[contains(concat(' ',normalize-space(@class),' '),' fa-clock ')]/../span").InnerText.Replace("\n", "");
int year = int.Parse(yearNodeStr.Split(' ')[0].Split('/')[^1]);
status = document.DocumentNode.SelectSingleNode("//aside//i[contains(concat(' ',normalize-space(@class),' '),' fa-rss ')]/../span").InnerText.Replace("\n", "");
switch (status.ToLower())
{
case "cancelled": releaseStatus = Manga.ReleaseStatusByte.Cancelled; break;
case "hiatus": releaseStatus = Manga.ReleaseStatusByte.OnHiatus; break;
case "discontinued": releaseStatus = Manga.ReleaseStatusByte.Cancelled; break;
case "complete": releaseStatus = Manga.ReleaseStatusByte.Completed; break;
case "ongoing": releaseStatus = Manga.ReleaseStatusByte.Continuing; break;
}
HtmlNode descriptionNode = document.DocumentNode
.SelectSingleNode("//div[@id='syn-target']");
string description = descriptionNode.InnerText;
Manga manga = new(sortName, authors.ToList(), description, altTitles, tags.ToArray(), posterUrl,
coverFileNameInCache, links,
year, originalLanguage, publicationId, releaseStatus, websiteUrl: websiteUrl);
AddMangaToCache(manga);
return manga;
}
public override Chapter[] GetChapters(Manga manga, string language="en")
{
Log($"Getting chapters {manga}");
RequestResult result = downloadClient.MakeRequest($"https://manhuaplus.org/manga/{manga.publicationId}", RequestType.Default);
if ((int)result.statusCode < 200 || (int)result.statusCode >= 300 || result.htmlDocument is null)
{
return Array.Empty<Chapter>();
}
HtmlNodeCollection chapterNodes = result.htmlDocument.DocumentNode.SelectNodes("//li[contains(concat(' ',normalize-space(@class),' '),' chapter ')]//a");
string[] urls = chapterNodes.Select(node => node.GetAttributeValue("href", "")).ToArray();
Regex urlRex = new (@".*\/chapter-([0-9\-]+).*");
List<Chapter> chapters = new();
foreach (string url in urls)
{
Match rexMatch = urlRex.Match(url);
string volumeNumber = "1";
string chapterNumber = rexMatch.Groups[1].Value;
string fullUrl = url;
chapters.Add(new Chapter(manga, "", volumeNumber, chapterNumber, fullUrl));
}
//Return Chapters ordered by Chapter-Number
Log($"Got {chapters.Count} chapters. {manga}");
return chapters.Order().ToArray();
}
public override HttpStatusCode DownloadChapter(Chapter chapter, ProgressToken? progressToken = null)
{
if (progressToken?.cancellationRequested ?? false)
{
progressToken.Cancel();
return HttpStatusCode.RequestTimeout;
}
Manga chapterParentManga = chapter.parentManga;
if (progressToken?.cancellationRequested ?? false)
{
progressToken.Cancel();
return HttpStatusCode.RequestTimeout;
}
Log($"Retrieving chapter-info {chapter} {chapterParentManga}");
RequestResult requestResult = this.downloadClient.MakeRequest(chapter.url, RequestType.Default);
if (requestResult.htmlDocument is null)
{
progressToken?.Cancel();
return HttpStatusCode.RequestTimeout;
}
HtmlDocument document = requestResult.htmlDocument;
HtmlNode[] images = document.DocumentNode.SelectNodes("//a[contains(concat(' ',normalize-space(@class),' '),' readImg ')]/img").ToArray();
List<string> urls = images.Select(node => node.GetAttributeValue("src", "")).ToList();
string comicInfoPath = Path.GetTempFileName();
File.WriteAllText(comicInfoPath, chapter.GetComicInfoXmlString());
return DownloadChapterImages(urls.ToArray(), chapter.GetArchiveFilePath(settings.downloadLocation), RequestType.MangaImage, comicInfoPath, progressToken:progressToken);
}
}

View File

@ -28,7 +28,7 @@ public class NotificationManagerJsonConverter : JsonConverter
case (byte)NotificationConnector.NotificationConnectorType.LunaSea: case (byte)NotificationConnector.NotificationConnectorType.LunaSea:
return new LunaSea(this._clone, jo.GetValue("id")!.Value<string>()!); return new LunaSea(this._clone, jo.GetValue("id")!.Value<string>()!);
case (byte)NotificationConnector.NotificationConnectorType.Ntfy: case (byte)NotificationConnector.NotificationConnectorType.Ntfy:
return new Ntfy(this._clone, jo.GetValue("endpoint")!.Value<string>()!, jo.GetValue("topic")!.Value<string>()!, jo.GetValue("auth")!.Value<string>()!); return new Ntfy(this._clone, jo.GetValue("endpoint")!.Value<string>()!, jo.GetValue("auth")!.Value<string>()!);
} }
throw new Exception(); throw new Exception();

View File

@ -1,63 +1,34 @@
using System.Text; using System.Text;
using System.Text.RegularExpressions;
using Newtonsoft.Json; using Newtonsoft.Json;
namespace Tranga.NotificationConnectors; namespace Tranga.NotificationConnectors;
public class Ntfy : NotificationConnector public class Ntfy : NotificationConnector
{ {
// ReSharper disable twice MemberCanBePrivate.Global // ReSharper disable once MemberCanBePrivate.Global
public string endpoint { get; init; } public string endpoint { get; init; }
public string auth { get; init; } private string auth { get; init; }
public string topic { get; init; } private const string Topic = "tranga";
private readonly HttpClient _client = new(); private readonly HttpClient _client = new();
[JsonConstructor]
public Ntfy(GlobalBase clone, string endpoint, string topic, string auth) : base(clone, NotificationConnectorType.Ntfy)
{
this.endpoint = endpoint;
this.topic = topic;
this.auth = auth;
}
public Ntfy(GlobalBase clone, string endpoint, string username, string password, string? topic = null) : [JsonConstructor]
this(clone, EndpointAndTopicFromUrl(endpoint)[0], topic??EndpointAndTopicFromUrl(endpoint)[1], AuthFromUsernamePassword(username, password)) public Ntfy(GlobalBase clone, string endpoint, string auth) : base(clone, NotificationConnectorType.Ntfy)
{ {
if (!baseUrlRex.IsMatch(endpoint))
} throw new ArgumentException("endpoint does not match pattern");
this.endpoint = endpoint;
private static string AuthFromUsernamePassword(string username, string password) this.auth = auth;
{
string authHeader = "Basic " + Convert.ToBase64String(Encoding.UTF8.GetBytes($"{username}:{password}"));
string authParam = Convert.ToBase64String(Encoding.UTF8.GetBytes(authHeader)).Replace("=","");
return authParam;
}
private static string[] EndpointAndTopicFromUrl(string url)
{
string[] ret = new string[2];
if (!baseUrlRex.IsMatch(url))
throw new ArgumentException("url does not match pattern");
Regex rootUriRex = new(@"(https?:\/\/[a-zA-Z0-9-\.]+\.[a-zA-Z0-9]+)(?:\/([a-zA-Z0-9-\.]+))?.*");
Match match = rootUriRex.Match(url);
if(!match.Success)
throw new ArgumentException($"Error getting URI from provided endpoint-URI: {url}");
ret[0] = match.Groups[1].Value;
ret[1] = match.Groups[2].Success && match.Groups[2].Value.Length > 0 ? match.Groups[2].Value : "tranga";
return ret;
} }
public override string ToString() public override string ToString()
{ {
return $"Ntfy {endpoint} {topic}"; return $"Ntfy {endpoint} {Topic}";
} }
public override void SendNotification(string title, string notificationText) public override void SendNotification(string title, string notificationText)
{ {
Log($"Sending notification: {title} - {notificationText}"); Log($"Sending notification: {title} - {notificationText}");
MessageData message = new(title, topic, notificationText); MessageData message = new(title, notificationText);
HttpRequestMessage request = new(HttpMethod.Post, $"{this.endpoint}?auth={this.auth}"); HttpRequestMessage request = new(HttpMethod.Post, $"{this.endpoint}?auth={this.auth}");
request.Content = new StringContent(JsonConvert.SerializeObject(message, Formatting.None), Encoding.UTF8, "application/json"); request.Content = new StringContent(JsonConvert.SerializeObject(message, Formatting.None), Encoding.UTF8, "application/json");
HttpResponseMessage response = _client.Send(request); HttpResponseMessage response = _client.Send(request);
@ -76,9 +47,9 @@ public class Ntfy : NotificationConnector
public string message { get; } public string message { get; }
public int priority { get; } public int priority { get; }
public MessageData(string title, string topic, string message) public MessageData(string title, string message)
{ {
this.topic = topic; this.topic = Topic;
this.title = title; this.title = title;
this.message = message; this.message = message;
this.priority = 3; this.priority = 3;

View File

@ -122,7 +122,7 @@ public class Server : GlobalBase
break; break;
} }
string filePath = manga?.coverFileNameInCache ?? ""; string filePath = settings.GetFullCoverPath((Manga)manga!);
if (File.Exists(filePath)) if (File.Exists(filePath))
{ {
FileStream coverStream = new(filePath, FileMode.Open); FileStream coverStream = new(filePath, FileMode.Open);
@ -410,7 +410,7 @@ public class Server : GlobalBase
break; break;
case "Settings/AprilFoolsMode": case "Settings/AprilFoolsMode":
if (!requestVariables.TryGetValue("enabled", out string? aprilFoolsModeEnabledStr) || if (!requestVariables.TryGetValue("enabled", out string? aprilFoolsModeEnabledStr) ||
!bool.TryParse(aprilFoolsModeEnabledStr, out bool aprilFoolsModeEnabled)) bool.TryParse(aprilFoolsModeEnabledStr, out bool aprilFoolsModeEnabled))
{ {
SendResponse(HttpStatusCode.BadRequest, response); SendResponse(HttpStatusCode.BadRequest, response);
break; break;
@ -492,13 +492,12 @@ public class Server : GlobalBase
}else if (notificationConnectorType is NotificationConnector.NotificationConnectorType.Ntfy) }else if (notificationConnectorType is NotificationConnector.NotificationConnectorType.Ntfy)
{ {
if (!requestVariables.TryGetValue("ntfyUrl", out string? ntfyUrl) || if (!requestVariables.TryGetValue("ntfyUrl", out string? ntfyUrl) ||
!requestVariables.TryGetValue("ntfyUser", out string? ntfyUser)|| !requestVariables.TryGetValue("ntfyAuth", out string? ntfyAuth))
!requestVariables.TryGetValue("ntfyPass", out string? ntfyPass))
{ {
SendResponse(HttpStatusCode.BadRequest, response); SendResponse(HttpStatusCode.BadRequest, response);
break; break;
} }
AddNotificationConnector(new Ntfy(this, ntfyUrl, ntfyUser, ntfyPass, null)); AddNotificationConnector(new Ntfy(this, ntfyUrl, ntfyAuth));
SendResponse(HttpStatusCode.Accepted, response); SendResponse(HttpStatusCode.Accepted, response);
} }
else else
@ -535,13 +534,12 @@ public class Server : GlobalBase
}else if (notificationConnectorType is NotificationConnector.NotificationConnectorType.Ntfy) }else if (notificationConnectorType is NotificationConnector.NotificationConnectorType.Ntfy)
{ {
if (!requestVariables.TryGetValue("ntfyUrl", out string? ntfyUrl) || if (!requestVariables.TryGetValue("ntfyUrl", out string? ntfyUrl) ||
!requestVariables.TryGetValue("ntfyUser", out string? ntfyUser)|| !requestVariables.TryGetValue("ntfyAuth", out string? ntfyAuth))
!requestVariables.TryGetValue("ntfyPass", out string? ntfyPass))
{ {
SendResponse(HttpStatusCode.BadRequest, response); SendResponse(HttpStatusCode.BadRequest, response);
break; break;
} }
notificationConnector = new Ntfy(this, ntfyUrl, ntfyUser, ntfyPass, null); notificationConnector = new Ntfy(this, ntfyUrl, ntfyAuth);
} }
else else
{ {

View File

@ -24,8 +24,7 @@ public partial class Tranga : GlobalBase
new MangaKatana(this), new MangaKatana(this),
new Mangaworld(this), new Mangaworld(this),
new Bato(this), new Bato(this),
new MangaLife(this), new MangaLife(this)
new ManhuaPlus(this)
}; };
foreach(DirectoryInfo dir in new DirectoryInfo(Path.GetTempPath()).GetDirectories("trangatemp"))//Cleanup old temp folders foreach(DirectoryInfo dir in new DirectoryInfo(Path.GetTempPath()).GetDirectories("trangatemp"))//Cleanup old temp folders
dir.Delete(); dir.Delete();
@ -55,7 +54,12 @@ public partial class Tranga : GlobalBase
return _connectors; return _connectors;
} }
public Manga? GetPublicationById(string internalId) => GetCachedManga(internalId); public Manga? GetPublicationById(string internalId)
{
if (cachedPublications.Exists(publication => publication.internalId == internalId))
return cachedPublications.First(publication => publication.internalId == internalId);
return null;
}
public bool TryGetPublicationById(string internalId, out Manga? manga) public bool TryGetPublicationById(string internalId, out Manga? manga)
{ {

View File

@ -8,11 +8,9 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="GlaxArguments" Version="1.1.0" />
<PackageReference Include="HtmlAgilityPack" Version="1.11.46" /> <PackageReference Include="HtmlAgilityPack" Version="1.11.46" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="PuppeteerSharp" Version="10.0.0" /> <PackageReference Include="PuppeteerSharp" Version="10.0.0" />
<PackageReference Include="Soenneker.Utils.String.NeedlemanWunsch" Version="2.1.301" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@ -1,5 +1,4 @@
using Logging; using Logging;
using GlaxArguments;
namespace Tranga; namespace Tranga;
@ -8,53 +7,46 @@ public partial class Tranga : GlobalBase
public static void Main(string[] args) public static void Main(string[] args)
{ {
Argument downloadLocation = new (new[] { "-d", "--downloadLocation" }, 1, "Directory to which downloaded Manga are saved"); Console.WriteLine(string.Join(' ', args));
Argument workingDirectory = new (new[] { "-w", "--workingDirectory" }, 1, "Directory in which application-data is saved"); string[]? help = GetArg(args, ArgEnum.Help);
Argument consoleLogger = new (new []{"-c", "--consoleLogger"}, 0, "Enables the consoleLogger"); if (help is not null)
Argument fileLogger = new (new []{"-f", "--fileLogger"}, 0, "Enables the fileLogger");
Argument fPath = new (new []{"-l", "--fPath"}, 1, "Log Folder Path");
Argument[] arguments = new[]
{ {
downloadLocation, PrintHelp();
workingDirectory, return;
consoleLogger, }
fileLogger,
fPath string[]? consoleLogger = GetArg(args, ArgEnum.ConsoleLogger);
}; string[]? fileLogger = GetArg(args, ArgEnum.FileLogger);
ArgumentFetcher fetcher = new (arguments); string? directoryPath = GetArg(args, ArgEnum.FileLoggerPath)?[0];
Dictionary<Argument, string[]> fetched = fetcher.Fetch(args);
string? directoryPath = fetched.TryGetValue(fPath, out string[]? path) ? path[0] : null;
if (directoryPath is not null && !Directory.Exists(directoryPath)) if (directoryPath is not null && !Directory.Exists(directoryPath))
Directory.CreateDirectory(directoryPath); Directory.CreateDirectory(directoryPath);
List<Logger.LoggerType> enabledLoggers = new(); List<Logger.LoggerType> enabledLoggers = new();
if(fetched.ContainsKey(consoleLogger)) if(consoleLogger is not null)
enabledLoggers.Add(Logger.LoggerType.ConsoleLogger); enabledLoggers.Add(Logger.LoggerType.ConsoleLogger);
if (fetched.ContainsKey(fileLogger)) if (fileLogger is not null)
enabledLoggers.Add(Logger.LoggerType.FileLogger); enabledLoggers.Add(Logger.LoggerType.FileLogger);
Logger logger = new(enabledLoggers.ToArray(), Console.Out, Console.OutputEncoding, directoryPath); Logger logger = new(enabledLoggers.ToArray(), Console.Out, Console.OutputEncoding, directoryPath);
TrangaSettings? settings = null; TrangaSettings? settings = null;
bool dlp = fetched.TryGetValue(downloadLocation, out string[]? downloadLocationPath); string[]? downloadLocationPath = GetArg(args, ArgEnum.DownloadLocation);
bool wdp = fetched.TryGetValue(downloadLocation, out string[]? workingDirectoryPath); string[]? workingDirectory = GetArg(args, ArgEnum.WorkingDirectory);
if (dlp && wdp) if (downloadLocationPath is not null && workingDirectory is not null)
{ {
settings = new TrangaSettings(downloadLocationPath![0], workingDirectoryPath![0]); settings = new TrangaSettings(downloadLocationPath[0], workingDirectory[0]);
}else if (dlp) }else if (downloadLocationPath is not null)
{ {
if (settings is null) if (settings is null)
settings = new TrangaSettings(downloadLocation: downloadLocationPath![0]); settings = new TrangaSettings(downloadLocation: downloadLocationPath[0]);
else else
settings = new TrangaSettings(downloadLocation: downloadLocationPath![0], settings.workingDirectory); settings = new TrangaSettings(downloadLocation: downloadLocationPath[0], settings.workingDirectory);
}else if (wdp) }else if (workingDirectory is not null)
{ {
if (settings is null) if (settings is null)
settings = new TrangaSettings(downloadLocation: workingDirectoryPath![0]); settings = new TrangaSettings(downloadLocation: workingDirectory[0]);
else else
settings = new TrangaSettings(settings.downloadLocation, workingDirectoryPath![0]); settings = new TrangaSettings(settings.downloadLocation, workingDirectory[0]);
} }
else else
{ {
@ -66,4 +58,84 @@ public partial class Tranga : GlobalBase
Tranga _ = new (logger, settings); Tranga _ = new (logger, settings);
} }
private static void PrintHelp()
{
Console.WriteLine("Tranga-Help:");
foreach (Argument argument in Arguments.Values)
{
foreach(string name in argument.names)
Console.Write("{0} ", name);
if(argument.parameterCount > 0)
Console.Write($"<{argument.parameterCount}>");
Console.Write("\r\n {0}\r\n", argument.helpText);
}
}
/// <summary>
/// Returns an array containing the parameters for the argument.
/// </summary>
/// <param name="args">List of argument-strings</param>
/// <param name="arg">Requested parameter</param>
/// <returns>
/// If there are no parameters for an argument, returns an empty array.
/// If the argument is not found returns null.
/// </returns>
private static string[]? GetArg(string[] args, ArgEnum arg)
{
List<string> argsList = args.ToList();
List<string> ret = new();
foreach (string name in Arguments[arg].names)
{
int argIndex = argsList.IndexOf(name);
if (argIndex != -1)
{
if (Arguments[arg].parameterCount == 0)
return ret.ToArray();
for (int parameterIndex = 1; parameterIndex <= Arguments[arg].parameterCount; parameterIndex++)
{
if(argIndex + parameterIndex >= argsList.Count || args[argIndex + parameterIndex].Contains('-'))//End of arguments, or no parameter provided, when one is required
Console.WriteLine($"No parameter provided for argument {name}. -h for help.");
ret.Add(args[argIndex + parameterIndex]);
}
}
}
return ret.Any() ? ret.ToArray() : null;
}
private static readonly Dictionary<ArgEnum, Argument> Arguments = new()
{
{ ArgEnum.DownloadLocation, new(new []{"-d", "--downloadLocation"}, 1, "Directory to which downloaded Manga are saved") },
{ ArgEnum.WorkingDirectory, new(new []{"-w", "--workingDirectory"}, 1, "Directory in which application-data is saved") },
{ ArgEnum.ConsoleLogger, new(new []{"-c", "--consoleLogger"}, 0, "Enables the consoleLogger") },
{ ArgEnum.FileLogger, new(new []{"-f", "--fileLogger"}, 0, "Enables the fileLogger") },
{ ArgEnum.FileLoggerPath, new (new []{"-l", "--fPath"}, 1, "Log Folder Path" ) },
{ ArgEnum.Help, new(new []{"-h", "--help"}, 0, "Print this") }
//{ ArgEnum., new(new []{""}, 1, "") }
};
internal enum ArgEnum
{
TrangaSettings,
DownloadLocation,
WorkingDirectory,
ConsoleLogger,
FileLogger,
FileLoggerPath,
Help
}
private struct Argument
{
public string[] names { get; }
public byte parameterCount { get; }
public string helpText { get; }
public Argument(string[] names, byte parameterCount, string helpText)
{
this.names = names;
this.parameterCount = parameterCount;
this.helpText = helpText;
}
}
} }