Merge branch 'cuttingedge' into json-api

Solved Merge conflicts with cuttingedge branch
This commit is contained in:
db-2001 2024-04-18 17:56:44 -04:00
commit dbc1b94124
16 changed files with 227 additions and 201 deletions

View File

@ -20,7 +20,7 @@ jobs:
# 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.1.0 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@v4.1.1 uses: docker/build-push-action@v5.3.0
with: with:
context: ./ context: ./
file: ./Dockerfile-base file: ./Dockerfile-base

View File

@ -22,7 +22,7 @@ jobs:
# 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.1.0 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@v4.1.1 uses: docker/build-push-action@v5.3.0
with: with:
context: ./ context: ./
file: ./Dockerfile file: ./Dockerfile

45
.github/workflows/docker-image-dev.yml vendored Normal file
View File

@ -0,0 +1,45 @@
name: Docker Image CI
on:
push:
branches: [ "dev" ]
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
# https://github.com/docker/setup-qemu-action#usage
- name: Set up QEMU
uses: docker/setup-qemu-action@v2.2.0
# https://github.com/marketplace/actions/docker-setup-buildx
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v3.3.0
# https://github.com/docker/login-action#docker-hub
- name: Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
# https://github.com/docker/build-push-action#multi-platform-image
- name: Build and push API
uses: docker/build-push-action@v5.3.0
with:
context: ./
file: ./Dockerfile
#platforms: linux/amd64,linux/arm64,linux/riscv64,linux/ppc64le,linux/s390x,linux/386,linux/mips64le,linux/mips64,linux/arm/v7,linux/arm/v6
platforms: linux/amd64
pull: true
push: true
tags: |
glax/tranga-api:dev

View File

@ -22,7 +22,7 @@ jobs:
# 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.1.0 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@v4.1.1 uses: docker/build-push-action@v5.3.0
with: with:
context: ./ context: ./
file: ./Dockerfile file: ./Dockerfile

View File

@ -44,8 +44,8 @@ internal sealed class TrangaCli : Command<TrangaCli.Settings>
if(settings.fileLogger is true) if(settings.fileLogger is true)
enabledLoggers.Add(Logger.LoggerType.FileLogger); enabledLoggers.Add(Logger.LoggerType.FileLogger);
string? logFilePath = settings.fileLoggerPath ?? ""; string? logFolderPath = settings.fileLoggerPath ?? "";
Logger logger = new(enabledLoggers.ToArray(), Console.Out, Console.OutputEncoding, logFilePath); Logger logger = new(enabledLoggers.ToArray(), Console.Out, Console.OutputEncoding, logFolderPath);
TrangaSettings? trangaSettings = null; TrangaSettings? trangaSettings = null;

View File

@ -20,17 +20,17 @@ public class Logger : TextWriter
private readonly FormattedConsoleLogger? _formattedConsoleLogger; private readonly FormattedConsoleLogger? _formattedConsoleLogger;
private readonly MemoryLogger _memoryLogger; private readonly MemoryLogger _memoryLogger;
public Logger(LoggerType[] enabledLoggers, TextWriter? stdOut, Encoding? encoding, string? logFilePath) public Logger(LoggerType[] enabledLoggers, TextWriter? stdOut, Encoding? encoding, string? logFolderPath)
{ {
this.Encoding = encoding ?? Encoding.UTF8; this.Encoding = encoding ?? Encoding.UTF8;
if(enabledLoggers.Contains(LoggerType.FileLogger) && (logFilePath is null || logFilePath == "")) DateTime now = DateTime.Now;
if(enabledLoggers.Contains(LoggerType.FileLogger) && (logFolderPath is null || logFolderPath == ""))
{ {
DateTime now = DateTime.Now; string filePath = Path.Join(LogDirectoryPath,
logFilePath = Path.Join(LogDirectoryPath,
$"{now.ToShortDateString()}_{now.Hour}-{now.Minute}-{now.Second}.log"); $"{now.ToShortDateString()}_{now.Hour}-{now.Minute}-{now.Second}.log");
_fileLogger = new FileLogger(logFilePath, encoding); _fileLogger = new FileLogger(filePath, encoding);
}else if (enabledLoggers.Contains(LoggerType.FileLogger) && logFilePath is not null) }else if (enabledLoggers.Contains(LoggerType.FileLogger) && logFolderPath is not null)
_fileLogger = new FileLogger(logFilePath, encoding); _fileLogger = new FileLogger(Path.Join(logFolderPath, $"{now.ToShortDateString()}_{now.Hour}-{now.Minute}-{now.Second}.log") , encoding);
if (enabledLoggers.Contains(LoggerType.ConsoleLogger) && stdOut is not null) if (enabledLoggers.Contains(LoggerType.ConsoleLogger) && stdOut is not null)
@ -43,6 +43,7 @@ public class Logger : TextWriter
throw new ArgumentException($"stdOut can not be null for LoggerType {LoggerType.ConsoleLogger}"); throw new ArgumentException($"stdOut can not be null for LoggerType {LoggerType.ConsoleLogger}");
} }
_memoryLogger = new MemoryLogger(encoding); _memoryLogger = new MemoryLogger(encoding);
WriteLine(GetType().ToString(), $"Logfile: {logFilePath}");
} }
public void WriteLine(string caller, string? value) public void WriteLine(string caller, string? value)

