Compare commits
22 Commits
1.5
...
0a51e7ad3d
Author | SHA1 | Date | |
---|---|---|---|
0a51e7ad3d | |||
e541b922dc | |||
604abd5f9a | |||
7b311eae75 | |||
d4eb72cd99 | |||
b515215f4b | |||
a16686dfbf | |||
4275703941 | |||
c3342984ea | |||
ed4bdb5b33 | |||
0f0902c932 | |||
6508055b43 | |||
abc66511d8 | |||
9ed36c47b5 | |||
fd1b2a8470 | |||
8058749ab5 | |||
8737617e5f | |||
7e4f43f1e2 | |||
12b1b2afd6 | |||
0f9ac60fcd | |||
8c87f2948c | |||
e0fb817256 |
@ -6,7 +6,7 @@ COPY . /src/
|
|||||||
RUN dotnet restore API/API.csproj
|
RUN dotnet restore API/API.csproj
|
||||||
RUN dotnet publish -c Release -o /publish
|
RUN dotnet publish -c Release -o /publish
|
||||||
|
|
||||||
FROM glax/tranga-base:dev as runtime
|
FROM glax/tranga-base:latest as runtime
|
||||||
WORKDIR /publish
|
WORKDIR /publish
|
||||||
COPY --from=build-env /publish .
|
COPY --from=build-env /publish .
|
||||||
EXPOSE 6531
|
EXPOSE 6531
|
||||||
|
@ -80,11 +80,11 @@ public class RequestHandler
|
|||||||
private Dictionary<string, string> GetRequestVariables(string query)
|
private Dictionary<string, string> GetRequestVariables(string query)
|
||||||
{
|
{
|
||||||
Dictionary<string, string> ret = new();
|
Dictionary<string, string> ret = new();
|
||||||
Regex queryRex = new (@"\?{1}([A-z]+=[A-z]+)+(&[A-z]+=[A-z]+)*");
|
Regex queryRex = new (@"\?{1}&?([A-z]+=[A-z]+)+(&[A-z]+=[A-z]+)*");
|
||||||
if (!queryRex.IsMatch(query))
|
if (!queryRex.IsMatch(query))
|
||||||
return ret;
|
return ret;
|
||||||
query = query.Substring(1);
|
query = query.Substring(1);
|
||||||
foreach(string kvpair in query.Split('&'))
|
foreach(string kvpair in query.Split('&').Where(str => str.Length>=3 ))
|
||||||
ret.Add(kvpair.Split('=')[0], kvpair.Split('=')[1]);
|
ret.Add(kvpair.Split('=')[0], kvpair.Split('=')[1]);
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
@ -152,7 +152,7 @@ public class RequestHandler
|
|||||||
_taskManager.GetAvailableConnectors().FirstOrDefault(con => con.Key == connectorName1).Value;
|
_taskManager.GetAvailableConnectors().FirstOrDefault(con => con.Key == connectorName1).Value;
|
||||||
if (connector1 is null)
|
if (connector1 is null)
|
||||||
return;
|
return;
|
||||||
Publication? publication1 = _taskManager.GetAllPublications().FirstOrDefault(pub => pub.internalId == internalId1);
|
Publication? publication1 = _taskManager.GetAllPublications().FirstOrDefault(pub => string.Concat(TrangaSettings.CleanIdRex.Matches(pub.internalId)) == string.Concat(TrangaSettings.CleanIdRex.Matches(internalId1)));
|
||||||
if (publication1 is null)
|
if (publication1 is null)
|
||||||
return;
|
return;
|
||||||
_taskManager.AddTask(new MonitorPublicationTask(connectorName1, (Publication)publication1, TimeSpan.Parse(reoccurrenceTime1), language1 ?? "en"));
|
_taskManager.AddTask(new MonitorPublicationTask(connectorName1, (Publication)publication1, TimeSpan.Parse(reoccurrenceTime1), language1 ?? "en"));
|
||||||
@ -174,7 +174,7 @@ public class RequestHandler
|
|||||||
_taskManager.GetAvailableConnectors().FirstOrDefault(con => con.Key == connectorName2).Value;
|
_taskManager.GetAvailableConnectors().FirstOrDefault(con => con.Key == connectorName2).Value;
|
||||||
if (connector2 is null)
|
if (connector2 is null)
|
||||||
return;
|
return;
|
||||||
Publication? publication2 = _taskManager.GetAllPublications().FirstOrDefault(pub => pub.internalId == internalId2);
|
Publication? publication2 = _taskManager.GetAllPublications().FirstOrDefault(pub => string.Concat(TrangaSettings.CleanIdRex.Matches(pub.internalId)) == string.Concat(TrangaSettings.CleanIdRex.Matches(internalId2)));
|
||||||
if (publication2 is null)
|
if (publication2 is null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
@ -261,7 +261,7 @@ public class RequestHandler
|
|||||||
variables.TryGetValue("internalId", out string? internalId1);
|
variables.TryGetValue("internalId", out string? internalId1);
|
||||||
if(internalId1 is null)
|
if(internalId1 is null)
|
||||||
return _taskManager.GetAllPublications();
|
return _taskManager.GetAllPublications();
|
||||||
return new [] { _taskManager.GetAllPublications().FirstOrDefault(pub => pub.internalId == internalId1) };
|
return new [] { _taskManager.GetAllPublications().FirstOrDefault(pub => string.Concat(TrangaSettings.CleanIdRex.Matches(pub.internalId)) == string.Concat(TrangaSettings.CleanIdRex.Matches(internalId1))) };
|
||||||
case "/Publications/FromConnector":
|
case "/Publications/FromConnector":
|
||||||
variables.TryGetValue("connectorName", out string? connectorName1);
|
variables.TryGetValue("connectorName", out string? connectorName1);
|
||||||
variables.TryGetValue("title", out string? title);
|
variables.TryGetValue("title", out string? title);
|
||||||
@ -288,7 +288,7 @@ public class RequestHandler
|
|||||||
Connector? connector2 = _taskManager.GetAvailableConnectors().FirstOrDefault(con => con.Key == connectorName2).Value;
|
Connector? connector2 = _taskManager.GetAvailableConnectors().FirstOrDefault(con => con.Key == connectorName2).Value;
|
||||||
if (connector2 is null)
|
if (connector2 is null)
|
||||||
return null;
|
return null;
|
||||||
Publication? publication = _taskManager.GetAllPublications().FirstOrDefault(pub => pub.internalId == internalId2);
|
Publication? publication = _taskManager.GetAllPublications().FirstOrDefault(pub => string.Concat(TrangaSettings.CleanIdRex.Matches(pub.internalId)) == string.Concat(TrangaSettings.CleanIdRex.Matches(internalId2)));
|
||||||
if (publication is null)
|
if (publication is null)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
using System.Net;
|
using System.Net;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using Logging;
|
using Logging;
|
||||||
@ -18,7 +19,10 @@ public class Server
|
|||||||
public Server(int port, TaskManager taskManager, Logger? logger = null)
|
public Server(int port, TaskManager taskManager, Logger? logger = null)
|
||||||
{
|
{
|
||||||
this.logger = logger;
|
this.logger = logger;
|
||||||
this._listener.Prefixes.Add($"http://*:{port}/");
|
if(RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
||||||
|
this._listener.Prefixes.Add($"http://*:{port}/");
|
||||||
|
else
|
||||||
|
this._listener.Prefixes.Add($"http://localhost:{port}/");
|
||||||
this._requestHandler = new RequestHandler(taskManager, this);
|
this._requestHandler = new RequestHandler(taskManager, this);
|
||||||
Listen();
|
Listen();
|
||||||
}
|
}
|
||||||
@ -51,7 +55,14 @@ public class Server
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_requestHandler.HandleRequest(request, response);
|
if (request.HttpMethod == "OPTIONS")
|
||||||
|
{
|
||||||
|
SendResponse(HttpStatusCode.OK, response);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_requestHandler.HandleRequest(request, response);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal void SendResponse(HttpStatusCode statusCode, HttpListenerResponse response, object? content = null)
|
internal void SendResponse(HttpStatusCode statusCode, HttpListenerResponse response, object? content = null)
|
||||||
@ -63,9 +74,16 @@ public class Server
|
|||||||
response.AddHeader("Access-Control-Max-Age", "1728000");
|
response.AddHeader("Access-Control-Max-Age", "1728000");
|
||||||
response.AppendHeader("Access-Control-Allow-Origin", "*");
|
response.AppendHeader("Access-Control-Allow-Origin", "*");
|
||||||
response.ContentType = "application/json";
|
response.ContentType = "application/json";
|
||||||
response.OutputStream.Write(content is not null
|
try
|
||||||
? Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(content))
|
{
|
||||||
: Array.Empty<byte>());
|
response.OutputStream.Write(content is not null
|
||||||
|
? Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(content))
|
||||||
|
: Array.Empty<byte>());
|
||||||
|
}
|
||||||
|
catch (HttpListenerException)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
response.OutputStream.Close();
|
response.OutputStream.Close();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
14
Dockerfile
@ -1,14 +0,0 @@
|
|||||||
# syntax=docker/dockerfile:1
|
|
||||||
|
|
||||||
FROM mcr.microsoft.com/dotnet/sdk:7.0 as build-env
|
|
||||||
WORKDIR /src
|
|
||||||
COPY . /src/
|
|
||||||
RUN dotnet restore Tranga-API/Tranga-API.csproj
|
|
||||||
RUN dotnet publish -c Release -o /publish
|
|
||||||
|
|
||||||
#FROM mcr.microsoft.com/dotnet/aspnet:7.0 as runtime
|
|
||||||
FROM glax/tranga-base:latest as runtime
|
|
||||||
WORKDIR /publish
|
|
||||||
COPY --from=build-env /publish .
|
|
||||||
EXPOSE 80
|
|
||||||
ENTRYPOINT ["dotnet", "/publish/Tranga-API.dll"]
|
|
36
README.md
@ -52,14 +52,14 @@
|
|||||||
<!-- ABOUT THE PROJECT -->
|
<!-- ABOUT THE PROJECT -->
|
||||||
## About The Project
|
## About The Project
|
||||||
|
|
||||||
Tranga can download Chapters and Metadata from Scanlation sites such as
|
Tranga can download Chapters and Metadata from "Scanlation" sites such as
|
||||||
|
|
||||||
- [MangaDex.org](https://mangadex.org/)
|
- [MangaDex.org](https://mangadex.org/)
|
||||||
- [Manganato.com](https://manganato.com/)
|
- [Manganato.com](https://manganato.com/)
|
||||||
- [Mangasee](https://mangasee123.com/)
|
- [Mangasee](https://mangasee123.com/)
|
||||||
|
- ❓ Open an [issue](https://github.com/C9Glax/tranga/issues)
|
||||||
|
|
||||||
and automatically start updates in [Komga](https://komga.org/) and [Kavita](https://www.kavitareader.com/) to import them.
|
and automatically import them with [Komga](https://komga.org/) and [Kavita](https://www.kavitareader.com/). Also Notifications will be sent to your devices using [Gotify](https://gotify.net/) and [LunaSea](https://www.lunasea.app/).
|
||||||
|
|
||||||
### 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
|
||||||
@ -76,19 +76,18 @@ 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/)
|
||||||
- Love <3 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>
|
||||||
|
|
||||||
|
|
||||||
## Screenshots
|
## Screenshots
|
||||||
|
|
||||||

|
|  |  |
|
||||||
|
|-----------------------------------:|:----------------------------------|
|
||||||
|
|
||||||

|
|  |  |  |
|
||||||
|
|-----------------------------------:|:-------------------------------------------------:|:-----------------------------------|
|
||||||
|  |  |
|
|
||||||
|-----------------------------------:|:-------------------------------------------------:|
|
|
||||||
|
|
||||||
<p align="right">(<a href="#readme-top">back to top</a>)</p>
|
<p align="right">(<a href="#readme-top">back to top</a>)</p>
|
||||||
|
|
||||||
@ -110,39 +109,42 @@ Download [docker-compose.yaml](https://git.bernloehr.eu/glax/Tranga/src/branch/m
|
|||||||
|
|
||||||
Wherever you are mounting `/usr/share/Tranga-API` you also need to mount that same path + `/imageCache` in the webserver container.
|
Wherever you are mounting `/usr/share/Tranga-API` you also need to mount that same path + `/imageCache` in the webserver container.
|
||||||
|
|
||||||
### Usage
|
### Docker-Website usage
|
||||||
|
|
||||||
There is two ways to download Mangas:
|
There is two ways to download Mangas:
|
||||||
- Downloading everything and monitor for new Chapters
|
- Downloading everything and monitor for new Chapters
|
||||||
- Selecting specific Volumes/Chapters
|
- Selecting specific Volumes/Chapters
|
||||||
|
|
||||||
On the website you add new tasks, by selecting the blue '+' field. Next select the connector/site you want to use, and enter a search term.
|
On the website you add new tasks, by selecting the blue '+' field. Next select the connector/site you want to use, and enter a search term.
|
||||||
After pressing 'Search', the results will be presented below - this might, depending on the result-size, take a while.
|
After clicking 'Search' (or pressing Enter), the results will be presented below - this might, depending on the result-size, take a while because we are already preloading the cover-images.
|
||||||
Next select the publication and a new popup will open with two options:
|
Next select the publication (by selecting the cover) and a new popup will open with two options:
|
||||||
- "Monitor" - Download all chapters and monitor for new ones
|
- "Monitor" - Download all chapters and monitor for new ones
|
||||||
- "Download Chapter" - Download specific chapters only
|
- "Download Chapter" - Download specific chapters only
|
||||||
|
|
||||||
When selecting `Monitor` you will be presented with a new window and the selection of the interval you want to check for new chapters.
|
When selecting `Monitor` you will be presented with a new window and the selection of the interval you want to check for new chapters (Default: Every 3 hours).
|
||||||
When selecting `Download Chapter` a list will open with all available chapters from which you can then select a range.
|
When selecting `Download Chapter` a list will open with all available chapters - that have not yet been downloaded - from which you can then select a range (see below).
|
||||||
|
|
||||||
The syntax for selecting chapters is as follows:
|
The syntax for selecting chapters is as follows:
|
||||||
- To download a single Chapter enter either the index number (the number at the very start of the line) or its absolute number like so: `c(h)(apter)[number]`, spaces are allowed.
|
- To download a single Chapter enter either the index number (the number at the very start of the line) or its absolute number like so: `c(h)(apter)[number]`, spaces are allowed.
|
||||||
- To download a range of chapters enter either a range of index numbers (`3-6`) or chapters (`ch 12-23`).
|
- To download a range of chapters enter either a range of index numbers (`3-6`) or chapters (`ch 12-23`).
|
||||||
- For volumes the syntax is as follows: `v(ol)[number](-[number])`, again spaces allowed.
|
- For volumes the syntax is as follows: `v(ol)[number](-[number])`, again spaces allowed.
|
||||||
|
|
||||||
Examples: `2-12`, `c1`, `ch 2`, `chapter 3`, `v 2`, `vol3-4`, `v2c4` (note: you can only specify a single chapter with this syntax).
|
Examples: `2-12`, `c1`, `ch 2`, `chapter 3`, `v 2`, `vol3-4`, `v2c4` (note: you can only specify a single chapter with this last syntax).
|
||||||
|
|
||||||
### Prerequisites
|
### Prerequisites
|
||||||
|
|
||||||
|
#### To Build
|
||||||
[.NET-Core 7.0 SDK](https://dotnet.microsoft.com/en-us/download/dotnet/7.0)
|
[.NET-Core 7.0 SDK](https://dotnet.microsoft.com/en-us/download/dotnet/7.0)
|
||||||
|
#### To Run
|
||||||
|
[.NET-Core 7.0 Runtime](https://dotnet.microsoft.com/en-us/download/dotnet/7.0) scroll down a bit, should be on the right the second item.
|
||||||
|
|
||||||
<!-- ROADMAP -->
|
<!-- ROADMAP -->
|
||||||
## Roadmap
|
## Roadmap
|
||||||
|
|
||||||
- [ ] Docker ARM support
|
- [ ] Docker ARM support
|
||||||
- [ ] ?
|
- [ ] ❓
|
||||||
|
|
||||||
See the [open issues](https://git.bernloehr.eu/glax/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).
|
||||||
|
|
||||||
<p align="right">(<a href="#readme-top">back to top</a>)</p>
|
<p align="right">(<a href="#readme-top">back to top</a>)</p>
|
||||||
|
|
||||||
|
@ -186,15 +186,21 @@ public abstract class Connector
|
|||||||
/// <returns>true if chapter is present</returns>
|
/// <returns>true if chapter is present</returns>
|
||||||
public bool CheckChapterIsDownloaded(Publication publication, Chapter chapter)
|
public bool CheckChapterIsDownloaded(Publication publication, Chapter chapter)
|
||||||
{
|
{
|
||||||
Regex legalCharacters = new Regex(@"([A-z]*[0-9]* *\.*-*,*\]*\[*'*\'*\)*\(*~*!*)*");
|
|
||||||
string oldFilePath = Path.Join(downloadLocation, publication.folderName, $"{string.Concat(legalCharacters.Matches(chapter.name ?? ""))} - V{chapter.volumeNumber}C{chapter.chapterNumber} - {chapter.sortNumber}.cbz");
|
|
||||||
string oldFilePath2 = Path.Join(downloadLocation, publication.folderName, $"{string.Concat(legalCharacters.Matches(chapter.name ?? ""))} - VC{chapter.chapterNumber} - {chapter.chapterNumber}.cbz");
|
|
||||||
string newFilePath = GetArchiveFilePath(publication, chapter);
|
string newFilePath = GetArchiveFilePath(publication, chapter);
|
||||||
if (File.Exists(oldFilePath))
|
FileInfo[] archives = new DirectoryInfo(Path.Join(downloadLocation, publication.folderName)).GetFiles("*.cbz");
|
||||||
File.Move(oldFilePath, newFilePath);
|
Regex chapterRex = new(@"(Vol.[0-9]*)*Ch.[0-9]+");
|
||||||
else if (File.Exists(oldFilePath2))
|
|
||||||
File.Move(oldFilePath2, newFilePath);
|
if (File.Exists(newFilePath))
|
||||||
return File.Exists(newFilePath);
|
return true;
|
||||||
|
|
||||||
|
if (archives.FirstOrDefault(archive =>
|
||||||
|
chapterRex.Match(archive.Name).Value.Split("Ch.")[^1] == chapter.chapterNumber) is { } path)
|
||||||
|
{
|
||||||
|
logger?.WriteLine(this.GetType().ToString(), "Move existing Chapter to new name.");
|
||||||
|
File.Move(path.FullName, newFilePath);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
205
Tranga/Connectors/MangaKatana.cs
Normal file
@ -0,0 +1,205 @@
|
|||||||
|
using System.Globalization;
|
||||||
|
using System.Net;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using HtmlAgilityPack;
|
||||||
|
using Logging;
|
||||||
|
using Tranga.TrangaTasks;
|
||||||
|
|
||||||
|
namespace Tranga.Connectors;
|
||||||
|
|
||||||
|
public class MangaKatana : Connector
|
||||||
|
{
|
||||||
|
public override string name { get; }
|
||||||
|
|
||||||
|
public MangaKatana(string downloadLocation, string imageCachePath, Logger? logger) : base(downloadLocation, imageCachePath, logger)
|
||||||
|
{
|
||||||
|
this.name = "MangaKatana";
|
||||||
|
this.downloadClient = new DownloadClient(new Dictionary<byte, int>()
|
||||||
|
{
|
||||||
|
{(byte)1, 60}
|
||||||
|
}, logger);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Publication[] GetPublications(string publicationTitle = "")
|
||||||
|
{
|
||||||
|
logger?.WriteLine(this.GetType().ToString(), $"Getting Publications (title={publicationTitle})");
|
||||||
|
string sanitizedTitle = string.Concat(Regex.Matches(publicationTitle, "[A-z]* *")).ToLower().Replace(' ', '_');
|
||||||
|
string requestUrl = $"https://mangakatana.com/?search={sanitizedTitle}&search_by=book_name";
|
||||||
|
DownloadClient.RequestResult requestResult =
|
||||||
|
downloadClient.MakeRequest(requestUrl, (byte)1);
|
||||||
|
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
|
||||||
|
return Array.Empty<Publication>();
|
||||||
|
|
||||||
|
return ParsePublicationsFromHtml(requestResult.result);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Publication[] ParsePublicationsFromHtml(Stream html)
|
||||||
|
{
|
||||||
|
StreamReader reader = new(html);
|
||||||
|
string htmlString = reader.ReadToEnd();
|
||||||
|
HtmlDocument document = new();
|
||||||
|
document.LoadHtml(htmlString);
|
||||||
|
IEnumerable<HtmlNode> searchResults = document.DocumentNode.SelectNodes("//*[@id='book_list']/div");
|
||||||
|
List<string> urls = new();
|
||||||
|
foreach (HtmlNode mangaResult in searchResults)
|
||||||
|
{
|
||||||
|
urls.Add(mangaResult.Descendants("a").First().GetAttributes()
|
||||||
|
.First(a => a.Name == "href").Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
HashSet<Publication> ret = new();
|
||||||
|
foreach (string url in urls)
|
||||||
|
{
|
||||||
|
DownloadClient.RequestResult requestResult =
|
||||||
|
downloadClient.MakeRequest(url, (byte)1);
|
||||||
|
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
|
||||||
|
return Array.Empty<Publication>();
|
||||||
|
|
||||||
|
ret.Add(ParseSinglePublicationFromHtml(requestResult.result, url.Split('/')[^1]));
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Publication ParseSinglePublicationFromHtml(Stream html, string publicationId)
|
||||||
|
{
|
||||||
|
StreamReader reader = new(html);
|
||||||
|
string htmlString = reader.ReadToEnd();
|
||||||
|
HtmlDocument document = new();
|
||||||
|
document.LoadHtml(htmlString);
|
||||||
|
string status = "";
|
||||||
|
Dictionary<string, string> altTitles = new();
|
||||||
|
Dictionary<string, string>? links = null;
|
||||||
|
HashSet<string> tags = new();
|
||||||
|
string[] authors = Array.Empty<string>();
|
||||||
|
string originalLanguage = "";
|
||||||
|
|
||||||
|
HtmlNode infoNode = document.DocumentNode.SelectSingleNode("//*[@id='single_book']");
|
||||||
|
string sortName = infoNode.Descendants("h1").First(n => n.HasClass("heading")).InnerText;
|
||||||
|
HtmlNode infoTable = infoNode.SelectSingleNode("//*[@id='single_book']/div[2]/div/ul");
|
||||||
|
|
||||||
|
foreach (HtmlNode row in infoTable.Descendants("li"))
|
||||||
|
{
|
||||||
|
string key = row.SelectNodes("div").First().InnerText.ToLower();
|
||||||
|
string value = row.SelectNodes("div").Last().InnerText;
|
||||||
|
string keySanitized = string.Concat(Regex.Matches(key, "[a-z]"));
|
||||||
|
|
||||||
|
switch (keySanitized)
|
||||||
|
{
|
||||||
|
case "altnames":
|
||||||
|
string[] alts = value.Split(" ; ");
|
||||||
|
for (int i = 0; i < alts.Length; i++)
|
||||||
|
altTitles.Add(i.ToString(), alts[i]);
|
||||||
|
break;
|
||||||
|
case "authorsartists":
|
||||||
|
authors = value.Split(',');
|
||||||
|
break;
|
||||||
|
case "status":
|
||||||
|
status = value;
|
||||||
|
break;
|
||||||
|
case "genres":
|
||||||
|
tags = row.SelectNodes("div").Last().Descendants("a").Select(a => a.InnerText).ToHashSet();
|
||||||
|
break;
|
||||||
|
default: break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
string posterUrl = document.DocumentNode.SelectSingleNode("//*[@id='single_book']/div[1]/div").Descendants("img").First()
|
||||||
|
.GetAttributes().First(a => a.Name == "src").Value;
|
||||||
|
|
||||||
|
string coverFileNameInCache = SaveCoverImageToCache(posterUrl, 1);
|
||||||
|
|
||||||
|
string description = document.DocumentNode.SelectSingleNode("//*[@id='single_book']/div[3]/p").InnerText;
|
||||||
|
while (description.StartsWith('\n'))
|
||||||
|
description = description.Substring(1);
|
||||||
|
|
||||||
|
string yearString = infoTable.Descendants("div").First(d => d.HasClass("updateAt"))
|
||||||
|
.InnerText.Split('-')[^1];
|
||||||
|
int year = Convert.ToInt32(yearString);
|
||||||
|
|
||||||
|
return new Publication(sortName, authors.ToList(), description, altTitles, tags.ToArray(), posterUrl, coverFileNameInCache, links,
|
||||||
|
year, originalLanguage, status, publicationId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Chapter[] GetChapters(Publication publication, string language = "")
|
||||||
|
{
|
||||||
|
logger?.WriteLine(this.GetType().ToString(), $"Getting Chapters for {publication.sortName} {publication.internalId} (language={language})");
|
||||||
|
string requestUrl = $"https://mangakatana.com/manga/{publication.publicationId}";
|
||||||
|
// Leaving this in for verification if the page exists
|
||||||
|
DownloadClient.RequestResult requestResult =
|
||||||
|
downloadClient.MakeRequest(requestUrl, (byte)1);
|
||||||
|
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
|
||||||
|
return Array.Empty<Chapter>();
|
||||||
|
|
||||||
|
//Return Chapters ordered by Chapter-Number
|
||||||
|
NumberFormatInfo chapterNumberFormatInfo = new()
|
||||||
|
{
|
||||||
|
NumberDecimalSeparator = "."
|
||||||
|
};
|
||||||
|
List<Chapter> chapters = ParseChaptersFromHtml(requestUrl);
|
||||||
|
logger?.WriteLine(this.GetType().ToString(), $"Done getting Chapters for {publication.internalId}");
|
||||||
|
return chapters.OrderBy(chapter => Convert.ToSingle(chapter.chapterNumber, chapterNumberFormatInfo)).ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Chapter> ParseChaptersFromHtml(string mangaUrl)
|
||||||
|
{
|
||||||
|
// Using HtmlWeb will include the chapters since they are loaded with js
|
||||||
|
HtmlWeb web = new();
|
||||||
|
HtmlDocument document = web.Load(mangaUrl);
|
||||||
|
|
||||||
|
List<Chapter> ret = new();
|
||||||
|
|
||||||
|
HtmlNode chapterList = document.DocumentNode.SelectSingleNode("//div[contains(@class, 'chapters')]/table/tbody");
|
||||||
|
|
||||||
|
foreach (HtmlNode chapterInfo in chapterList.Descendants("tr"))
|
||||||
|
{
|
||||||
|
string fullString = chapterInfo.Descendants("a").First().InnerText;
|
||||||
|
|
||||||
|
string? volumeNumber = fullString.Contains("Vol.") ? fullString.Replace("Vol.", "").Split(' ')[0] : null;
|
||||||
|
string? chapterNumber = fullString.Split(':')[0].Split("Chapter ")[1].Replace('-', '.');
|
||||||
|
string chapterName = string.Concat(fullString.Split(':')[1..]);
|
||||||
|
string url = chapterInfo.Descendants("a").First()
|
||||||
|
.GetAttributeValue("href", "");
|
||||||
|
ret.Add(new Chapter(chapterName, volumeNumber, chapterNumber, url));
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override HttpStatusCode DownloadChapter(Publication publication, Chapter chapter, DownloadChapterTask parentTask, CancellationToken? cancellationToken = null)
|
||||||
|
{
|
||||||
|
if (cancellationToken?.IsCancellationRequested ?? false)
|
||||||
|
return HttpStatusCode.RequestTimeout;
|
||||||
|
logger?.WriteLine(this.GetType().ToString(), $"Downloading Chapter-Info {publication.sortName} {publication.internalId} {chapter.volumeNumber}-{chapter.chapterNumber}");
|
||||||
|
string requestUrl = chapter.url;
|
||||||
|
// Leaving this in to check if the page exists
|
||||||
|
DownloadClient.RequestResult requestResult =
|
||||||
|
downloadClient.MakeRequest(requestUrl, (byte)1);
|
||||||
|
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
|
||||||
|
return requestResult.statusCode;
|
||||||
|
|
||||||
|
string[] imageUrls = ParseImageUrlsFromHtml(requestUrl);
|
||||||
|
|
||||||
|
string comicInfoPath = Path.GetTempFileName();
|
||||||
|
File.WriteAllText(comicInfoPath, GetComicInfoXmlString(publication, chapter, logger));
|
||||||
|
|
||||||
|
return DownloadChapterImages(imageUrls, GetArchiveFilePath(publication, chapter), (byte)1, parentTask, comicInfoPath, "https://mangakatana.com/", cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
private string[] ParseImageUrlsFromHtml(string mangaUrl)
|
||||||
|
{
|
||||||
|
HtmlWeb web = new();
|
||||||
|
HtmlDocument document = web.Load(mangaUrl);
|
||||||
|
|
||||||
|
// Images are loaded dynamically, but the urls are present in a piece of js code on the page
|
||||||
|
string js = document.DocumentNode.SelectSingleNode("//script[contains(., 'data-src')]").InnerText
|
||||||
|
.Replace("\r", "")
|
||||||
|
.Replace("\n", "")
|
||||||
|
.Replace("\t", "");
|
||||||
|
string regexPat = @"(var thzq=\[')(.*)(,];function)";
|
||||||
|
var group = Regex.Matches(js, regexPat).First().Groups[2].Value.Replace("'", "");
|
||||||
|
var urls = group.Split(',');
|
||||||
|
|
||||||
|
return urls;
|
||||||
|
}
|
||||||
|
}
|
@ -6,18 +6,18 @@ namespace Tranga.NotificationManagers;
|
|||||||
|
|
||||||
public class LunaSea : NotificationManager
|
public class LunaSea : NotificationManager
|
||||||
{
|
{
|
||||||
public string webhook { get; }
|
public string id { get; }
|
||||||
private readonly HttpClient _client = new();
|
private readonly HttpClient _client = new();
|
||||||
public LunaSea(string webhook, Logger? logger = null) : base(NotificationManagerType.LunaSea, logger)
|
public LunaSea(string id, Logger? logger = null) : base(NotificationManagerType.LunaSea, logger)
|
||||||
{
|
{
|
||||||
this.webhook = webhook;
|
this.id = id;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void SendNotification(string title, string notificationText)
|
public override void SendNotification(string title, string notificationText)
|
||||||
{
|
{
|
||||||
logger?.WriteLine(this.GetType().ToString(), $"Sending notification: {title} - {notificationText}");
|
logger?.WriteLine(this.GetType().ToString(), $"Sending notification: {title} - {notificationText}");
|
||||||
MessageData message = new(title, notificationText);
|
MessageData message = new(title, notificationText);
|
||||||
HttpRequestMessage request = new(HttpMethod.Post, webhook);
|
HttpRequestMessage request = new(HttpMethod.Post, $"https://notify.lunasea.app/v1/custom/{id}");
|
||||||
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);
|
||||||
if (!response.IsSuccessStatusCode)
|
if (!response.IsSuccessStatusCode)
|
||||||
|
@ -27,7 +27,8 @@ public class TaskManager
|
|||||||
{
|
{
|
||||||
new MangaDex(settings.downloadLocation, settings.coverImageCache, logger),
|
new MangaDex(settings.downloadLocation, settings.coverImageCache, logger),
|
||||||
new Manganato(settings.downloadLocation, settings.coverImageCache, logger),
|
new Manganato(settings.downloadLocation, settings.coverImageCache, logger),
|
||||||
new Mangasee(settings.downloadLocation, settings.coverImageCache, logger)
|
new Mangasee(settings.downloadLocation, settings.coverImageCache, logger),
|
||||||
|
new MangaKatana(settings.downloadLocation, settings.coverImageCache, logger)
|
||||||
};
|
};
|
||||||
|
|
||||||
this.settings = settings;
|
this.settings = settings;
|
||||||
@ -82,9 +83,8 @@ public class TaskManager
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
TrangaTask[] failedDownloadChapterTasks = _allTasks.Where(taskQuery =>
|
foreach (TrangaTask failedDownloadChapterTask in _allTasks.Where(taskQuery =>
|
||||||
taskQuery.state is TrangaTask.ExecutionState.Failed && taskQuery is DownloadChapterTask).ToArray();
|
taskQuery.state is TrangaTask.ExecutionState.Failed && taskQuery is DownloadChapterTask).ToArray())
|
||||||
foreach (TrangaTask failedDownloadChapterTask in failedDownloadChapterTasks)
|
|
||||||
{
|
{
|
||||||
DeleteTask(failedDownloadChapterTask);
|
DeleteTask(failedDownloadChapterTask);
|
||||||
TrangaTask newTask = failedDownloadChapterTask.Clone();
|
TrangaTask newTask = failedDownloadChapterTask.Clone();
|
||||||
@ -92,13 +92,6 @@ public class TaskManager
|
|||||||
AddTask(newTask);
|
AddTask(newTask);
|
||||||
}
|
}
|
||||||
|
|
||||||
TrangaTask[] successfulDownloadChapterTasks = _allTasks.Where(taskQuery =>
|
|
||||||
taskQuery.state is TrangaTask.ExecutionState.Success && taskQuery is DownloadChapterTask).ToArray();
|
|
||||||
foreach(TrangaTask successfulDownloadChapterTask in successfulDownloadChapterTasks)
|
|
||||||
{
|
|
||||||
DeleteTask(successfulDownloadChapterTask);
|
|
||||||
}
|
|
||||||
|
|
||||||
if(waitingTasksCount != _allTasks.Count(task => task.state is TrangaTask.ExecutionState.Waiting))
|
if(waitingTasksCount != _allTasks.Count(task => task.state is TrangaTask.ExecutionState.Waiting))
|
||||||
ExportDataAndSettings();
|
ExportDataAndSettings();
|
||||||
waitingTasksCount = _allTasks.Count(task => task.state is TrangaTask.ExecutionState.Waiting);
|
waitingTasksCount = _allTasks.Count(task => task.state is TrangaTask.ExecutionState.Waiting);
|
||||||
@ -136,18 +129,15 @@ public class TaskManager
|
|||||||
_allTasks.Add(newTask);
|
_allTasks.Add(newTask);
|
||||||
break;
|
break;
|
||||||
case TrangaTask.Task.MonitorPublication:
|
case TrangaTask.Task.MonitorPublication:
|
||||||
if (!_allTasks.Any(mTask => mTask is MonitorPublicationTask mpt && newTask is MonitorPublicationTask nMpt &&
|
MonitorPublicationTask mpt = (MonitorPublicationTask)newTask;
|
||||||
mpt.publication.internalId == nMpt.publication.internalId &&
|
if(!GetTasksMatching(mpt.task, mpt.connectorName, internalId:mpt.publication.internalId).Any())
|
||||||
mpt.connectorName == nMpt.connectorName))
|
|
||||||
_allTasks.Add(newTask);
|
_allTasks.Add(newTask);
|
||||||
else
|
else
|
||||||
logger?.WriteLine(this.GetType().ToString(), $"Task already exists {newTask}");
|
logger?.WriteLine(this.GetType().ToString(), $"Task already exists {newTask}");
|
||||||
break;
|
break;
|
||||||
case TrangaTask.Task.DownloadChapter:
|
case TrangaTask.Task.DownloadChapter:
|
||||||
if (!_allTasks.Any(mTask => mTask is DownloadChapterTask dct && newTask is DownloadChapterTask nDct &&
|
DownloadChapterTask dct = (DownloadChapterTask)newTask;
|
||||||
dct.publication.internalId == nDct.publication.internalId &&
|
if(!GetTasksMatching(dct.task, dct.connectorName, internalId:dct.publication.internalId, chapterSortNumber:dct.chapter.sortNumber).Any())
|
||||||
dct.connectorName == nDct.connectorName &&
|
|
||||||
dct.chapter.sortNumber == nDct.chapter.sortNumber))
|
|
||||||
_allTasks.Add(newTask);
|
_allTasks.Add(newTask);
|
||||||
else
|
else
|
||||||
logger?.WriteLine(this.GetType().ToString(), $"Task already exists {newTask}");
|
logger?.WriteLine(this.GetType().ToString(), $"Task already exists {newTask}");
|
||||||
@ -166,6 +156,8 @@ public class TaskManager
|
|||||||
_runningDownloadChapterTasks[cRemoveTask].Cancel();
|
_runningDownloadChapterTasks[cRemoveTask].Cancel();
|
||||||
_runningDownloadChapterTasks.Remove(cRemoveTask);
|
_runningDownloadChapterTasks.Remove(cRemoveTask);
|
||||||
}
|
}
|
||||||
|
foreach(TrangaTask childTask in removeTask.childTasks)
|
||||||
|
DeleteTask(childTask);
|
||||||
}
|
}
|
||||||
|
|
||||||
public IEnumerable<TrangaTask> GetTasksMatching(TrangaTask.Task taskType, string? connectorName = null, string? searchString = null, string? internalId = null, string? chapterSortNumber = null)
|
public IEnumerable<TrangaTask> GetTasksMatching(TrangaTask.Task taskType, string? connectorName = null, string? searchString = null, string? internalId = null, string? chapterSortNumber = null)
|
||||||
@ -188,7 +180,7 @@ public class TaskManager
|
|||||||
{
|
{
|
||||||
return _allTasks.Where(mTask =>
|
return _allTasks.Where(mTask =>
|
||||||
mTask is MonitorPublicationTask mpt && mpt.connectorName == connectorName &&
|
mTask is MonitorPublicationTask mpt && mpt.connectorName == connectorName &&
|
||||||
mpt.publication.internalId == internalId);
|
string.Concat(TrangaSettings.CleanIdRex.Matches(mpt.publication.internalId)) == string.Concat(TrangaSettings.CleanIdRex.Matches(internalId)));
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
return _allTasks.Where(tTask =>
|
return _allTasks.Where(tTask =>
|
||||||
@ -208,7 +200,7 @@ public class TaskManager
|
|||||||
{
|
{
|
||||||
return _allTasks.Where(mTask =>
|
return _allTasks.Where(mTask =>
|
||||||
mTask is DownloadChapterTask dct && dct.connectorName == connectorName &&
|
mTask is DownloadChapterTask dct && dct.connectorName == connectorName &&
|
||||||
dct.publication.publicationId == internalId &&
|
string.Concat(TrangaSettings.CleanIdRex.Matches(dct.publication.internalId)) == string.Concat(TrangaSettings.CleanIdRex.Matches(internalId)) &&
|
||||||
dct.chapter.sortNumber == chapterSortNumber);
|
dct.chapter.sortNumber == chapterSortNumber);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@ -342,17 +334,16 @@ public class TaskManager
|
|||||||
this._allTasks = JsonConvert.DeserializeObject<HashSet<TrangaTask>>(buffer, new JsonSerializerSettings() { Converters = { new TrangaTask.TrangaTaskJsonConverter() } })!;
|
this._allTasks = JsonConvert.DeserializeObject<HashSet<TrangaTask>>(buffer, new JsonSerializerSettings() { Converters = { new TrangaTask.TrangaTaskJsonConverter() } })!;
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (TrangaTask task in this._allTasks.Where(tTask => tTask.parentTaskId is not null))
|
foreach (TrangaTask task in this._allTasks.Where(tTask => tTask.parentTaskId is not null).ToArray())
|
||||||
{
|
{
|
||||||
TrangaTask? parentTask = this._allTasks.FirstOrDefault(pTask => pTask.taskId == task.parentTaskId);
|
TrangaTask? parentTask = this._allTasks.FirstOrDefault(pTask => pTask.taskId == task.parentTaskId);
|
||||||
if (parentTask is not null)
|
if (parentTask is not null)
|
||||||
{
|
{
|
||||||
task.parentTask = parentTask;
|
this.DeleteTask(task);
|
||||||
parentTask.AddChildTask(task);
|
parentTask.lastExecuted = DateTime.UnixEpoch;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if (File.Exists(settings.knownPublicationsPath))
|
if (File.Exists(settings.knownPublicationsPath))
|
||||||
{
|
{
|
||||||
logger?.WriteLine(this.GetType().ToString(), $"Importing known publications from {settings.knownPublicationsPath}");
|
logger?.WriteLine(this.GetType().ToString(), $"Importing known publications from {settings.knownPublicationsPath}");
|
||||||
@ -369,9 +360,7 @@ public class TaskManager
|
|||||||
private void ExportDataAndSettings()
|
private void ExportDataAndSettings()
|
||||||
{
|
{
|
||||||
logger?.WriteLine(this.GetType().ToString(), $"Exporting settings to {settings.settingsFilePath}");
|
logger?.WriteLine(this.GetType().ToString(), $"Exporting settings to {settings.settingsFilePath}");
|
||||||
while(IsFileInUse(settings.settingsFilePath))
|
settings.ExportSettings();
|
||||||
Thread.Sleep(50);
|
|
||||||
File.WriteAllText(settings.settingsFilePath, JsonConvert.SerializeObject(settings));
|
|
||||||
|
|
||||||
logger?.WriteLine(this.GetType().ToString(), $"Exporting tasks to {settings.tasksFilePath}");
|
logger?.WriteLine(this.GetType().ToString(), $"Exporting tasks to {settings.tasksFilePath}");
|
||||||
while(IsFileInUse(settings.tasksFilePath))
|
while(IsFileInUse(settings.tasksFilePath))
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
using Logging;
|
using System.Text.RegularExpressions;
|
||||||
|
using Logging;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using Tranga.LibraryManagers;
|
using Tranga.LibraryManagers;
|
||||||
using Tranga.NotificationManagers;
|
using Tranga.NotificationManagers;
|
||||||
@ -15,6 +16,7 @@ public class TrangaSettings
|
|||||||
[JsonIgnore] public string coverImageCache => Path.Join(workingDirectory, "imageCache");
|
[JsonIgnore] public string coverImageCache => Path.Join(workingDirectory, "imageCache");
|
||||||
public HashSet<LibraryManager> libraryManagers { get; }
|
public HashSet<LibraryManager> libraryManagers { get; }
|
||||||
public HashSet<NotificationManager> notificationManagers { get; }
|
public HashSet<NotificationManager> notificationManagers { get; }
|
||||||
|
[JsonIgnore] public static Regex CleanIdRex = new (@"([a-zA-Z0-9]*-*_*)*");
|
||||||
|
|
||||||
public TrangaSettings(string downloadLocation, string workingDirectory, HashSet<LibraryManager>? libraryManagers,
|
public TrangaSettings(string downloadLocation, string workingDirectory, HashSet<LibraryManager>? libraryManagers,
|
||||||
HashSet<NotificationManager>? notificationManagers)
|
HashSet<NotificationManager>? notificationManagers)
|
||||||
@ -43,6 +45,29 @@ public class TrangaSettings
|
|||||||
return settings;
|
return settings;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void ExportSettings()
|
||||||
|
{
|
||||||
|
if (File.Exists(settingsFilePath))
|
||||||
|
{
|
||||||
|
bool inUse = true;
|
||||||
|
while (inUse)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using FileStream stream = new (settingsFilePath, FileMode.Open, FileAccess.Read, FileShare.None);
|
||||||
|
stream.Close();
|
||||||
|
inUse = false;
|
||||||
|
}
|
||||||
|
catch (IOException)
|
||||||
|
{
|
||||||
|
inUse = true;
|
||||||
|
Thread.Sleep(50);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
File.WriteAllText(settingsFilePath, JsonConvert.SerializeObject(this));
|
||||||
|
}
|
||||||
|
|
||||||
public void UpdateSettings(UpdateField field, Logger? logger = null, params string[] values)
|
public void UpdateSettings(UpdateField field, Logger? logger = null, params string[] values)
|
||||||
{
|
{
|
||||||
switch (field)
|
switch (field)
|
||||||
@ -81,6 +106,7 @@ public class TrangaSettings
|
|||||||
newLunaSea.SendNotification("Success!", "LunaSea was added to Tranga!");
|
newLunaSea.SendNotification("Success!", "LunaSea was added to Tranga!");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
ExportSettings();
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum UpdateField { DownloadLocation, Komga, Kavita, Gotify, LunaSea}
|
public enum UpdateField { DownloadLocation, Komga, Kavita, Gotify, LunaSea}
|
||||||
|
@ -22,14 +22,14 @@ public abstract class TrangaTask
|
|||||||
public DateTime lastExecuted { get; set; }
|
public DateTime lastExecuted { get; set; }
|
||||||
[Newtonsoft.Json.JsonIgnore] public ExecutionState state { get; set; }
|
[Newtonsoft.Json.JsonIgnore] public ExecutionState state { get; set; }
|
||||||
public Task task { get; }
|
public Task task { get; }
|
||||||
public string taskId { get; }
|
public string taskId { get; init; }
|
||||||
[Newtonsoft.Json.JsonIgnore] public TrangaTask? parentTask { get; set; }
|
[Newtonsoft.Json.JsonIgnore] public TrangaTask? parentTask { get; set; }
|
||||||
public string? parentTaskId { get; set; }
|
public string? parentTaskId { get; set; }
|
||||||
[Newtonsoft.Json.JsonIgnore] protected HashSet<TrangaTask> childTasks { get; }
|
[Newtonsoft.Json.JsonIgnore] internal HashSet<TrangaTask> childTasks { get; }
|
||||||
public double progress => GetProgress();
|
public double progress => GetProgress();
|
||||||
[Newtonsoft.Json.JsonIgnore]public DateTime executionStarted { get; private set; }
|
[Newtonsoft.Json.JsonIgnore]public DateTime executionStarted { get; private set; }
|
||||||
[Newtonsoft.Json.JsonIgnore]public DateTime lastChange { get; private set; }
|
[Newtonsoft.Json.JsonIgnore]public DateTime lastChange { get; internal set; }
|
||||||
[Newtonsoft.Json.JsonIgnore]public DateTime executionApproximatelyFinished => progress != 0 ? lastChange.Add(GetRemainingTime()) : DateTime.MaxValue;
|
[Newtonsoft.Json.JsonIgnore]public DateTime executionApproximatelyFinished => lastChange.Add(GetRemainingTime());
|
||||||
public TimeSpan executionApproximatelyRemaining => executionApproximatelyFinished.Subtract(DateTime.Now);
|
public TimeSpan executionApproximatelyRemaining => executionApproximatelyFinished.Subtract(DateTime.Now);
|
||||||
[Newtonsoft.Json.JsonIgnore]public DateTime nextExecution => lastExecuted.Add(reoccurrence);
|
[Newtonsoft.Json.JsonIgnore]public DateTime nextExecution => lastExecuted.Add(reoccurrence);
|
||||||
|
|
||||||
@ -72,9 +72,16 @@ public abstract class TrangaTask
|
|||||||
this.state = ExecutionState.Running;
|
this.state = ExecutionState.Running;
|
||||||
this.executionStarted = DateTime.Now;
|
this.executionStarted = DateTime.Now;
|
||||||
this.lastChange = DateTime.Now;
|
this.lastChange = DateTime.Now;
|
||||||
|
if(parentTask is not null && parentTask.childTasks.All(ct => ct.state is ExecutionState.Waiting or ExecutionState.Failed))
|
||||||
|
parentTask.executionStarted = DateTime.Now;
|
||||||
|
|
||||||
HttpStatusCode statusCode = ExecuteTask(taskManager, logger, cancellationToken);
|
HttpStatusCode statusCode = ExecuteTask(taskManager, logger, cancellationToken);
|
||||||
|
|
||||||
while(childTasks.Any(ct => ct.state is ExecutionState.Enqueued or ExecutionState.Running))
|
while(childTasks.Any(ct => ct.state is ExecutionState.Enqueued or ExecutionState.Running))
|
||||||
Thread.Sleep(1000);
|
Thread.Sleep(1000);
|
||||||
|
foreach(TrangaTask childTask in this.childTasks.ToArray())
|
||||||
|
taskManager.DeleteTask(childTask);
|
||||||
|
|
||||||
if ((int)statusCode >= 200 && (int)statusCode < 300)
|
if ((int)statusCode >= 200 && (int)statusCode < 300)
|
||||||
{
|
{
|
||||||
this.lastExecuted = DateTime.Now;
|
this.lastExecuted = DateTime.Now;
|
||||||
@ -106,10 +113,10 @@ public abstract class TrangaTask
|
|||||||
|
|
||||||
private TimeSpan GetRemainingTime()
|
private TimeSpan GetRemainingTime()
|
||||||
{
|
{
|
||||||
if(progress == 0 || lastChange == DateTime.MaxValue || executionStarted == DateTime.UnixEpoch)
|
if(progress == 0 || state is ExecutionState.Enqueued or ExecutionState.Waiting or ExecutionState.Failed || lastChange == DateTime.MaxValue)
|
||||||
return TimeSpan.Zero;
|
return DateTime.MaxValue.Subtract(lastChange).Subtract(TimeSpan.FromHours(1));
|
||||||
TimeSpan elapsed = lastChange.Subtract(executionStarted);
|
TimeSpan elapsed = lastChange.Subtract(executionStarted);
|
||||||
return elapsed.Divide(progress).Subtract(elapsed);
|
return elapsed.Divide(progress).Multiply(1 - progress);
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum Task : byte
|
public enum Task : byte
|
||||||
|
@ -47,6 +47,9 @@ public class DownloadChapterTask : TrangaTask
|
|||||||
internal void IncrementProgress(double amount)
|
internal void IncrementProgress(double amount)
|
||||||
{
|
{
|
||||||
this._dctProgress += amount;
|
this._dctProgress += amount;
|
||||||
|
this.lastChange = DateTime.Now;
|
||||||
|
if(this.parentTask is not null)
|
||||||
|
this.parentTask.lastChange = DateTime.Now;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override string ToString()
|
public override string ToString()
|
||||||
|
@ -84,7 +84,7 @@ async function GetRunningTasks(){
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function GetDownloadTasks(){
|
async function GetDownloadTasks(){
|
||||||
var uri = apiUri + "/Tasks?taskType=DownloadNewChapters";
|
var uri = apiUri + "/Tasks?taskType=MonitorPublication";
|
||||||
let json = await GetData(uri);
|
let json = await GetData(uri);
|
||||||
return json;
|
return json;
|
||||||
}
|
}
|
||||||
|
@ -142,7 +142,7 @@
|
|||||||
<div>
|
<div>
|
||||||
<span class="title">LunaSea</span>
|
<span class="title">LunaSea</span>
|
||||||
<div>Configured: <span id="lunaseaConfigured">✅❌</span></div>
|
<div>Configured: <span id="lunaseaConfigured">✅❌</span></div>
|
||||||
<label for="lunaseaWebhook"></label><input placeholder="Webhook-Url" id="lunaseaWebhook" type="text">
|
<label for="lunaseaWebhook"></label><input placeholder="device/:id or user/:id" id="lunaseaWebhook" type="text">
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label for="libraryUpdateTime" style="margin-right: 5px;">Update Time</label><input id="libraryUpdateTime" type="time" value="00:01:00" step="10">
|
<label for="libraryUpdateTime" style="margin-right: 5px;">Update Time</label><input id="libraryUpdateTime" type="time" value="00:01:00" step="10">
|
||||||
|
@ -195,13 +195,13 @@ function DownloadChapterTaskClick(){
|
|||||||
|
|
||||||
function DeleteTaskClick(){
|
function DeleteTaskClick(){
|
||||||
taskToDelete = tasks.filter(tTask => tTask.publication.internalId === toEditId)[0];
|
taskToDelete = tasks.filter(tTask => tTask.publication.internalId === toEditId)[0];
|
||||||
DeleteTask("DownloadNewChapters", taskToDelete.connectorName, toEditId);
|
DeleteTask("MonitorPublication", taskToDelete.connectorName, toEditId);
|
||||||
HidePublicationPopup();
|
HidePublicationPopup();
|
||||||
}
|
}
|
||||||
|
|
||||||
function StartTaskClick(){
|
function StartTaskClick(){
|
||||||
var toEditTask = tasks.filter(task => task.publication.internalId == toEditId)[0];
|
var toEditTask = tasks.filter(task => task.publication.internalId == toEditId)[0];
|
||||||
StartTask("DownloadNewChapters", toEditTask.connectorName, toEditId);
|
StartTask("MonitorPublication", toEditTask.connectorName, toEditId);
|
||||||
HidePublicationPopup();
|
HidePublicationPopup();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Before Width: | Height: | Size: 1.0 MiB After Width: | Height: | Size: 2.1 MiB |
Before Width: | Height: | Size: 2.6 MiB After Width: | Height: | Size: 2.5 MiB |
BIN
screenshots/progress.png
Normal file
After Width: | Height: | Size: 354 KiB |
Before Width: | Height: | Size: 2.2 MiB After Width: | Height: | Size: 1.8 MiB |
Before Width: | Height: | Size: 1.7 MiB After Width: | Height: | Size: 1.5 MiB |