View File

@ -1,12 +1,3 @@
<!-- PROJECT SHIELDS -->
<!--
*** I'm using markdown "reference style" links for readability.
*** Reference links are enclosed in brackets [ ] instead of parentheses ( ).
*** See the bottom of this document for the declaration of the reference variables
*** for contributors-url, forks-url, etc. This is an optional, concise syntax you may use.
*** https://www.markdownguide.org/basic-syntax/#reference-style-links
-->
<!-- PROJECT LOGO --> <!-- PROJECT LOGO -->
<br /> <br />
<div align="center"> <div align="center">
@ -61,8 +52,8 @@ Tranga can download Chapters and Metadata from "Scanlation" sites such as
- [Manga4Life](https://manga4life.com) (en) - [Manga4Life](https://manga4life.com) (en)
- ❓ Open an [issue](https://github.com/C9Glax/tranga/issues) - ❓ Open an [issue](https://github.com/C9Glax/tranga/issues)
and trigger an scan with [Komga](https://komga.org/) and [Kavita](https://www.kavitareader.com/). and trigger a library-scan with [Komga](https://komga.org/) and [Kavita](https://www.kavitareader.com/).
Notifications will can sent to your devices using [Gotify](https://gotify.net/) and [LunaSea](https://www.lunasea.app/). Notifications can be sent to your devices using [Gotify](https://gotify.net/) and [LunaSea](https://www.lunasea.app/).
### What this does and doesn't do ### What this does and doesn't do
@ -75,15 +66,15 @@ This project downloads the images for a Manga from the specified Scanlation-Webs
It does this on an interval, and checks for any Chapters (.cbz-Archive) not already existing in your specified Download-Location. (If you rename or move files, it will download those again) It does this on an interval, and checks for any Chapters (.cbz-Archive) not already existing in your specified Download-Location. (If you rename or move files, it will download those again)
Tranga can (if configured) trigger a scan in Komga or Kavita, however the directory in which the Manga reside has to be available to both Tranga and Komga/Kavita. Tranga can (if configured) trigger a scan in Komga or Kavita, however the directory in which the Manga reside has to be available to both Tranga and Komga/Kavita.
The project doesn't manage metadata, doesn't curate, change or enhance any information that isn't available on the selected Scanlation-Site. The project doesn't manage metadata, and doesn't curate, change or enhance any information that isn't available on the selected Scanlation-Site.
It will blindly use whatever is scrapes (yes this is a glorified Web-scraper). It will blindly use whatever is scrapes (yes this is a glorified Web-scraper).
### Inspiration: ### Inspiration:
Because [Kaizoku](https://github.com/oae/kaizoku) was relying on [mangal](https://github.com/metafates/mangal) and mangal Because [Kaizoku](https://github.com/oae/kaizoku) was relying on [mangal](https://github.com/metafates/mangal) and mangal
hasn't received bugfixes for it's issues with Titles not showing up, or throwing errors because of illegal characters, hasn't received bugfixes for its issues with Titles not showing up, or throwing errors because of illegal characters,
there were no alternatives for automatic downloads. However [Kaizoku](https://github.com/oae/kaizoku) certainly had a great Web-UI. there were no alternatives for automatic downloads. However, [Kaizoku](https://github.com/oae/kaizoku) certainly had a great Web-UI.
That is why I wanted to create my own project, in a language I understand, and that I am able to maintain myself. That is why I wanted to create my own project, in a language I understand, and that I am able to maintain myself.
@ -102,25 +93,15 @@ That is why I wanted to create my own project, in a language I understand, and t
<!-- GETTING STARTED --> <!-- GETTING STARTED -->
## Getting Started ## Getting Started
There is two release types:
- CLI
- Docker
### CLI
Head over to [releases](https://git.bernloehr.eu/glax/Tranga/releases) and download.
~~The CLI will guide you through setup.~~ Not in the current version.
Right now it is barebones with options to view logs and make HTTP-Requests
### Docker ### Docker
Download [docker-compose.yaml](https://git.bernloehr.eu/glax/Tranga/src/branch/master/docker-compose.yaml) and configure to your needs. Download [docker-compose.yaml](https://git.bernloehr.eu/glax/Tranga/src/branch/master/docker-compose.yaml) and configure to your needs.
Mount `/Manga` to wherever you want your chapters (`.cbz`-Archives) downloaded (for exampled where Komga/Kavita can access them). Mount `/Manga` to wherever you want your chapters (`.cbz`-Archives) downloaded (where Komga/Kavita can access them).
The `docker-compose` also includes [tranga-website](https://github.com/C9Glax/tranga-website) as frontend. For its configuration refer to the repo README. The `docker-compose` also includes [tranga-website](https://github.com/C9Glax/tranga-website) as frontend. For its configuration refer to the repo README.
For compatibility do not execute the compose as root (which you should not do anyways...) but as user that can
access the folder.
### Prerequisites ### Prerequisites
#### To Build #### To Build
@ -131,7 +112,6 @@ The `docker-compose` also includes [tranga-website](https://github.com/C9Glax/tr
<!-- ROADMAP --> <!-- ROADMAP -->
## Roadmap ## Roadmap
- [ ] Docker ARM support
- [ ] ❓ - [ ] ❓
See the [open issues](https://github.com/C9Glax/tranga/issues) for a full list of proposed features (and known issues). See the [open issues](https://github.com/C9Glax/tranga/issues) for a full list of proposed features (and known issues).

View File

@ -19,7 +19,7 @@ public readonly struct Chapter : IComparable
public string fileName { get; } public string fileName { get; }
private static readonly Regex LegalCharacters = new (@"([A-z]*[0-9]* *\.*-*,*\]*\[*'*\'*\)*\(*~*!*)*"); private static readonly Regex LegalCharacters = new (@"([A-z]*[0-9]* *\.*-*,*\]*\[*'*\'*\)*\(*~*!*)*");
private static readonly Regex IllegalStrings = new(@"Vol(ume)?.?", RegexOptions.IgnoreCase); private static readonly Regex IllegalStrings = new(@"(Vol(ume)?|Ch(apter)?)\.?", RegexOptions.IgnoreCase);
private static readonly Regex Digits = new(@"[0-9\.]*"); private static readonly Regex Digits = new(@"[0-9\.]*");
public Chapter(Manga parentManga, string? name, string? volumeNumber, string chapterNumber, string url) public Chapter(Manga parentManga, string? name, string? volumeNumber, string chapterNumber, string url)
{ {

View File

@ -10,6 +10,7 @@ internal class ChromiumDownloadClient : DownloadClient
{ {
private IBrowser browser { get; set; } private IBrowser browser { get; set; }
private const string ChromiumVersion = "1154303"; private const string ChromiumVersion = "1154303";
private const int StartTimeoutMs = 30000;
private async Task<IBrowser> DownloadBrowser() private async Task<IBrowser> DownloadBrowser()
{ {
@ -40,7 +41,7 @@ internal class ChromiumDownloadClient : DownloadClient
await browserFetcher.DownloadAsync(ChromiumVersion); await browserFetcher.DownloadAsync(ChromiumVersion);
} }
Log("Starting Browser."); Log($"Starting Browser. ({StartTimeoutMs}ms timeout)");
return await Puppeteer.LaunchAsync(new LaunchOptions return await Puppeteer.LaunchAsync(new LaunchOptions
{ {
Headless = true, Headless = true,
@ -50,7 +51,7 @@ internal class ChromiumDownloadClient : DownloadClient
"--disable-dev-shm-usage", "--disable-dev-shm-usage",
"--disable-setuid-sandbox", "--disable-setuid-sandbox",
"--no-sandbox"}, "--no-sandbox"},
Timeout = 10000 Timeout = StartTimeoutMs
}); });
} }

View File

@ -241,6 +241,15 @@ public abstract class MangaConnector : GlobalBase
int chapter = 0; int chapter = 0;
//Download all Images to temporary Folder //Download all Images to temporary Folder
if (imageUrls.Length == 0)
{
Log("No images found");
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
File.SetUnixFileMode(saveArchiveFilePath, UserRead | UserWrite | UserExecute | GroupRead | GroupWrite | GroupExecute);
Directory.Delete(tempFolder, true);
progressToken?.Complete();
return HttpStatusCode.NoContent;
}
foreach (string imageUrl in imageUrls) foreach (string imageUrl in imageUrls)
{ {
string extension = imageUrl.Split('.')[^1].Split('?')[0]; string extension = imageUrl.Split('.')[^1].Split('?')[0];

View File

@ -20,12 +20,18 @@ public class MangaDex : MangaConnector
int total = int.MaxValue; //How many total results are there, is updated on first request int total = int.MaxValue; //How many total results are there, is updated on first request
HashSet<Manga> retManga = new(); HashSet<Manga> retManga = new();
int loadedPublicationData = 0; int loadedPublicationData = 0;
List<JsonNode> results = new();
//Request all search-results
while (offset < total) //As long as we haven't requested all "Pages" while (offset < total) //As long as we haven't requested all "Pages"
{ {
//Request next Page //Request next Page
RequestResult requestResult = RequestResult requestResult = downloadClient.MakeRequest(
downloadClient.MakeRequest( $"https://api.mangadex.org/manga?limit={limit}&title={publicationTitle}&offset={offset}" +
$"https://api.mangadex.org/manga?limit={limit}&title={publicationTitle}&offset={offset}", RequestType.MangaInfo); $"&contentRating%5B%5D=safe&contentRating%5B%5D=suggestive&contentRating%5B%5D=erotica" +
$"&contentRating%5B%5D=pornographic" +
$"&includes%5B%5D=manga&includes%5B%5D=cover_art&includes%5B%5D=author" +
$"&includes%5B%5D=artist&includes%5B%5D=tag", RequestType.MangaInfo);
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300) if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
break; break;
JsonObject? result = JsonSerializer.Deserialize<JsonObject>(requestResult.result); JsonObject? result = JsonSerializer.Deserialize<JsonObject>(requestResult.result);
@ -39,18 +45,14 @@ public class MangaDex : MangaConnector
else continue; else continue;
if (result.ContainsKey("data")) if (result.ContainsKey("data"))
{ results.AddRange(result["data"]!.AsArray()!);//Manga-data-Array
JsonArray mangaInResult = result["data"]!.AsArray(); //Manga-data-Array }
//Loop each Manga and extract information from JSON
foreach (JsonNode? mangaNode in mangaInResult) foreach (JsonNode mangaNode in results)
{ {
if(mangaNode is null) Log($"Getting publication data. {++loadedPublicationData}/{total}");
continue; if(MangaFromJsonObject(mangaNode.AsObject()) is { } manga)
Log($"Getting publication data. {++loadedPublicationData}/{total}"); retManga.Add(manga); //Add Publication (Manga) to result
if(MangaFromJsonObject((JsonObject) mangaNode) is { } manga)
retManga.Add(manga); //Add Publication (Manga) to result
}
}//else continue;
} }
Log($"Retrieved {retManga.Count} publications. Term=\"{publicationTitle}\""); Log($"Retrieved {retManga.Count} publications. Term=\"{publicationTitle}\"");
return retManga.ToArray(); return retManga.ToArray();
@ -78,94 +80,96 @@ public class MangaDex : MangaConnector
private Manga? MangaFromJsonObject(JsonObject manga) private Manga? MangaFromJsonObject(JsonObject manga)
{ {
if (!manga.ContainsKey("attributes")) if (!manga.TryGetPropertyValue("id", out JsonNode? idNode))
return null; return null;
JsonObject attributes = manga["attributes"]!.AsObject(); string publicationId = idNode!.GetValue<string>();
if (!manga.TryGetPropertyValue("attributes", out JsonNode? attributesNode))
return null;
JsonObject attributes = attributesNode!.AsObject();
if (!attributes.TryGetPropertyValue("title", out JsonNode? titleNode))
return null;
string title = titleNode!.AsObject().ContainsKey("en") switch
{
true => titleNode.AsObject()["en"]!.GetValue<string>(),
false => titleNode.AsObject().First().Value!.GetValue<string>()
};
if(!manga.ContainsKey("id"))
return null;
string publicationId = manga["id"]!.GetValue<string>();
if(!attributes.ContainsKey("title"))
return null;
string title = attributes["title"]!.AsObject().ContainsKey("en") && attributes["title"]!["en"] is not null
? attributes["title"]!["en"]!.GetValue<string>()
: attributes["title"]![((IDictionary<string, JsonNode?>)attributes["title"]!.AsObject()).Keys.First()]!.GetValue<string>();
if(!attributes.ContainsKey("description"))
return null;
string? description = attributes["description"]!.AsObject().ContainsKey("en") && attributes["description"]!["en"] is not null
? attributes["description"]!["en"]!.GetValue<string?>()
: null;
if(!attributes.ContainsKey("altTitles"))
return null;
JsonArray altTitlesObject = attributes["altTitles"]!.AsArray();
Dictionary<string, string> altTitlesDict = new(); Dictionary<string, string> altTitlesDict = new();
foreach (JsonNode? altTitleNode in altTitlesObject) if (attributes.TryGetPropertyValue("altTitles", out JsonNode? altTitlesNode))
{ {
JsonObject altTitleObject = (JsonObject)altTitleNode!; foreach (JsonNode? altTitleNode in altTitlesNode!.AsArray())
string key = ((IDictionary<string, JsonNode?>)altTitleObject).Keys.ToArray()[0];
altTitlesDict.TryAdd(key, altTitleObject[key]!.GetValue<string>());
}
if(!attributes.ContainsKey("tags"))
return null;
JsonArray tagsObject = attributes["tags"]!.AsArray();
HashSet<string> tags = new();
foreach (JsonNode? tagNode in tagsObject)
{
JsonObject tagObject = (JsonObject)tagNode!;
if(tagObject["attributes"]!["name"]!.AsObject().ContainsKey("en"))
tags.Add(tagObject["attributes"]!["name"]!["en"]!.GetValue<string>());
}
string? posterId = null;
HashSet<string> authorIds = new();
if (manga.ContainsKey("relationships") && manga["relationships"] is not null)
{
JsonArray relationships = manga["relationships"]!.AsArray();
posterId = relationships.FirstOrDefault(relationship => relationship!["type"]!.GetValue<string>() == "cover_art")!["id"]!.GetValue<string>();
foreach (JsonNode? node in relationships.Where(relationship =>
relationship!["type"]!.GetValue<string>() == "author"))
authorIds.Add(node!["id"]!.GetValue<string>());
}
string? coverUrl = GetCoverUrl(publicationId, posterId);
string? coverCacheName = null;
if (coverUrl is not null)
coverCacheName = SaveCoverImageToCache(coverUrl, RequestType.MangaCover);
List<string> authors = GetAuthors(authorIds);
Dictionary<string, string> linksDict = new();
if (attributes.ContainsKey("links") && attributes["links"] is not null)
{
JsonObject linksObject = attributes["links"]!.AsObject();
foreach (string key in ((IDictionary<string, JsonNode?>)linksObject).Keys)
{ {
linksDict.Add(key, linksObject[key]!.GetValue<string>()); JsonObject altTitleNodeObject = altTitleNode!.AsObject();
altTitlesDict.TryAdd(altTitleNodeObject.First().Key, altTitleNodeObject.First().Value!.GetValue<string>());
} }
} }
int? year = attributes.ContainsKey("year") && attributes["year"] is not null if (!attributes.TryGetPropertyValue("description", out JsonNode? descriptionNode))
? attributes["year"]!.GetValue<int?>() return null;
: null; string description = descriptionNode!.AsObject().ContainsKey("en") switch
{
true => descriptionNode.AsObject()["en"]!.GetValue<string>(),
false => descriptionNode.AsObject().First().Value!.GetValue<string>()
};
Dictionary<string, string> linksDict = new();
if (attributes.TryGetPropertyValue("links", out JsonNode? linksNode))
foreach (KeyValuePair<string, JsonNode> linkKv in linksNode!.AsObject())
linksDict.TryAdd(linkKv.Key, linkKv.Value.GetValue<string>());
string? originalLanguage = string? originalLanguage =
attributes.ContainsKey("originalLanguage") && attributes["originalLanguage"] is not null attributes.TryGetPropertyValue("originalLanguage", out JsonNode? originalLanguageNode) switch
? attributes["originalLanguage"]!.GetValue<string?>() {
: null; true => originalLanguageNode!.GetValue<string>(),
false => null
if(!attributes.ContainsKey("status")) };
return null;
string status = attributes["status"]!.GetValue<string>(); Manga.ReleaseStatusByte status = Manga.ReleaseStatusByte.Unreleased;
Manga.ReleaseStatusByte releaseStatus = Manga.ReleaseStatusByte.Unreleased; if (attributes.TryGetPropertyValue("status", out JsonNode? statusNode))
switch (status.ToLower())
{ {
case "ongoing": releaseStatus = Manga.ReleaseStatusByte.Continuing; break; status = statusNode!.GetValue<string>().ToLower() switch
case "completed": releaseStatus = Manga.ReleaseStatusByte.Completed; break; {
case "hiatus": releaseStatus = Manga.ReleaseStatusByte.OnHiatus; break; "ongoing" => Manga.ReleaseStatusByte.Continuing,
case "cancelled": releaseStatus = Manga.ReleaseStatusByte.Cancelled; break; "completed" => Manga.ReleaseStatusByte.Completed,
"hiatus" => Manga.ReleaseStatusByte.OnHiatus,
"cancelled" => Manga.ReleaseStatusByte.Cancelled,
_ => Manga.ReleaseStatusByte.Unreleased
};
}
int? year = attributes.TryGetPropertyValue("year", out JsonNode? yearNode) switch
{
true => yearNode!.GetValue<int>(),
false => null
};
HashSet<string> tags = new(128);
if (attributes.TryGetPropertyValue("tags", out JsonNode? tagsNode))
foreach (JsonNode? tagNode in tagsNode!.AsArray())
tags.Add(tagNode!["attributes"]!["name"]!["en"]!.GetValue<string>());
if (!manga.TryGetPropertyValue("relationships", out JsonNode? relationshipsNode))
return null;
JsonNode? coverNode = relationshipsNode!.AsArray()
.FirstOrDefault(rel => rel!["type"]!.GetValue<string>().Equals("cover_art"));
if (coverNode is null)
return null;
string fileName = coverNode["attributes"]!["fileName"]!.GetValue<string>();
string coverUrl = $"https://uploads.mangadex.org/covers/{publicationId}/{fileName}";
string coverCacheName = SaveCoverImageToCache(coverUrl, RequestType.MangaCover);
List<string> authors = new();
JsonNode?[] authorNodes = relationshipsNode.AsArray()
.Where(rel => rel!["type"]!.GetValue<string>().Equals("author") || rel!["type"]!.GetValue<string>().Equals("artist")).ToArray();
foreach (JsonNode? authorNode in authorNodes)
{
string authorName = authorNode!["attributes"]!["name"]!.GetValue<string>();
if(!authors.Contains(authorName))
authors.Add(authorName);
} }
Manga pub = new( Manga pub = new(
@ -179,9 +183,9 @@ public class MangaDex : MangaConnector
linksDict, linksDict,
year, year,
originalLanguage, originalLanguage,
status, Enum.GetName(status) ?? "",
publicationId, publicationId,
releaseStatus status
); );
cachedPublications.Add(pub); cachedPublications.Add(pub);
return pub; return pub;
@ -200,7 +204,7 @@ public class MangaDex : MangaConnector
//Request next "Page" //Request next "Page"
RequestResult requestResult = RequestResult requestResult =
downloadClient.MakeRequest( downloadClient.MakeRequest(
$"https://api.mangadex.org/manga/{manga.publicationId}/feed?limit={limit}&offset={offset}&translatedLanguage%5B%5D={language}", RequestType.MangaDexFeed); $"https://api.mangadex.org/manga/{manga.publicationId}/feed?limit={limit}&offset={offset}&translatedLanguage%5B%5D={language}&contentRating%5B%5D=safe&contentRating%5B%5D=suggestive&contentRating%5B%5D=erotica&contentRating%5B%5D=pornographic", RequestType.MangaDexFeed);
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300) if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
break; break;
JsonObject? result = JsonSerializer.Deserialize<JsonObject>(requestResult.result); JsonObject? result = JsonSerializer.Deserialize<JsonObject>(requestResult.result);
@ -288,50 +292,4 @@ public class MangaDex : MangaConnector
//Download Chapter-Images //Download Chapter-Images
return DownloadChapterImages(imageUrls.ToArray(), chapter.GetArchiveFilePath(settings.downloadLocation), RequestType.MangaImage, comicInfoPath, progressToken:progressToken); return DownloadChapterImages(imageUrls.ToArray(), chapter.GetArchiveFilePath(settings.downloadLocation), RequestType.MangaImage, comicInfoPath, progressToken:progressToken);
} }
private string? GetCoverUrl(string publicationId, string? posterId)
{
Log($"Getting CoverUrl for Publication {publicationId}");
if (posterId is null)
{
Log("No cover.");
return null;
}
//Request information where to download Cover
RequestResult requestResult =
downloadClient.MakeRequest($"https://api.mangadex.org/cover/{posterId}", RequestType.MangaCover);
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
return null;
JsonObject? result = JsonSerializer.Deserialize<JsonObject>(requestResult.result);
if (result is null)
return null;
string fileName = result["data"]!["attributes"]!["fileName"]!.GetValue<string>();
string coverUrl = $"https://uploads.mangadex.org/covers/{publicationId}/{fileName}";
Log($"Cover-Url {publicationId} -> {coverUrl}");
return coverUrl;
}
private List<string> GetAuthors(IEnumerable<string> authorIds)
{
Log("Retrieving authors.");
List<string> ret = new();
foreach (string authorId in authorIds)
{
RequestResult requestResult =
downloadClient.MakeRequest($"https://api.mangadex.org/author/{authorId}", RequestType.MangaDexAuthor);
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
return ret;
JsonObject? result = JsonSerializer.Deserialize<JsonObject>(requestResult.result);
if (result is null)
return ret;
string authorName = result["data"]!["attributes"]!["name"]!.GetValue<string>();
ret.Add(authorName);
Log($"Got author {authorId} -> {authorName}");
}
return ret;
}
} }

View File

@ -199,8 +199,8 @@ public class Mangasee : MangaConnector
string? volumeNumber = m.Groups[2].Success ? m.Groups[2].Value : "1"; string? volumeNumber = m.Groups[2].Success ? m.Groups[2].Value : "1";
string chapterNumber = m.Groups[1].Value; string chapterNumber = m.Groups[1].Value;
url = string.Concat(Regex.Match(url, @"(.*)-page-[0-9]+(\.html)").Groups.Values.Select(v => v.Value)); string chapterUrl = Regex.Replace(url, @"-page-[0-9]+(\.html)", ".html");
chapters.Add(new Chapter(manga, "", volumeNumber, chapterNumber, url)); chapters.Add(new Chapter(manga, "", volumeNumber, chapterNumber, chapterUrl));
} }
//Return Chapters ordered by Chapter-Number //Return Chapters ordered by Chapter-Number

View File

@ -222,6 +222,9 @@ public class Server : GlobalBase
case "Settings/customRequestLimit": case "Settings/customRequestLimit":
SendResponse(HttpStatusCode.OK, response, settings.requestLimits); SendResponse(HttpStatusCode.OK, response, settings.requestLimits);
break; break;
case "Settings/AprilFoolsMode":
SendResponse(HttpStatusCode.OK, response, settings.aprilFoolsMode);
break;
case "NotificationConnectors": case "NotificationConnectors":
SendResponse(HttpStatusCode.OK, response, notificationConnectors); SendResponse(HttpStatusCode.OK, response, notificationConnectors);
break; break;
@ -419,7 +422,7 @@ public class Server : GlobalBase
case "Settings/UpdateDownloadLocation": case "Settings/UpdateDownloadLocation":
if (!requestParams.TryGetValue("downloadLocation", out string? downloadLocation) || if (!requestParams.TryGetValue("downloadLocation", out string? downloadLocation) ||
!requestParams.TryGetValue("moveFiles", out string? moveFilesStr) || !requestParams.TryGetValue("moveFiles", out string? moveFilesStr) ||
!Boolean.TryParse(moveFilesStr, out bool moveFiles)) !bool.TryParse(moveFilesStr, out bool moveFiles))
{ {
SendResponse(HttpStatusCode.BadRequest, response); SendResponse(HttpStatusCode.BadRequest, response);
break; break;
@ -427,6 +430,16 @@ public class Server : GlobalBase
settings.UpdateDownloadLocation(downloadLocation, moveFiles); settings.UpdateDownloadLocation(downloadLocation, moveFiles);
SendResponse(HttpStatusCode.Accepted, response); SendResponse(HttpStatusCode.Accepted, response);
break; break;
case "Settings/AprilFoolsMode":
if (!requestVariables.TryGetValue("enabled", out string? aprilFoolsModeEnabledStr) ||
bool.TryParse(aprilFoolsModeEnabledStr, out bool aprilFoolsModeEnabled))
{
SendResponse(HttpStatusCode.BadRequest, response);
break;
}
settings.UpdateAprilFoolsMode(aprilFoolsModeEnabled);
SendResponse(HttpStatusCode.Accepted, response);
break;
/*case "Settings/UpdateWorkingDirectory": /*case "Settings/UpdateWorkingDirectory":
if (!requestParams.TryGetValue("workingDirectory", out string? workingDirectory)) if (!requestParams.TryGetValue("workingDirectory", out string? workingDirectory))
{ {

View File

@ -73,10 +73,23 @@ public partial class Tranga : GlobalBase
{ {
while (keepRunning) while (keepRunning)
{ {
jobBoss.CheckJobs(); if(!settings.aprilFoolsMode || !IsAprilFirst())
jobBoss.CheckJobs();
else
Log("April Fools Mode in Effect");
Thread.Sleep(100); Thread.Sleep(100);
} }
}); });
t.Start(); t.Start();
} }
private bool IsAprilFirst()
{
//UTC 01 Apr +-12hrs
DateTime start = new DateTime(DateTime.Now.Year, 03, 31, 12, 0, 0, DateTimeKind.Utc);
DateTime end = new DateTime(DateTime.Now.Year, 04, 02, 12, 0, 0, DateTimeKind.Utc);
if (DateTime.UtcNow > start && DateTime.UtcNow < end)
return true;
return false;
}
} }

View File

@ -17,16 +17,16 @@ public partial class Tranga : GlobalBase
string[]? consoleLogger = GetArg(args, ArgEnum.ConsoleLogger); string[]? consoleLogger = GetArg(args, ArgEnum.ConsoleLogger);
string[]? fileLogger = GetArg(args, ArgEnum.FileLogger); string[]? fileLogger = GetArg(args, ArgEnum.FileLogger);
string? filePath = GetArg(args, ArgEnum.FileLoggerPath)?[0]; string? directoryPath = GetArg(args, ArgEnum.FileLoggerPath)?[0];
if (filePath is not null && !Directory.Exists(new FileInfo(filePath).DirectoryName)) if (directoryPath is not null && !Directory.Exists(directoryPath))
Directory.CreateDirectory(new FileInfo(filePath).DirectoryName!); Directory.CreateDirectory(directoryPath);
List<Logger.LoggerType> enabledLoggers = new(); List<Logger.LoggerType> enabledLoggers = new();
if(consoleLogger is not null) if(consoleLogger is not null)
enabledLoggers.Add(Logger.LoggerType.ConsoleLogger); enabledLoggers.Add(Logger.LoggerType.ConsoleLogger);
if (fileLogger is not null) if (fileLogger is not null)
enabledLoggers.Add(Logger.LoggerType.FileLogger); enabledLoggers.Add(Logger.LoggerType.FileLogger);
Logger logger = new(enabledLoggers.ToArray(), Console.Out, Console.OutputEncoding, filePath); Logger logger = new(enabledLoggers.ToArray(), Console.Out, Console.OutputEncoding, directoryPath);
TrangaSettings? settings = null; TrangaSettings? settings = null;
string[]? downloadLocationPath = GetArg(args, ArgEnum.DownloadLocation); string[]? downloadLocationPath = GetArg(args, ArgEnum.DownloadLocation);
@ -109,7 +109,7 @@ public partial class Tranga : GlobalBase
{ ArgEnum.WorkingDirectory, new(new []{"-w", "--workingDirectory"}, 1, "Directory in which application-data is 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.ConsoleLogger, new(new []{"-c", "--consoleLogger"}, 0, "Enables the consoleLogger") },
{ ArgEnum.FileLogger, new(new []{"-f", "--fileLogger"}, 0, "Enables the fileLogger") }, { ArgEnum.FileLogger, new(new []{"-f", "--fileLogger"}, 0, "Enables the fileLogger") },
{ ArgEnum.FileLoggerPath, new (new []{"-l", "--fPath"}, 1, "LogFilePath" ) }, { ArgEnum.FileLoggerPath, new (new []{"-l", "--fPath"}, 1, "Log Folder Path" ) },
{ ArgEnum.Help, new(new []{"-h", "--help"}, 0, "Print this") } { ArgEnum.Help, new(new []{"-h", "--help"}, 0, "Print this") }
//{ ArgEnum., new(new []{""}, 1, "") } //{ ArgEnum., new(new []{""}, 1, "") }
}; };

View File

@ -1,5 +1,4 @@
using System.Net.Http.Headers; using System.Runtime.InteropServices;
using System.Runtime.InteropServices;
using Newtonsoft.Json; using Newtonsoft.Json;
using Tranga.LibraryConnectors; using Tranga.LibraryConnectors;
using Tranga.MangaConnectors; using Tranga.MangaConnectors;
@ -13,14 +12,15 @@ public class TrangaSettings
public string downloadLocation { get; private set; } public string downloadLocation { get; private set; }
public string workingDirectory { get; private set; } public string workingDirectory { get; private set; }
public int apiPortNumber { get; init; } public int apiPortNumber { get; init; }
public string userAgent { get; set; } = DefaultUserAgent; public string userAgent { get; private set; } = DefaultUserAgent;
[JsonIgnore] public string settingsFilePath => Path.Join(workingDirectory, "settings.json"); [JsonIgnore] public string settingsFilePath => Path.Join(workingDirectory, "settings.json");
[JsonIgnore] public string libraryConnectorsFilePath => Path.Join(workingDirectory, "libraryConnectors.json"); [JsonIgnore] public string libraryConnectorsFilePath => Path.Join(workingDirectory, "libraryConnectors.json");
[JsonIgnore] public string notificationConnectorsFilePath => Path.Join(workingDirectory, "notificationConnectors.json"); [JsonIgnore] public string notificationConnectorsFilePath => Path.Join(workingDirectory, "notificationConnectors.json");
[JsonIgnore] public string jobsFolderPath => Path.Join(workingDirectory, "jobs"); [JsonIgnore] public string jobsFolderPath => Path.Join(workingDirectory, "jobs");
[JsonIgnore] public string coverImageCache => Path.Join(workingDirectory, "imageCache"); [JsonIgnore] public string coverImageCache => Path.Join(workingDirectory, "imageCache");
[JsonIgnore] internal static readonly string DefaultUserAgent = $"Tranga ({Enum.GetName(Environment.OSVersion.Platform)}; {(Environment.Is64BitOperatingSystem ? "x64" : "")}) / 1.0"; [JsonIgnore] internal static readonly string DefaultUserAgent = $"Tranga ({Enum.GetName(Environment.OSVersion.Platform)}; {(Environment.Is64BitOperatingSystem ? "x64" : "")}) / 1.0";
public ushort? version { get; set; } = 1; public ushort? version { get; } = 1;
public bool aprilFoolsMode { get; private set; } = true;
[JsonIgnore]internal static readonly Dictionary<RequestType, int> DefaultRequestLimits = new () [JsonIgnore]internal static readonly Dictionary<RequestType, int> DefaultRequestLimits = new ()
{ {
{RequestType.MangaInfo, 250}, {RequestType.MangaInfo, 250},
@ -102,6 +102,12 @@ public class TrangaSettings
})!; })!;
} }
public void UpdateAprilFoolsMode(bool enabled)
{
this.aprilFoolsMode = enabled;
ExportSettings();
}
public void UpdateDownloadLocation(string newPath, bool moveFiles = true) public void UpdateDownloadLocation(string newPath, bool moveFiles = true)
{ {
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))