42 Commits
1.4 ... 1.4.2

Author SHA1 Message Date
9fd8bf1741 website uses taskId 2023-06-10 16:00:41 +02:00
d5c9c5ba96 Redid progress calcuation on DownloadNewChaptersTask and DownloadChapterTask 2023-06-10 16:00:16 +02:00
c8e27921ab Added taskId to trangaTask and parentTaskId to DownloadChapterTask as unique identifier to attach ChildTasks to ParentTask on deserialization. 2023-06-10 15:59:42 +02:00
6eaba07801 Changed progress type from float to double 2023-06-10 15:58:11 +02:00
41929e0c72 DownloadChapterTask sets execution of parentTask 2023-06-10 15:04:37 +02:00
4fcaca1a6e Multiple authors resolves #7 2023-06-10 14:45:04 +02:00
0e3c7f32d7 Added CancellationToken to TrangaTask #14 2023-06-10 14:34:30 +02:00
1c94625840 Added CancellationToken to TrangaTask #14 2023-06-10 14:27:09 +02:00
32f89f9dce Multiple authors resolves #7 2023-06-10 14:05:23 +02:00
234735a562 Order of tasks closes #15
Also API /Queue/Get orders in order of nextExecution
2023-06-10 00:45:55 +02:00
8b916eb854 invalid Ids 2023-06-10 00:23:23 +02:00
29e1790c93 website tasks-width now max 95vw 2023-06-10 00:10:16 +02:00
ac4c799a74 Better indication if tasks have started. 2023-06-10 00:07:41 +02:00
7c62883c37 invalid id 2023-06-10 00:02:51 +02:00
02018253bf wrong nesting ... 2023-06-10 00:01:38 +02:00
2aec884009 Moved update interval for task-progress to own interval, progress gets continually updated. 2023-06-09 23:58:04 +02:00
b3321ff030 unnecessary log 2023-06-09 23:48:39 +02:00
16c1094875 Replaced Task-Progress-Tracking Window with more fancy one 2023-06-09 23:46:10 +02:00
5763d50409 #14 temporary workaround for disposing tasks 2023-06-09 23:45:53 +02:00
ad43297358 API: Updated /Tasks/GetProgress to return progress of specific task (by sortNumber) 2023-06-09 23:43:57 +02:00
b17800e0ef Decrement progress of parenttask when childtask fails 2023-06-09 23:43:19 +02:00
89c80d2997 Fixed bug where tasks would instantly failed when launched #14 2023-06-09 23:42:54 +02:00
6485b8744f API: Updated /Tasks/GetProgress to return progress of specific task (by sortNumber) 2023-06-09 23:42:18 +02:00
a3a96b6b55 Added DecrementProgress function to TrangaTask 2023-06-09 23:38:28 +02:00
5bce3c6fdd Website: Monitor task creation styling 2023-06-09 22:15:29 +02:00
5fa0c98d05 Documentation how to create tasks #11 2023-06-09 11:26:51 +02:00
b166013770 resolves #13 Website: Clear previous results 2023-06-09 11:12:43 +02:00
02fe849046 Better downloadChapter selection 2023-06-09 11:06:18 +02:00
d42393c83a Website + API ability to download specific volumes 2023-06-08 19:53:05 +02:00
c685bd622f Website:
New task-Creation dialog
Redesigned Settings dialog
2023-06-08 19:25:28 +02:00
dc83cc2194 Fixed Range on CLI downloadchaptertask creation 2023-06-08 19:25:03 +02:00
7784f2024e API changes:
/Tranga/GetAvailableControllers => /Controllers/Get
/Tranga/GetKnownPublications =>/Publications/GetKnown
/Tranga/GetPublicationsFromConnector => /Publications/GetFromConnector
/Tasks/GetTaskTypes => /Tasks/GetTypes
/Tasks/GetTaskProgress => /Tasks/GetProgress
/Tasks/Create is now split in 3:
    /Tasks/CreateMonitorTask
    /Tasks/CreateUpdateLibraryTask
    /Tasks/CreateDownloadChaptersTask
2023-06-08 19:24:46 +02:00
4895079887 Remove DownloadChapterTask from _runningDownloadChapterTasks after completion 2023-06-07 15:01:24 +02:00
ab1ddc6dc8 Less cluttered log 2023-06-07 00:31:27 +02:00
87eade10cf #40 task timeout criteria 2023-06-07 00:27:53 +02:00
1f3ac41b30 removed unnecessary cast 2023-06-07 00:24:58 +02:00
6a304bb330 #40 task timeout 2023-06-07 00:24:27 +02:00
b0642d1251 removed unnecessary check 2023-06-06 22:11:57 +02:00
63b5139e93 Split error message for better logging 2023-06-06 22:11:38 +02:00
e938784388 Created own base image for tranga-api (to stop apt always updating) 2023-06-06 22:11:26 +02:00
c436389426 renamed wrong variable names publicationId -> internalId 2023-06-06 21:57:10 +02:00
5099e25f3f Fixed wrong comparison on add new task 2023-06-06 21:56:51 +02:00
20 changed files with 868 additions and 427 deletions

View File

@ -6,9 +6,9 @@ COPY . /src/
RUN dotnet restore Tranga-API/Tranga-API.csproj RUN dotnet restore Tranga-API/Tranga-API.csproj
RUN dotnet publish -c Release -o /publish RUN dotnet publish -c Release -o /publish
FROM mcr.microsoft.com/dotnet/aspnet:7.0 as runtime #FROM mcr.microsoft.com/dotnet/aspnet:7.0 as runtime
FROM glax/tranga-base:latest as runtime
WORKDIR /publish WORKDIR /publish
COPY --from=build-env /publish . COPY --from=build-env /publish .
EXPOSE 80 EXPOSE 80
RUN apt-get update && apt-get install -y libx11-6 libx11-xcb1 libatk1.0-0 libgtk-3-0 libcups2 libdrm2 libxkbcommon0 libxcomposite1 libxdamage1 libxrandr2 libgbm1 libpango-1.0-0 libcairo2 libasound2 libxshmfence1 libnss3
ENTRYPOINT ["dotnet", "/publish/Tranga-API.dll"] ENTRYPOINT ["dotnet", "/publish/Tranga-API.dll"]

4
Dockerfile-base Normal file
View File

@ -0,0 +1,4 @@
# syntax=docker/dockerfile:1
FROM mcr.microsoft.com/dotnet/aspnet:7.0 as runtime
WORKDIR /publish
RUN apt-get update && apt-get install -y libx11-6 libx11-xcb1 libatk1.0-0 libgtk-3-0 libcups2 libdrm2 libxkbcommon0 libxcomposite1 libxdamage1 libxrandr2 libgbm1 libpango-1.0-0 libcairo2 libasound2 libxshmfence1 libnss3

View File

@ -36,6 +36,7 @@
<li> <li>
<a href="#getting-started">Getting Started</a> <a href="#getting-started">Getting Started</a>
<ul> <ul>
<li><a href="#prerequisites">Usage</a></li>
<li><a href="#prerequisites">Prerequisites</a></li> <li><a href="#prerequisites">Prerequisites</a></li>
</ul> </ul>
</li> </li>
@ -109,6 +110,28 @@ 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
There is two ways to download Mangas:
- Downloading everything and monitor for new 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.
After pressing 'Search', the results will be presented below - this might, depending on the result-size, take a while.
Next select the publication and a new popup will open with two options:
- "Monitor" - Download all chapters and monitor for new ones
- "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 `Download Chapter` a list will open with all available chapters from which you can then select a range.
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 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.
Examples: `2-12`, `c1`, `ch 2`, `chapter 3`, `v 2`, `vol3-4`, `v2c4` (note: you can only specify a single chapter with this syntax).
### Prerequisites ### Prerequisites
[.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)

View File

@ -1,6 +1,7 @@
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using Logging; using Logging;
using Tranga; using Tranga;
using Tranga.TrangaTasks;
string applicationFolderPath = Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), "Tranga-API"); string applicationFolderPath = Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), "Tranga-API");
string downloadFolderPath = RuntimeInformation.IsOSPlatform(OSPlatform.Linux) ? "/Manga" : Path.Join(applicationFolderPath, "Manga"); string downloadFolderPath = RuntimeInformation.IsOSPlatform(OSPlatform.Linux) ? "/Manga" : Path.Join(applicationFolderPath, "Manga");
@ -54,11 +55,17 @@ app.UseSwaggerUI();
app.UseCors(corsHeader); app.UseCors(corsHeader);
app.MapGet("/Tranga/GetAvailableControllers", () => taskManager.GetAvailableConnectors().Keys.ToArray()); app.MapGet("/Controllers/Get", () => taskManager.GetAvailableConnectors().Keys.ToArray());
app.MapGet("/Tranga/GetKnownPublications", () => taskManager.GetAllPublications()); app.MapGet("/Publications/GetKnown", (string? internalId) =>
{
if(internalId is null)
return taskManager.GetAllPublications();
return new [] { taskManager.GetAllPublications().FirstOrDefault(pub => pub.internalId == internalId) };
});
app.MapGet("/Tranga/GetPublicationsFromConnector", (string connectorName, string title) => app.MapGet("/Publications/GetFromConnector", (string connectorName, string title) =>
{ {
Connector? connector = taskManager.GetAvailableConnectors().FirstOrDefault(con => con.Key == connectorName).Value; Connector? connector = taskManager.GetAvailableConnectors().FirstOrDefault(con => con.Key == connectorName).Value;
if (connector is null) if (connector is null)
@ -68,13 +75,54 @@ app.MapGet("/Tranga/GetPublicationsFromConnector", (string connectorName, string
return taskManager.GetPublicationsFromConnector(connector, title); return taskManager.GetPublicationsFromConnector(connector, title);
}); });
app.MapGet("/Tasks/GetTaskTypes", () => Enum.GetNames(typeof(TrangaTask.Task))); app.MapGet("/Publications/GetChapters", (string connectorName, string internalId, string? language) =>
app.MapPost("/Tasks/Create", (string taskType, string? connectorName, string? publicationId, string reoccurrenceTime, string? language) =>
{ {
TrangaTask.Task task = Enum.Parse<TrangaTask.Task>(taskType); Connector? connector = taskManager.GetAvailableConnectors().FirstOrDefault(con => con.Key == connectorName).Value;
taskManager.AddTask(task, connectorName, publicationId, TimeSpan.Parse(reoccurrenceTime), language??""); if (connector is null)
return Array.Empty<Chapter>();
Publication? publication = taskManager.GetAllPublications().FirstOrDefault(pub => pub.internalId == internalId);
if (publication is null)
return Array.Empty<Chapter>();
return connector.GetChapters((Publication)publication, language??"en");
});
app.MapGet("/Tasks/GetTypes", () => Enum.GetNames(typeof(TrangaTask.Task)));
app.MapPost("/Tasks/CreateMonitorTask",
(string connectorName, string internalId, string reoccurrenceTime, string? language) =>
{
Connector? connector =
taskManager.GetAvailableConnectors().FirstOrDefault(con => con.Key == connectorName).Value;
if (connector is null)
return;
Publication? publication = taskManager.GetAllPublications().FirstOrDefault(pub => pub.internalId == internalId);
if (publication is null)
return;
taskManager.AddTask(new DownloadNewChaptersTask(TrangaTask.Task.DownloadNewChapters, connectorName,
(Publication)publication,
TimeSpan.Parse(reoccurrenceTime), language ?? "en"));
});
app.MapPost("/Tasks/CreateUpdateLibraryTask", (string reoccurrenceTime) =>
{
taskManager.AddTask(new UpdateLibrariesTask(TrangaTask.Task.UpdateLibraries, TimeSpan.Parse(reoccurrenceTime)));
});
app.MapPost("/Tasks/CreateDownloadChaptersTask", (string connectorName, string internalId, string chapters, string? language) => {
Connector? connector =
taskManager.GetAvailableConnectors().FirstOrDefault(con => con.Key == connectorName).Value;
if (connector is null)
return;
Publication? publication = taskManager.GetAllPublications().FirstOrDefault(pub => pub.internalId == internalId);
if (publication is null)
return;
IEnumerable<Chapter> toDownload = connector.SearchChapters((Publication)publication, chapters, language ?? "en");
foreach(Chapter chapter in toDownload)
taskManager.AddTask(new DownloadChapterTask(TrangaTask.Task.DownloadChapter, connectorName,
(Publication)publication, chapter, "en"));
}); });
app.MapDelete("/Tasks/Delete", (string taskType, string? connectorName, string? publicationId) => app.MapDelete("/Tasks/Delete", (string taskType, string? connectorName, string? publicationId) =>
@ -96,14 +144,24 @@ app.MapGet("/Tasks/Get", (string taskType, string? connectorName, string? search
} }
}); });
app.MapGet("/Tasks/GetTaskProgress", (string taskType, string? connectorName, string? publicationId) => app.MapGet("/Tasks/GetProgress", (string taskType, string connectorName, string publicationId, string? chapterSortNumber) =>
{ {
Connector? connector =
taskManager.GetAvailableConnectors().FirstOrDefault(con => con.Key == connectorName).Value;
if (connector is null)
return -1f;
try try
{ {
TrangaTask? task = null;
TrangaTask.Task pTask = Enum.Parse<TrangaTask.Task>(taskType); TrangaTask.Task pTask = Enum.Parse<TrangaTask.Task>(taskType);
TrangaTask? task = taskManager if (pTask is TrangaTask.Task.DownloadNewChapters)
.GetTasksMatching(pTask, connectorName: connectorName, internalId: publicationId)?.First(); {
task = taskManager.GetTasksMatching(pTask, connectorName: connectorName, internalId: publicationId).FirstOrDefault();
}else if (pTask is TrangaTask.Task.DownloadChapter && chapterSortNumber is not null)
{
task = taskManager.GetTasksMatching(pTask, connectorName: connectorName, internalId: publicationId,
chapterSortNumber: chapterSortNumber).FirstOrDefault();
}
if (task is null) if (task is null)
return -1f; return -1f;
@ -138,7 +196,7 @@ app.MapGet("/Tasks/GetRunningTasks",
() => taskManager.GetAllTasks().Where(task => task.state is TrangaTask.ExecutionState.Running)); () => taskManager.GetAllTasks().Where(task => task.state is TrangaTask.ExecutionState.Running));
app.MapGet("/Queue/GetList", app.MapGet("/Queue/GetList",
() => taskManager.GetAllTasks().Where(task => task.state is TrangaTask.ExecutionState.Enqueued)); () => taskManager.GetAllTasks().Where(task => task.state is TrangaTask.ExecutionState.Enqueued).OrderBy(task => task.nextExecution));
app.MapPost("/Queue/Enqueue", (string taskType, string? connectorName, string? publicationId) => app.MapPost("/Queue/Enqueue", (string taskType, string? connectorName, string? publicationId) =>
{ {

View File

@ -499,22 +499,7 @@ public static class Tranga_Cli
while(selectedChapters is null || selectedChapters.Length < 1) while(selectedChapters is null || selectedChapters.Length < 1)
selectedChapters = Console.ReadLine(); selectedChapters = Console.ReadLine();
if (selectedChapters.Length == 1 && selectedChapters.ToLower() == "q") return connector.SearchChapters(publication, selectedChapters);
{
Console.Clear();
Console.WriteLine("aborted.");
logger.WriteLine("Tranga_CLI", "aborted.");
return Array.Empty<Chapter>();
}
if (selectedChapters.Contains('-'))
{
int start = Convert.ToInt32(selectedChapters.Split('-')[0]);
int end = Convert.ToInt32(selectedChapters.Split('-')[1]);
return availableChapters[start..end];
}
else
return new Chapter[] { availableChapters[Convert.ToInt32(selectedChapters)] };
} }
private static Connector? SelectConnector(Connector[] connectors, Logger logger) private static Connector? SelectConnector(Connector[] connectors, Logger logger)

View File

@ -53,6 +53,72 @@ public abstract class Connector
/// <param name="language">Language of the Chapters</param> /// <param name="language">Language of the Chapters</param>
/// <returns>Array of Chapters matching Publication and Language</returns> /// <returns>Array of Chapters matching Publication and Language</returns>
public abstract Chapter[] GetChapters(Publication publication, string language = ""); public abstract Chapter[] GetChapters(Publication publication, string language = "");
public Chapter[] SearchChapters(Publication publication, string searchTerm, string? language = null)
{
Chapter[] availableChapters = this.GetChapters(publication, language??"en");
Regex volumeRegex = new ("((v(ol)*(olume)*)+ *([0-9]+(-[0-9]+)?){1})", RegexOptions.IgnoreCase);
Regex chapterRegex = new ("((c(h)*(hapter)*)+ *([0-9]+(-[0-9]+)?){1})", RegexOptions.IgnoreCase);
Regex singleResultRegex = new("([0-9]+)", RegexOptions.IgnoreCase);
Regex rangeResultRegex = new("([0-9]+(-[0-9]+))", RegexOptions.IgnoreCase);
if (volumeRegex.IsMatch(searchTerm) && chapterRegex.IsMatch(searchTerm))
{
string volume = singleResultRegex.Match(volumeRegex.Match(searchTerm).Value).Value;
string chapter = singleResultRegex.Match(chapterRegex.Match(searchTerm).Value).Value;
return availableChapters.Where(aCh => aCh.volumeNumber is not null && aCh.chapterNumber is not null &&
aCh.volumeNumber.Equals(volume, StringComparison.InvariantCultureIgnoreCase) &&
aCh.chapterNumber.Equals(chapter, StringComparison.InvariantCultureIgnoreCase))
.ToArray();
}
else if (volumeRegex.IsMatch(searchTerm))
{
string volume = volumeRegex.Match(searchTerm).Value;
if (rangeResultRegex.IsMatch(volume))
{
string range = rangeResultRegex.Match(volume).Value;
int start = Convert.ToInt32(range.Split('-')[0]);
int end = Convert.ToInt32(range.Split('-')[1]);
return availableChapters.Where(aCh => aCh.volumeNumber is not null &&
Convert.ToInt32(aCh.volumeNumber) >= start &&
Convert.ToInt32(aCh.volumeNumber) <= end).ToArray();
}
else if(singleResultRegex.IsMatch(volume))
return availableChapters.Where(aCh =>
aCh.volumeNumber is not null &&
aCh.volumeNumber.Equals(volume, StringComparison.InvariantCultureIgnoreCase)).ToArray();
}
else if (chapterRegex.IsMatch(searchTerm))
{
string chapter = volumeRegex.Match(searchTerm).Value;
if (rangeResultRegex.IsMatch(chapter))
{
string range = rangeResultRegex.Match(chapter).Value;
int start = Convert.ToInt32(range.Split('-')[0]);
int end = Convert.ToInt32(range.Split('-')[1]);
return availableChapters.Where(aCh => aCh.chapterNumber is not null &&
Convert.ToInt32(aCh.chapterNumber) >= start &&
Convert.ToInt32(aCh.chapterNumber) <= end).ToArray();
}
else if(singleResultRegex.IsMatch(chapter))
return availableChapters.Where(aCh =>
aCh.chapterNumber is not null &&
aCh.chapterNumber.Equals(chapter, StringComparison.InvariantCultureIgnoreCase)).ToArray();
}
else
{
if (rangeResultRegex.IsMatch(searchTerm))
{
int start = Convert.ToInt32(searchTerm.Split('-')[0]);
int end = Convert.ToInt32(searchTerm.Split('-')[1]);
return availableChapters[start..(end + 1)];
}
else if(singleResultRegex.IsMatch(searchTerm))
return new [] { availableChapters[Convert.ToInt32(searchTerm)] };
}
return Array.Empty<Chapter>();
}
/// <summary> /// <summary>
/// Retrieves the Chapter (+Images) from the website. /// Retrieves the Chapter (+Images) from the website.
@ -61,7 +127,8 @@ public abstract class Connector
/// <param name="publication">Publication that contains Chapter</param> /// <param name="publication">Publication that contains Chapter</param>
/// <param name="chapter">Chapter with Images to retrieve</param> /// <param name="chapter">Chapter with Images to retrieve</param>
/// <param name="parentTask">Will be used for progress-tracking</param> /// <param name="parentTask">Will be used for progress-tracking</param>
public abstract void DownloadChapter(Publication publication, Chapter chapter, DownloadChapterTask parentTask); /// <param name="cancellationToken"></param>
public abstract void DownloadChapter(Publication publication, Chapter chapter, DownloadChapterTask parentTask, CancellationToken? cancellationToken = null);
/// <summary> /// <summary>
/// Copies the already downloaded cover from cache to downloadLocation /// Copies the already downloaded cover from cache to downloadLocation
@ -100,7 +167,7 @@ public abstract class Connector
new XElement("Tags", string.Join(',',publication.tags)), new XElement("Tags", string.Join(',',publication.tags)),
new XElement("LanguageISO", publication.originalLanguage), new XElement("LanguageISO", publication.originalLanguage),
new XElement("Title", chapter.name), new XElement("Title", chapter.name),
new XElement("Writer", publication.author), new XElement("Writer", string.Join(',', publication.authors)),
new XElement("Volume", chapter.volumeNumber), new XElement("Volume", chapter.volumeNumber),
new XElement("Number", chapter.chapterNumber) new XElement("Number", chapter.chapterNumber)
); );
@ -161,8 +228,10 @@ public abstract class Connector
/// <param name="comicInfoPath">Path of the generate Chapter ComicInfo.xml, if it was generated</param> /// <param name="comicInfoPath">Path of the generate Chapter ComicInfo.xml, if it was generated</param>
/// <param name="requestType">RequestType for RateLimits</param> /// <param name="requestType">RequestType for RateLimits</param>
/// <param name="referrer">Used in http request header</param> /// <param name="referrer">Used in http request header</param>
protected void DownloadChapterImages(string[] imageUrls, string saveArchiveFilePath, byte requestType, DownloadChapterTask parentTask, string? comicInfoPath = null, string? referrer = null) protected void DownloadChapterImages(string[] imageUrls, string saveArchiveFilePath, byte requestType, DownloadChapterTask parentTask, string? comicInfoPath = null, string? referrer = null, CancellationToken? cancellationToken = null)
{ {
if (cancellationToken?.IsCancellationRequested??false)
return;
logger?.WriteLine("Connector", $"Downloading Images for {saveArchiveFilePath}"); logger?.WriteLine("Connector", $"Downloading Images for {saveArchiveFilePath}");
//Check if Publication Directory already exists //Check if Publication Directory already exists
string directoryPath = Path.GetDirectoryName(saveArchiveFilePath)!; string directoryPath = Path.GetDirectoryName(saveArchiveFilePath)!;
@ -183,7 +252,9 @@ public abstract class Connector
string extension = split[^1]; string extension = split[^1];
logger?.WriteLine("Connector", $"Downloading Image {chapter + 1:000}/{imageUrls.Length:000} {parentTask.publication.sortName} {parentTask.publication.internalId} Vol.{parentTask.chapter.volumeNumber} Ch.{parentTask.chapter.chapterNumber} {parentTask.progress:P2}"); logger?.WriteLine("Connector", $"Downloading Image {chapter + 1:000}/{imageUrls.Length:000} {parentTask.publication.sortName} {parentTask.publication.internalId} Vol.{parentTask.chapter.volumeNumber} Ch.{parentTask.chapter.chapterNumber} {parentTask.progress:P2}");
DownloadImage(imageUrl, Path.Join(tempFolder, $"{chapter++}.{extension}"), requestType, referrer); DownloadImage(imageUrl, Path.Join(tempFolder, $"{chapter++}.{extension}"), requestType, referrer);
parentTask.IncrementProgress(1f / imageUrls.Length); parentTask.IncrementProgress(1.0 / imageUrls.Length);
if (cancellationToken?.IsCancellationRequested??false)
return;
} }
if(comicInfoPath is not null) if(comicInfoPath is not null)

View File

@ -93,19 +93,21 @@ public class MangaDex : Connector
} }
string? posterId = null; string? posterId = null;
string? authorId = null; HashSet<string> authorIds = new();
if (manga.ContainsKey("relationships") && manga["relationships"] is not null) if (manga.ContainsKey("relationships") && manga["relationships"] is not null)
{ {
JsonArray relationships = manga["relationships"]!.AsArray(); JsonArray relationships = manga["relationships"]!.AsArray();
posterId = relationships.FirstOrDefault(relationship => relationship!["type"]!.GetValue<string>() == "cover_art")!["id"]!.GetValue<string>(); posterId = relationships.FirstOrDefault(relationship => relationship!["type"]!.GetValue<string>() == "cover_art")!["id"]!.GetValue<string>();
authorId = relationships.FirstOrDefault(relationship => relationship!["type"]!.GetValue<string>() == "author")!["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? coverUrl = GetCoverUrl(publicationId, posterId);
string? coverCacheName = null; string? coverCacheName = null;
if (coverUrl is not null) if (coverUrl is not null)
coverCacheName = SaveCoverImageToCache(coverUrl, (byte)RequestType.AtHomeServer); coverCacheName = SaveCoverImageToCache(coverUrl, (byte)RequestType.AtHomeServer);
string? author = GetAuthor(authorId); List<string> authors = GetAuthors(authorIds);
Dictionary<string, string> linksDict = new(); Dictionary<string, string> linksDict = new();
if (attributes.ContainsKey("links") && attributes["links"] is not null) if (attributes.ContainsKey("links") && attributes["links"] is not null)
@ -129,7 +131,7 @@ public class MangaDex : Connector
Publication pub = new ( Publication pub = new (
title, title,
author, authors,
description, description,
altTitlesDict, altTitlesDict,
tags.ToArray(), tags.ToArray(),
@ -205,8 +207,10 @@ public class MangaDex : Connector
return chapters.OrderBy(chapter => Convert.ToSingle(chapter.chapterNumber, chapterNumberFormatInfo)).ToArray(); return chapters.OrderBy(chapter => Convert.ToSingle(chapter.chapterNumber, chapterNumberFormatInfo)).ToArray();
} }
public override void DownloadChapter(Publication publication, Chapter chapter, DownloadChapterTask parentTask) public override void DownloadChapter(Publication publication, Chapter chapter, DownloadChapterTask parentTask, CancellationToken? cancellationToken = null)
{ {
if (cancellationToken?.IsCancellationRequested??false)
return;
logger?.WriteLine(this.GetType().ToString(), $"Downloading Chapter-Info {publication.sortName} {publication.internalId} {chapter.volumeNumber}-{chapter.chapterNumber}"); logger?.WriteLine(this.GetType().ToString(), $"Downloading Chapter-Info {publication.sortName} {publication.internalId} {chapter.volumeNumber}-{chapter.chapterNumber}");
//Request URLs for Chapter-Images //Request URLs for Chapter-Images
DownloadClient.RequestResult requestResult = DownloadClient.RequestResult requestResult =
@ -229,7 +233,7 @@ public class MangaDex : Connector
File.WriteAllText(comicInfoPath, GetComicInfoXmlString(publication, chapter, logger)); File.WriteAllText(comicInfoPath, GetComicInfoXmlString(publication, chapter, logger));
//Download Chapter-Images //Download Chapter-Images
DownloadChapterImages(imageUrls.ToArray(), GetArchiveFilePath(publication, chapter), (byte)RequestType.AtHomeServer, parentTask, comicInfoPath); DownloadChapterImages(imageUrls.ToArray(), GetArchiveFilePath(publication, chapter), (byte)RequestType.AtHomeServer, parentTask, comicInfoPath, cancellationToken:cancellationToken);
} }
private string? GetCoverUrl(string publicationId, string? posterId) private string? GetCoverUrl(string publicationId, string? posterId)
@ -257,21 +261,23 @@ public class MangaDex : Connector
return coverUrl; return coverUrl;
} }
private string? GetAuthor(string? authorId) private List<string> GetAuthors(IEnumerable<string> authorIds)
{ {
if (authorId is null) List<string> ret = new();
return null; foreach (string authorId in authorIds)
{
DownloadClient.RequestResult requestResult = DownloadClient.RequestResult requestResult =
downloadClient.MakeRequest($"https://api.mangadex.org/author/{authorId}", (byte)RequestType.Author); downloadClient.MakeRequest($"https://api.mangadex.org/author/{authorId}", (byte)RequestType.Author);
if (requestResult.statusCode != HttpStatusCode.OK) if (requestResult.statusCode != HttpStatusCode.OK)
return null; return ret;
JsonObject? result = JsonSerializer.Deserialize<JsonObject>(requestResult.result); JsonObject? result = JsonSerializer.Deserialize<JsonObject>(requestResult.result);
if (result is null) if (result is null)
return null; return ret;
string author = result["data"]!["attributes"]!["name"]!.GetValue<string>(); string authorName = result["data"]!["attributes"]!["name"]!.GetValue<string>();
logger?.WriteLine(this.GetType().ToString(), $"Got author {authorId} -> {author}"); ret.Add(authorName);
return author; logger?.WriteLine(this.GetType().ToString(), $"Got author {authorId} -> {authorName}");
}
return ret;
} }
} }

View File

@ -1,4 +1,5 @@
using System.Net; using System.Globalization;
using System.Net;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using HtmlAgilityPack; using HtmlAgilityPack;
using Logging; using Logging;
@ -70,8 +71,8 @@ public class Manganato : Connector
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();
string? author = null, originalLanguage = null; string[] authors = Array.Empty<string>();
int? year = DateTime.Now.Year; string originalLanguage = "";
HtmlNode infoNode = document.DocumentNode.Descendants("div").First(d => d.HasClass("story-info-right")); HtmlNode infoNode = document.DocumentNode.Descendants("div").First(d => d.HasClass("story-info-right"));
@ -93,7 +94,7 @@ public class Manganato : Connector
altTitles.Add(i.ToString(), alts[i]); altTitles.Add(i.ToString(), alts[i]);
break; break;
case "authors": case "authors":
author = value; authors = value.Split('-');
break; break;
case "status": case "status":
status = value; status = value;
@ -118,9 +119,9 @@ public class Manganato : Connector
string yearString = document.DocumentNode.Descendants("li").Last(li => li.HasClass("a-h")).Descendants("span") string yearString = document.DocumentNode.Descendants("li").Last(li => li.HasClass("a-h")).Descendants("span")
.First(s => s.HasClass("chapter-time")).InnerText; .First(s => s.HasClass("chapter-time")).InnerText;
year = Convert.ToInt32(yearString.Split(',')[^1]) + 2000; int year = Convert.ToInt32(yearString.Split(',')[^1]) + 2000;
return new Publication(sortName, author, description, altTitles, tags.ToArray(), posterUrl, coverFileNameInCache, links, return new Publication(sortName, authors.ToList(), description, altTitles, tags.ToArray(), posterUrl, coverFileNameInCache, links,
year, originalLanguage, status, publicationId); year, originalLanguage, status, publicationId);
} }
@ -132,11 +133,18 @@ public class Manganato : Connector
downloadClient.MakeRequest(requestUrl, (byte)1); downloadClient.MakeRequest(requestUrl, (byte)1);
if (requestResult.statusCode != HttpStatusCode.OK) if (requestResult.statusCode != HttpStatusCode.OK)
return Array.Empty<Chapter>(); return Array.Empty<Chapter>();
return ParseChaptersFromHtml(requestResult.result); //Return Chapters ordered by Chapter-Number
NumberFormatInfo chapterNumberFormatInfo = new()
{
NumberDecimalSeparator = "."
};
List<Chapter> chapters = ParseChaptersFromHtml(requestResult.result);
logger?.WriteLine(this.GetType().ToString(), $"Done getting Chapters for {publication.internalId}");
return chapters.OrderBy(chapter => Convert.ToSingle(chapter.chapterNumber, chapterNumberFormatInfo)).ToArray();
} }
private Chapter[] ParseChaptersFromHtml(Stream html) private List<Chapter> ParseChaptersFromHtml(Stream html)
{ {
StreamReader reader = new (html); StreamReader reader = new (html);
string htmlString = reader.ReadToEnd(); string htmlString = reader.ReadToEnd();
@ -158,11 +166,13 @@ public class Manganato : Connector
ret.Add(new Chapter(chapterName, volumeNumber, chapterNumber, url)); ret.Add(new Chapter(chapterName, volumeNumber, chapterNumber, url));
} }
ret.Reverse(); ret.Reverse();
return ret.ToArray(); return ret;
} }
public override void DownloadChapter(Publication publication, Chapter chapter, DownloadChapterTask parentTask) public override void DownloadChapter(Publication publication, Chapter chapter, DownloadChapterTask parentTask, CancellationToken? cancellationToken = null)
{ {
if (cancellationToken?.IsCancellationRequested??false)
return;
logger?.WriteLine(this.GetType().ToString(), $"Downloading Chapter-Info {publication.sortName} {publication.internalId} {chapter.volumeNumber}-{chapter.chapterNumber}"); logger?.WriteLine(this.GetType().ToString(), $"Downloading Chapter-Info {publication.sortName} {publication.internalId} {chapter.volumeNumber}-{chapter.chapterNumber}");
string requestUrl = chapter.url; string requestUrl = chapter.url;
DownloadClient.RequestResult requestResult = DownloadClient.RequestResult requestResult =
@ -175,7 +185,7 @@ public class Manganato : Connector
string comicInfoPath = Path.GetTempFileName(); string comicInfoPath = Path.GetTempFileName();
File.WriteAllText(comicInfoPath, GetComicInfoXmlString(publication, chapter, logger)); File.WriteAllText(comicInfoPath, GetComicInfoXmlString(publication, chapter, logger));
DownloadChapterImages(imageUrls, GetArchiveFilePath(publication, chapter), (byte)1, parentTask, comicInfoPath, "https://chapmanganato.com/"); DownloadChapterImages(imageUrls, GetArchiveFilePath(publication, chapter), (byte)1, parentTask, comicInfoPath, "https://chapmanganato.com/", cancellationToken);
} }
private string[] ParseImageUrlsFromHtml(Stream html) private string[] ParseImageUrlsFromHtml(Stream html)

View File

@ -1,4 +1,5 @@
using System.Net; using System.Globalization;
using System.Net;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Xml.Linq; using System.Xml.Linq;
using HtmlAgilityPack; using HtmlAgilityPack;
@ -140,10 +141,9 @@ public class Mangasee : Connector
HtmlNode[] authorsNodes = attributes.Descendants("li") HtmlNode[] authorsNodes = attributes.Descendants("li")
.First(node => node.InnerText.Contains("author(s):", StringComparison.CurrentCultureIgnoreCase)) .First(node => node.InnerText.Contains("author(s):", StringComparison.CurrentCultureIgnoreCase))
.Descendants("a").ToArray(); .Descendants("a").ToArray();
string[] authors = new string[authorsNodes.Length]; List<string> authors = new();
for (int j = 0; j < authors.Length; j++) foreach(HtmlNode authorNode in authorsNodes)
authors[j] = authorsNodes[j].InnerText; authors.Add(authorNode.InnerText);
string author = string.Join(" - ", authors);
HtmlNode[] genreNodes = attributes.Descendants("li") HtmlNode[] genreNodes = attributes.Descendants("li")
.First(node => node.InnerText.Contains("genre(s):", StringComparison.CurrentCultureIgnoreCase)) .First(node => node.InnerText.Contains("genre(s):", StringComparison.CurrentCultureIgnoreCase))
@ -170,7 +170,7 @@ public class Mangasee : Connector
foreach(string at in a) foreach(string at in a)
altTitles.Add((i++).ToString(), at); altTitles.Add((i++).ToString(), at);
return new Publication(sortName, author, description, altTitles, tags.ToArray(), posterUrl, coverFileNameInCache, links, return new Publication(sortName, authors, description, altTitles, tags.ToArray(), posterUrl, coverFileNameInCache, links,
year, originalLanguage, status, publicationId); year, originalLanguage, status, publicationId);
} }
@ -200,20 +200,29 @@ public class Mangasee : Connector
ret.Add(new Chapter("", volumeNumber, chapterNumber, url)); ret.Add(new Chapter("", volumeNumber, chapterNumber, url));
} }
ret.Reverse(); //Return Chapters ordered by Chapter-Number
return ret.ToArray(); NumberFormatInfo chapterNumberFormatInfo = new()
{
NumberDecimalSeparator = "."
};
logger?.WriteLine(this.GetType().ToString(), $"Done getting Chapters for {publication.internalId}");
return ret.OrderBy(chapter => Convert.ToSingle(chapter.chapterNumber, chapterNumberFormatInfo)).ToArray();
} }
public override void DownloadChapter(Publication publication, Chapter chapter, DownloadChapterTask parentTask) public override void DownloadChapter(Publication publication, Chapter chapter, DownloadChapterTask parentTask, CancellationToken? cancellationToken = null)
{ {
while (this._browser is null) if (cancellationToken?.IsCancellationRequested??false)
return;
while (this._browser is null && !(cancellationToken?.IsCancellationRequested??false))
{ {
logger?.WriteLine(this.GetType().ToString(), "Waiting for headless browser to download..."); logger?.WriteLine(this.GetType().ToString(), "Waiting for headless browser to download...");
Thread.Sleep(1000); Thread.Sleep(1000);
} }
if (cancellationToken?.IsCancellationRequested??false)
return;
logger?.WriteLine(this.GetType().ToString(), $"Downloading Chapter-Info {publication.sortName} {publication.internalId} {chapter.volumeNumber}-{chapter.chapterNumber}"); logger?.WriteLine(this.GetType().ToString(), $"Downloading Chapter-Info {publication.sortName} {publication.internalId} {chapter.volumeNumber}-{chapter.chapterNumber}");
IPage page = _browser.NewPageAsync().Result; IPage page = _browser!.NewPageAsync().Result;
IResponse response = page.GoToAsync(chapter.url).Result; IResponse response = page.GoToAsync(chapter.url).Result;
if (response.Ok) if (response.Ok)
{ {
@ -229,7 +238,7 @@ public class Mangasee : Connector
string comicInfoPath = Path.GetTempFileName(); string comicInfoPath = Path.GetTempFileName();
File.WriteAllText(comicInfoPath, GetComicInfoXmlString(publication, chapter, logger)); File.WriteAllText(comicInfoPath, GetComicInfoXmlString(publication, chapter, logger));
DownloadChapterImages(urls.ToArray(), GetArchiveFilePath(publication, chapter), (byte)1, parentTask, comicInfoPath); DownloadChapterImages(urls.ToArray(), GetArchiveFilePath(publication, chapter), (byte)1, parentTask, comicInfoPath, cancellationToken:cancellationToken);
} }
} }
} }

View File

@ -49,9 +49,8 @@ public abstract class LibraryManager
Method = HttpMethod.Get, Method = HttpMethod.Get,
RequestUri = new Uri(url) RequestUri = new Uri(url)
}; };
logger?.WriteLine("LibraryManager", $"GET {url}");
HttpResponseMessage response = client.Send(requestMessage); HttpResponseMessage response = client.Send(requestMessage);
logger?.WriteLine("LibraryManager", $"{(int)response.StatusCode} {response.StatusCode}: {response.ReasonPhrase}"); logger?.WriteLine("LibraryManager", $"GET {url} -> {(int)response.StatusCode}: {response.ReasonPhrase}");
if(response.StatusCode is HttpStatusCode.Unauthorized && response.RequestMessage!.RequestUri!.AbsoluteUri != url) if(response.StatusCode is HttpStatusCode.Unauthorized && response.RequestMessage!.RequestUri!.AbsoluteUri != url)
return MakeRequest(response.RequestMessage!.RequestUri!.AbsoluteUri, authScheme, auth, logger); return MakeRequest(response.RequestMessage!.RequestUri!.AbsoluteUri, authScheme, auth, logger);
@ -76,9 +75,8 @@ public abstract class LibraryManager
Method = HttpMethod.Post, Method = HttpMethod.Post,
RequestUri = new Uri(url) RequestUri = new Uri(url)
}; };
logger?.WriteLine("LibraryManager", $"POST {url}");
HttpResponseMessage response = client.Send(requestMessage); HttpResponseMessage response = client.Send(requestMessage);
logger?.WriteLine("LibraryManager", $"{(int)response.StatusCode} {response.StatusCode}: {response.ReasonPhrase}"); logger?.WriteLine("LibraryManager", $"POST {url} -> {(int)response.StatusCode}: {response.ReasonPhrase}");
if(response.StatusCode is HttpStatusCode.Unauthorized && response.RequestMessage!.RequestUri!.AbsoluteUri != url) if(response.StatusCode is HttpStatusCode.Unauthorized && response.RequestMessage!.RequestUri!.AbsoluteUri != url)
return MakePost(response.RequestMessage!.RequestUri!.AbsoluteUri, authScheme, auth, logger); return MakePost(response.RequestMessage!.RequestUri!.AbsoluteUri, authScheme, auth, logger);

View File

@ -12,7 +12,7 @@ namespace Tranga;
public readonly struct Publication public readonly struct Publication
{ {
public string sortName { get; } public string sortName { get; }
public string? author { get; } public List<string> authors { get; }
public Dictionary<string,string> altTitles { get; } public Dictionary<string,string> altTitles { get; }
// ReSharper disable trice MemberCanBePrivate.Global, trust // ReSharper disable trice MemberCanBePrivate.Global, trust
public string? description { get; } public string? description { get; }
@ -29,10 +29,22 @@ public readonly struct Publication
private static readonly Regex LegalCharacters = new Regex(@"[A-Z]*[a-z]*[0-9]* *\.*-*,*'*\'*\)*\(*~*!*"); private static readonly Regex LegalCharacters = new Regex(@"[A-Z]*[a-z]*[0-9]* *\.*-*,*'*\'*\)*\(*~*!*");
public Publication(string sortName, string? author, string? description, Dictionary<string,string> altTitles, string[] tags, string? posterUrl, string? coverFileNameInCache, Dictionary<string,string>? links, int? year, string? originalLanguage, string status, string publicationId) [JsonConstructor] //Legacy
public Publication(string sortName, string? author, string? description, Dictionary<string, string> altTitles,
string[] tags, string? posterUrl, string? coverFileNameInCache, Dictionary<string, string>? links, int? year,
string? originalLanguage, string status, string publicationId)
{
List<string> pAuthors = new();
if(author is not null)
pAuthors.Add(author);
this = new Publication(sortName, pAuthors, description, altTitles, tags, posterUrl,
coverFileNameInCache, links, year, originalLanguage, status, publicationId);
}
public Publication(string sortName, List<string> authors, string? description, Dictionary<string,string> altTitles, string[] tags, string? posterUrl, string? coverFileNameInCache, Dictionary<string,string>? links, int? year, string? originalLanguage, string status, string publicationId)
{ {
this.sortName = sortName; this.sortName = sortName;
this.author = author; this.authors = authors;
this.description = description; this.description = description;
this.altTitles = altTitles; this.altTitles = altTitles;
this.tags = tags; this.tags = tags;

View File

@ -13,12 +13,14 @@ namespace Tranga;
public class TaskManager public class TaskManager
{ {
public Dictionary<Publication, List<Chapter>> chapterCollection = new(); public Dictionary<Publication, List<Chapter>> chapterCollection = new();
private HashSet<TrangaTask> _allTasks = new HashSet<TrangaTask>(); private HashSet<TrangaTask> _allTasks = new();
private bool _continueRunning = true; private bool _continueRunning = true;
private readonly Connector[] _connectors; private readonly Connector[] _connectors;
public TrangaSettings settings { get; } public TrangaSettings settings { get; }
private Logger? logger { get; } private Logger? logger { get; }
private readonly Dictionary<DownloadChapterTask, CancellationTokenSource> _runningDownloadChapterTasks = new();
/// <param name="downloadFolderPath">Local path to save data (Manga) to</param> /// <param name="downloadFolderPath">Local path to save data (Manga) to</param>
/// <param name="workingDirectory">Path to the working directory</param> /// <param name="workingDirectory">Path to the working directory</param>
/// <param name="imageCachePath">Path to the cover-image cache</param> /// <param name="imageCachePath">Path to the cover-image cache</param>
@ -87,8 +89,9 @@ public class TaskManager
while (_continueRunning) while (_continueRunning)
{ {
TrangaTask[] tmp = _allTasks.Where(taskQuery => TrangaTask[] tmp = _allTasks.Where(taskQuery =>
taskQuery.nextExecution < DateTime.Now && taskQuery.nextExecution < DateTime.Now &&
taskQuery.state is TrangaTask.ExecutionState.Waiting or TrangaTask.ExecutionState.Enqueued).ToArray(); taskQuery.state is TrangaTask.ExecutionState.Waiting or TrangaTask.ExecutionState.Enqueued)
.OrderBy(tmpTask => tmpTask.nextExecution).ToArray();
foreach (TrangaTask task in tmp) foreach (TrangaTask task in tmp)
{ {
task.state = TrangaTask.ExecutionState.Enqueued; task.state = TrangaTask.ExecutionState.Enqueued;
@ -117,6 +120,29 @@ public class TaskManager
break; break;
} }
} }
HashSet<DownloadChapterTask> toRemove = new();
foreach (KeyValuePair<DownloadChapterTask, CancellationTokenSource> removeTask in _runningDownloadChapterTasks)
{
if (removeTask.Key.GetType() == typeof(DownloadChapterTask) &&
DateTime.Now.Subtract(removeTask.Key.lastChange) > TimeSpan.FromMinutes(3))//3 Minutes since last update to task -> remove
{
logger?.WriteLine(this.GetType().ToString(), $"Disposing failed task {removeTask.Key}.");
removeTask.Key.parentTask?.DecrementProgress(removeTask.Key.progress);
removeTask.Value.Cancel();
toRemove.Add(removeTask.Key);
}
}
foreach (DownloadChapterTask taskToRemove in toRemove)
{
DeleteTask(taskToRemove);
DownloadChapterTask newTask = new DownloadChapterTask(taskToRemove.task, taskToRemove.connectorName,
taskToRemove.publication, taskToRemove.chapter, taskToRemove.language,
taskToRemove.parentTask);
AddTask(newTask);
taskToRemove.parentTask?.ReplaceFailedChildTask(taskToRemove, newTask);
}
if(allTasksWaitingLength != _allTasks.Count(task => task.state is TrangaTask.ExecutionState.Waiting)) if(allTasksWaitingLength != _allTasks.Count(task => task.state is TrangaTask.ExecutionState.Waiting))
ExportDataAndSettings(); ExportDataAndSettings();
allTasksWaitingLength = _allTasks.Count(task => task.state is TrangaTask.ExecutionState.Waiting); allTasksWaitingLength = _allTasks.Count(task => task.state is TrangaTask.ExecutionState.Waiting);
@ -131,10 +157,13 @@ public class TaskManager
public void ExecuteTaskNow(TrangaTask task) public void ExecuteTaskNow(TrangaTask task)
{ {
task.state = TrangaTask.ExecutionState.Running; task.state = TrangaTask.ExecutionState.Running;
CancellationTokenSource cToken = new ();
Task t = new(() => Task t = new(() =>
{ {
task.Execute(this, this.logger); task.Execute(this, this.logger, cToken.Token);
}); }, cToken.Token);
if(task.GetType() == typeof(DownloadChapterTask))
_runningDownloadChapterTasks.Add((DownloadChapterTask)task, cToken);
t.Start(); t.Start();
} }
@ -152,9 +181,9 @@ public class TaskManager
case TrangaTask.Task.DownloadNewChapters: case TrangaTask.Task.DownloadNewChapters:
IEnumerable<TrangaTask> matchingdnc = IEnumerable<TrangaTask> matchingdnc =
_allTasks.Where(mTask => mTask.GetType() == typeof(DownloadNewChaptersTask)); _allTasks.Where(mTask => mTask.GetType() == typeof(DownloadNewChaptersTask));
if (matchingdnc.All(mTask => if (!matchingdnc.Any(mTask =>
((DownloadNewChaptersTask)mTask).publication.internalId != ((DownloadNewChaptersTask)newTask).publication.publicationId && ((DownloadNewChaptersTask)mTask).publication.internalId == ((DownloadNewChaptersTask)newTask).publication.internalId &&
((DownloadNewChaptersTask)mTask).connectorName != ((DownloadNewChaptersTask)newTask).connectorName)) ((DownloadNewChaptersTask)mTask).connectorName == ((DownloadNewChaptersTask)newTask).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}");
@ -178,9 +207,11 @@ public class TaskManager
{ {
logger?.WriteLine(this.GetType().ToString(), $"Removing Task {removeTask}"); logger?.WriteLine(this.GetType().ToString(), $"Removing Task {removeTask}");
_allTasks.Remove(removeTask); _allTasks.Remove(removeTask);
if (removeTask.GetType() == typeof(DownloadChapterTask))
_runningDownloadChapterTasks.Remove((DownloadChapterTask)removeTask);
} }
public TrangaTask? AddTask(TrangaTask.Task taskType, string? connectorName, string? publicationId, public TrangaTask? AddTask(TrangaTask.Task taskType, string? connectorName, string? internalId,
TimeSpan reoccurrenceTime, string? language = "en") TimeSpan reoccurrenceTime, string? language = "en")
{ {
TrangaTask? newTask = null; TrangaTask? newTask = null;
@ -190,10 +221,16 @@ public class TaskManager
newTask = new UpdateLibrariesTask(taskType, reoccurrenceTime); newTask = new UpdateLibrariesTask(taskType, reoccurrenceTime);
break; break;
case TrangaTask.Task.DownloadNewChapters: case TrangaTask.Task.DownloadNewChapters:
if(connectorName is null || publicationId is null || language is null) if (connectorName is null)
logger?.WriteLine(this.GetType().ToString(), $"Values connectorName, publicationName and language can not be null."); logger?.WriteLine(this.GetType().ToString(), $"Value connectorName can not be null.");
if(internalId is null)
logger?.WriteLine(this.GetType().ToString(), $"Value internalId can not be null.");
if(language is null)
logger?.WriteLine(this.GetType().ToString(), $"Value language can not be null.");
if (connectorName is null || internalId is null || language is null)
return null;
GetConnector(connectorName); //Check if connectorName is valid GetConnector(connectorName); //Check if connectorName is valid
Publication publication = GetAllPublications().First(pub => pub.internalId == publicationId); Publication publication = GetAllPublications().First(pub => pub.internalId == internalId);
newTask = new DownloadNewChaptersTask(taskType, connectorName!, publication, reoccurrenceTime, language!); newTask = new DownloadNewChaptersTask(taskType, connectorName!, publication, reoccurrenceTime, language!);
break; break;
} }
@ -238,7 +275,7 @@ public class TaskManager
ExportDataAndSettings(); ExportDataAndSettings();
} }
public IEnumerable<TrangaTask> GetTasksMatching(TrangaTask.Task taskType, string? connectorName = null, string? searchString = null, string? internalId = null) public IEnumerable<TrangaTask> GetTasksMatching(TrangaTask.Task taskType, string? connectorName = null, string? searchString = null, string? internalId = null, string? chapterSortNumber = null)
{ {
switch (taskType) switch (taskType)
{ {
@ -277,11 +314,12 @@ public class TaskManager
((DownloadChapterTask)mTask).connectorName == connectorName && ((DownloadChapterTask)mTask).connectorName == connectorName &&
((DownloadChapterTask)mTask).ToString().Contains(searchString, StringComparison.InvariantCultureIgnoreCase)); ((DownloadChapterTask)mTask).ToString().Contains(searchString, StringComparison.InvariantCultureIgnoreCase));
} }
else if (internalId is not null) else if (internalId is not null && chapterSortNumber is not null)
{ {
return matchingdc.Where(mTask => return matchingdc.Where(mTask =>
((DownloadChapterTask)mTask).connectorName == connectorName && ((DownloadChapterTask)mTask).connectorName == connectorName &&
((DownloadChapterTask)mTask).publication.publicationId == internalId); ((DownloadChapterTask)mTask).publication.publicationId == internalId &&
((DownloadChapterTask)mTask).chapter.sortNumber == chapterSortNumber);
} }
else else
return _allTasks.Where(tTask => return _allTasks.Where(tTask =>
@ -390,6 +428,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(task => task.GetType() == typeof(DownloadChapterTask)))
{
DownloadChapterTask dcTask = (DownloadChapterTask)task;
IEnumerable<TrangaTask> dncTasks = this._allTasks.Where(pTask => pTask.GetType() == typeof(DownloadNewChaptersTask));
DownloadNewChaptersTask? parentTask = (DownloadNewChaptersTask?)dncTasks.FirstOrDefault(pTask => pTask.taskId.Equals(dcTask.parentTaskId));
dcTask.parentTask = parentTask;
parentTask?.AddChildTask(dcTask);
}
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}");

View File

@ -1,4 +1,5 @@
using System.Text.Json.Serialization; using System.Globalization;
using System.Text.Json.Serialization;
using Logging; using Logging;
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
@ -20,10 +21,11 @@ public abstract class TrangaTask
public TimeSpan reoccurrence { get; } public TimeSpan reoccurrence { get; }
public DateTime lastExecuted { get; set; } public DateTime lastExecuted { get; set; }
public Task task { get; } public Task task { get; }
public string taskId { get; set; }
[Newtonsoft.Json.JsonIgnore]public ExecutionState state { get; set; } [Newtonsoft.Json.JsonIgnore]public ExecutionState state { get; set; }
[Newtonsoft.Json.JsonIgnore]public float progress { get; protected set; } [Newtonsoft.Json.JsonIgnore]public double progress { get; protected set; }
[Newtonsoft.Json.JsonIgnore]public DateTime nextExecution => lastExecuted.Add(reoccurrence); [Newtonsoft.Json.JsonIgnore]public DateTime nextExecution => lastExecuted.Add(reoccurrence);
[Newtonsoft.Json.JsonIgnore]public DateTime executionStarted { get; protected set; } [Newtonsoft.Json.JsonIgnore]public DateTime executionStarted { get; private set; }
[Newtonsoft.Json.JsonIgnore] [Newtonsoft.Json.JsonIgnore]
public DateTime executionApproximatelyFinished => this.progress != 0 public DateTime executionApproximatelyFinished => this.progress != 0
@ -32,6 +34,8 @@ public abstract class TrangaTask
[Newtonsoft.Json.JsonIgnore] [Newtonsoft.Json.JsonIgnore]
public TimeSpan executionApproximatelyRemaining => this.executionApproximatelyFinished.Subtract(DateTime.Now); public TimeSpan executionApproximatelyRemaining => this.executionApproximatelyFinished.Subtract(DateTime.Now);
[Newtonsoft.Json.JsonIgnore]public DateTime lastChange { get; private set; }
public enum ExecutionState public enum ExecutionState
{ {
@ -47,11 +51,21 @@ public abstract class TrangaTask
this.task = task; this.task = task;
this.progress = 0f; this.progress = 0f;
this.executionStarted = DateTime.Now; this.executionStarted = DateTime.Now;
this.lastChange = DateTime.MaxValue;
this.taskId = Convert.ToBase64String(System.Text.Encoding.ASCII.GetBytes(this.executionStarted.ToString(CultureInfo.InvariantCulture)));
} }
public float IncrementProgress(float amount) public double IncrementProgress(double amount)
{ {
this.progress += amount; this.progress += amount;
this.lastChange = DateTime.Now;
return this.progress;
}
public double DecrementProgress(double amount)
{
this.progress -= amount;
this.lastChange = DateTime.Now;
return this.progress; return this.progress;
} }
@ -60,19 +74,22 @@ public abstract class TrangaTask
/// </summary> /// </summary>
/// <param name="taskManager"></param> /// <param name="taskManager"></param>
/// <param name="logger"></param> /// <param name="logger"></param>
protected abstract void ExecuteTask(TaskManager taskManager, Logger? logger); /// <param name="cancellationToken"></param>
protected abstract void ExecuteTask(TaskManager taskManager, Logger? logger, CancellationToken? cancellationToken = null);
/// <summary> /// <summary>
/// Execute the task /// Execute the task
/// </summary> /// </summary>
/// <param name="taskManager">Should be the parent taskManager</param> /// <param name="taskManager">Should be the parent taskManager</param>
/// <param name="logger"></param> /// <param name="logger"></param>
public void Execute(TaskManager taskManager, Logger? logger) /// <param name="cancellationToken"></param>
public void Execute(TaskManager taskManager, Logger? logger, CancellationToken? cancellationToken = null)
{ {
logger?.WriteLine(this.GetType().ToString(), $"Executing Task {this}"); logger?.WriteLine(this.GetType().ToString(), $"Executing Task {this}");
this.state = ExecutionState.Running; this.state = ExecutionState.Running;
this.executionStarted = DateTime.Now; this.executionStarted = DateTime.Now;
ExecuteTask(taskManager, logger); this.lastChange = DateTime.Now;
ExecuteTask(taskManager, logger, cancellationToken);
this.lastExecuted = DateTime.Now; this.lastExecuted = DateTime.Now;
this.state = ExecutionState.Waiting; this.state = ExecutionState.Waiting;
logger?.WriteLine(this.GetType().ToString(), $"Finished Executing Task {this}"); logger?.WriteLine(this.GetType().ToString(), $"Finished Executing Task {this}");

View File

@ -9,7 +9,9 @@ public class DownloadChapterTask : TrangaTask
public Publication publication { get; } public Publication publication { get; }
public string language { get; } public string language { get; }
public Chapter chapter { get; } public Chapter chapter { get; }
[JsonIgnore]private DownloadNewChaptersTask? parentTask { get; init; } [JsonIgnore]public DownloadNewChaptersTask? parentTask { get; set; }
public string? parentTaskId { get; set; }
public DownloadChapterTask(Task task, string connectorName, Publication publication, Chapter chapter, string language = "en", DownloadNewChaptersTask? parentTask = null) : base(task, TimeSpan.Zero) public DownloadChapterTask(Task task, string connectorName, Publication publication, Chapter chapter, string language = "en", DownloadNewChaptersTask? parentTask = null) : base(task, TimeSpan.Zero)
{ {
@ -18,22 +20,21 @@ public class DownloadChapterTask : TrangaTask
this.publication = publication; this.publication = publication;
this.language = language; this.language = language;
this.parentTask = parentTask; this.parentTask = parentTask;
this.parentTaskId = parentTask?.taskId;
} }
protected override void ExecuteTask(TaskManager taskManager, Logger? logger) protected override void ExecuteTask(TaskManager taskManager, Logger? logger, CancellationToken? cancellationToken = null)
{ {
Publication pub = (Publication)this.publication!; if (cancellationToken?.IsCancellationRequested??false)
return;
if(this.parentTask is not null)
this.parentTask.state = ExecutionState.Running;
Connector connector = taskManager.GetConnector(this.connectorName); Connector connector = taskManager.GetConnector(this.connectorName);
connector.DownloadChapter(pub, this.chapter, this); connector.DownloadChapter(this.publication, this.chapter, this, cancellationToken);
if(this.parentTask is not null)
this.parentTask.state = ExecutionState.Waiting;
taskManager.DeleteTask(this); taskManager.DeleteTask(this);
} }
public new float IncrementProgress(float amount)
{
this.progress += amount;
parentTask?.IncrementProgress(amount);
return this.progress;
}
public override string ToString() public override string ToString()
{ {

View File

@ -8,38 +8,51 @@ public class DownloadNewChaptersTask : TrangaTask
public string connectorName { get; } public string connectorName { get; }
public Publication publication { get; } public Publication publication { get; }
public string language { get; } public string language { get; }
[JsonIgnore]private int childTaskAmount { get; set; } [JsonIgnore]private HashSet<DownloadChapterTask> childTasks { get; }
[JsonIgnore]public new double progress => childTasks.Count > 0 ? childTasks.Sum(childTask => childTask.progress) / childTasks.Count : 0;
public DownloadNewChaptersTask(Task task, string connectorName, Publication publication, TimeSpan reoccurrence, string language = "en") : base(task, reoccurrence) public DownloadNewChaptersTask(Task task, string connectorName, Publication publication, TimeSpan reoccurrence, string language = "en") : base(task, reoccurrence)
{ {
this.connectorName = connectorName; this.connectorName = connectorName;
this.publication = publication; this.publication = publication;
this.language = language; this.language = language;
childTaskAmount = 0; this.childTasks = new();
}
public new float IncrementProgress(float amount)
{
this.progress += amount / this.childTaskAmount;
return this.progress;
} }
protected override void ExecuteTask(TaskManager taskManager, Logger? logger) protected override void ExecuteTask(TaskManager taskManager, Logger? logger, CancellationToken? cancellationToken = null)
{ {
if (cancellationToken?.IsCancellationRequested??false)
return;
Publication pub = publication!; Publication pub = publication!;
Connector connector = taskManager.GetConnector(this.connectorName); Connector connector = taskManager.GetConnector(this.connectorName);
//Check if Publication already has a Folder //Check if Publication already has a Folder
pub.CreatePublicationFolder(taskManager.settings.downloadLocation); pub.CreatePublicationFolder(taskManager.settings.downloadLocation);
List<Chapter> newChapters = GetNewChaptersList(connector, pub, language!, ref taskManager.chapterCollection); List<Chapter> newChapters = GetNewChaptersList(connector, pub, language!, ref taskManager.chapterCollection);
this.childTaskAmount = newChapters.Count;
connector.CopyCoverFromCacheToDownloadLocation(pub, taskManager.settings); connector.CopyCoverFromCacheToDownloadLocation(pub, taskManager.settings);
pub.SaveSeriesInfoJson(connector.downloadLocation); pub.SaveSeriesInfoJson(connector.downloadLocation);
foreach (Chapter newChapter in newChapters) foreach (Chapter newChapter in newChapters)
taskManager.AddTask(new DownloadChapterTask(Task.DownloadChapter, this.connectorName!, pub, newChapter, this.language, this)); {
DownloadChapterTask newTask = new (Task.DownloadChapter, this.connectorName!, pub, newChapter, this.language, this);
taskManager.AddTask(newTask);
this.childTasks.Add(newTask);
}
}
public void ReplaceFailedChildTask(DownloadChapterTask failed, DownloadChapterTask newTask)
{
if (!this.childTasks.Contains(failed))
throw new ArgumentException($"Task {failed} is not childTask of {this}");
this.childTasks.Remove(failed);
this.childTasks.Add(newTask);
}
public void AddChildTask(DownloadChapterTask childTask)
{
this.childTasks.Add(childTask);
} }
/// <summary> /// <summary>

View File

@ -8,8 +8,10 @@ public class UpdateLibrariesTask : TrangaTask
{ {
} }
protected override void ExecuteTask(TaskManager taskManager, Logger? logger) protected override void ExecuteTask(TaskManager taskManager, Logger? logger, CancellationToken? cancellationToken = null)
{ {
if (cancellationToken?.IsCancellationRequested??false)
return;
foreach(LibraryManager lm in taskManager.settings.libraryManagers) foreach(LibraryManager lm in taskManager.settings.libraryManagers)
lm.UpdateLibrary(); lm.UpdateLibrary();
this.progress = 1f; this.progress = 1f;

View File

@ -43,25 +43,37 @@ function DeleteData(uri){
} }
async function GetAvailableControllers(){ async function GetAvailableControllers(){
var uri = apiUri + "/Tranga/GetAvailableControllers"; var uri = apiUri + "/Controllers/Get";
let json = await GetData(uri); let json = await GetData(uri);
return json; return json;
} }
async function GetPublication(connectorName, title){ async function GetPublicationFromConnector(connectorName, title){
var uri = apiUri + `/Tranga/GetPublicationsFromConnector?connectorName=${connectorName}&title=${title}`; var uri = apiUri + `/Publications/GetFromConnector?connectorName=${connectorName}&title=${title}`;
let json = await GetData(uri); let json = await GetData(uri);
return json; return json;
} }
async function GetKnownPublications(){ async function GetKnownPublications(){
var uri = apiUri + "/Tranga/GetKnownPublications"; var uri = apiUri + "/Publications/GetKnown";
let json = await GetData(uri);
return json;
}
async function GetPublication(internalId){
var uri = apiUri + `/Publications/GetKnown?internalId=${internalId}`;
let json = await GetData(uri);
return json;
}
async function GetChapters(internalId, connectorName, language){
var uri = apiUri + `/Publications/GetChapters?internalId=${internalId}&connectorName=${connectorName}&language=${language}`;
let json = await GetData(uri); let json = await GetData(uri);
return json; return json;
} }
async function GetTaskTypes(){ async function GetTaskTypes(){
var uri = apiUri + "/Tasks/GetTaskTypes"; var uri = apiUri + "/Tasks/GetTypes";
let json = await GetData(uri); let json = await GetData(uri);
return json; return json;
} }
@ -89,8 +101,18 @@ async function GetKomgaTask(){
return json; return json;
} }
function CreateTask(taskType, reoccurrence, connectorName, publicationId, language){ function CreateMonitorTask(connectorName, internalId, reoccurrence, language){
var uri = apiUri + `/Tasks/Create?taskType=${taskType}&connectorName=${connectorName}&publicationId=${publicationId}&reoccurrenceTime=${reoccurrence}&language=${language}`; var uri = apiUri + `/Tasks/CreateMonitorTask?connectorName=${connectorName}&internalId=${internalId}&reoccurrenceTime=${reoccurrence}&language=${language}`;
PostData(uri);
}
function CreateUpdateLibraryTask(reoccurrence){
var uri = apiUri + `/Tasks/CreateUpdateLibraryTask?reoccurrenceTime=${reoccurrence}`;
PostData(uri);
}
function CreateDownloadChaptersTask(connectorName, internalId, chapters, language){
var uri = apiUri + `/Tasks/CreateDownloadChaptersTask?connectorName=${connectorName}&internalId=${internalId}&chapters=${chapters}&language=${language}`;
PostData(uri); PostData(uri);
} }

View File

@ -25,36 +25,64 @@
<p>+</p> <p>+</p>
</div> </div>
<publication> <publication>
<img src="media/cover.jpg"> <img alt="cover" src="media/cover.jpg">
<publication-information> <publication-information>
<connector-name class="pill">MangaDex</connector-name> <connector-name class="pill">MangaDex</connector-name>
<publication-name>Tensei Pandemic</publication-name> <publication-name>Tensei Pandemic</publication-name>
</publication-information> </publication-information>
</publication> </publication>
</content> </content>
<popup id="addTaskPopup"> <popup id="selectPublicationPopup">
<blur-background id="blurBackgroundTaskPopup"></blur-background> <blur-background id="blurBackgroundTaskPopup"></blur-background>
<addtask-window> <popup-window>
<window-titlebar> <popup-title>Select Publication</popup-title>
<p>Add Task</p> <popup-content>
<img id="closePopupImg" src="media/close-x.svg" alt="Close"> <div>
</window-titlebar> <label for="connectors">Connector</label>
<window-content> <select id="connectors">
<addtask-settings> <option value=""></option>
<addtask-setting><label for="selectReccurrence">Recurrence</label><input id="selectReccurrence" type="time" value="01:00:00" step="3600"></addtask-setting> </select>
<addtask-setting><label for="connectors">Connector</label> </div>
<select id="connectors"> <div>
<option value=""></option> <label for="searchPublicationQuery">Search Title</label><input id="searchPublicationQuery" type="text"></addtask-setting>
</select> </div>
</addtask-setting> <input type="submit" value="Search" style="font-weight: bolder" onclick="NewSearch();">
<addtask-setting><label for="searchPublicationQuery">Search Title</label><input id="searchPublicationQuery" type="text"></addtask-setting> </popup-content>
<input type="submit" value="Search" onclick="NewSearch();"> <div id="taskSelectOutput"></div>
</addtask-settings> </popup-window>
<div id="taskSelectOutput"></div>
</window-content>
</addtask-window>
</popup> </popup>
<popup id="createMonitorTaskPopup">
<blur-background id="blurBackgroundCreateMonitorTaskPopup"></blur-background>
<popup-window>
<popup-title>Create Task: Monitor Publication</popup-title>
<popup-content>
<div>
<span>Run every</span>
<label for="hours"></label><input id="hours" type="number" value="3" min="0" max="23"><span>hours</span>
<label for="minutes"></label><input id="minutes" type="number" value="0" min="0" max="59"><span>minutes</span>
<input type="submit" value="Create" onclick="AddMonitorTask()">
</div>
</popup-content>
</popup-window>
</popup>
<popup id="createDownloadChaptersTask">
<blur-background id="blurBackgroundCreateDownloadChaptersTask"></blur-background>
<popup-window>
<popup-title>Create Task: Download Chapter(s)</popup-title>
<popup-content>
<div>
<label for="selectedChapters">Chapters:</label><input id="selectedChapters" placeholder="Select"><input type="submit" value="Select" onclick="DownloadChapterTaskClick()">
</div>
<div id="chapterOutput">
</div>
</popup-content>
</popup-window>
</popup>
<popup id="publicationViewerPopup"> <popup id="publicationViewerPopup">
<blur-background id="blurBackgroundPublicationPopup"></blur-background> <blur-background id="blurBackgroundPublicationPopup"></blur-background>
<publication-viewer> <publication-viewer>
@ -71,62 +99,68 @@
<publication-interactions> <publication-interactions>
<publication-starttask>Start Task ▶️</publication-starttask> <publication-starttask>Start Task ▶️</publication-starttask>
<publication-delete>Delete Task ❌</publication-delete> <publication-delete>Delete Task ❌</publication-delete>
<publication-add>Add Task </publication-add> <publication-add id="createMonitorTaskButton">Monitor </publication-add>
<publication-add id="createDownloadChapterTaskButton">Download Chapter </publication-add>
</publication-interactions> </publication-interactions>
</publication-information> </publication-information>
</publication-viewer> </publication-viewer>
</popup> </popup>
<popup id="settingsPopup"> <popup id="settingsPopup">
<blur-background id="blurBackgroundSettingsPopup"></blur-background> <blur-background id="blurBackgroundSettingsPopup"></blur-background>
<settings> <popup-window>
<span style="font-weight: bold; text-align: center; font-size: 16pt;">Settings</span> <popup-title>Settings</popup-title>
<div> <popup-content>
<p class="title">Download Location:</p> <div>
<span id="downloadLocation"></span> <p class="title">Download Location:</p>
</div> <span id="downloadLocation"></span>
<div> </div>
<p class="title">API-URI</p> <div>
<label for="settingApiUri"></label><input placeholder="https://" type="text" id="settingApiUri"> <p class="title">API-URI</p>
</div> <label for="settingApiUri"></label><input placeholder="https://" type="text" id="settingApiUri">
<komga-settings> </div>
<span class="title">Komga</span> <div>
<div>Configured: <span id="komgaConfigured">✅❌</span></div> <span class="title">Komga</span>
<label for="komgaUrl"></label><input placeholder="URL" id="komgaUrl" type="text"> <div>Configured: <span id="komgaConfigured">✅❌</span></div>
<label for="komgaUsername"></label><input placeholder="Username" id="komgaUsername" type="text"> <label for="komgaUrl"></label><input placeholder="URL" id="komgaUrl" type="text">
<label for="komgaPassword"></label><input placeholder="Password" id="komgaPassword" type="password"> <label for="komgaUsername"></label><input placeholder="Username" id="komgaUsername" type="text">
</komga-settings> <label for="komgaPassword"></label><input placeholder="Password" id="komgaPassword" type="password">
<kavita-settings> </div>
<span class="title">Kavita</span> <div>
<div>Configured: <span id="kavitaConfigured">✅❌</span></div> <span class="title">Kavita</span>
<label for="kavitaUrl"></label><input placeholder="URL" id="kavitaUrl" type="text"> <div>Configured: <span id="kavitaConfigured">✅❌</span></div>
<label for="kavitaUsername"></label><input placeholder="Username" id="kavitaUsername" type="text"> <label for="kavitaUrl"></label><input placeholder="URL" id="kavitaUrl" type="text">
<label for="kavitaPassword"></label><input placeholder="Password" id="kavitaPassword" type="password"> <label for="kavitaUsername"></label><input placeholder="Username" id="kavitaUsername" type="text">
</kavita-settings> <label for="kavitaPassword"></label><input placeholder="Password" id="kavitaPassword" type="password">
<div> </div>
<label for="libraryUpdateTime" style="margin-right: 5px;">Update Time</label><input id="libraryUpdateTime" type="time" value="00:01:00" step="10"> <div>
<input type="submit" value="Update" onclick="UpdateLibrarySettings()"> <label for="libraryUpdateTime" style="margin-right: 5px;">Update Time</label><input id="libraryUpdateTime" type="time" value="00:01:00" step="10">
</div> <input type="submit" value="Update" onclick="UpdateLibrarySettings()">
</settings> </div>
</popup-content>
</popup-window>
</popup>
<popup id="downloadTasksPopup">
<blur-background id="blurBackgroundTasksQueuePopup"></blur-background>
<popup-window>
<popup-title>Task Progress</popup-title>
<popup-content>
</popup-content>
</popup-window>
</popup> </popup>
</viewport> </viewport>
<footer> <footer>
<div> <div onclick="ShowTasksQueue();">
<img src="media/running.svg" alt="running"><div id="tasksRunningTag">0</div> <img src="media/running.svg" alt="running"><div id="tasksRunningTag">0</div>
</div> </div>
<div> <div onclick="ShowTasksQueue();">
<img src="media/queue.svg" alt="queue"><div id="tasksQueuedTag">0</div> <img src="media/queue.svg" alt="queue"><div id="tasksQueuedTag">0</div>
</div> </div>
<div>
<img src="media/tasks.svg" alt="queue"><div id="totalTasksTag">0</div>
</div>
<p id="madeWith">Made with Blåhaj 🦈</p> <p id="madeWith">Made with Blåhaj 🦈</p>
</footer> </footer>
</wrapper> </wrapper>
<footer-tag-popup>
<footer-tag-content>
<footer-tag-task-name>Test</footer-tag-task-name>
</footer-tag-content>
</footer-tag-popup>
<script src="apiConnector.js"></script> <script src="apiConnector.js"></script>
<script src="interaction.js"></script> <script src="interaction.js"></script>

View File

@ -10,7 +10,13 @@ const settingsPopup = document.querySelector("#settingsPopup");
const settingsCog = document.querySelector("#settingscog"); const settingsCog = document.querySelector("#settingscog");
const selectRecurrence = document.querySelector("#selectReccurrence"); const selectRecurrence = document.querySelector("#selectReccurrence");
const tasksContent = document.querySelector("content"); const tasksContent = document.querySelector("content");
const addTaskPopup = document.querySelector("#addTaskPopup"); const selectPublicationPopup = document.querySelector("#selectPublicationPopup");
const createMonitorTaskButton = document.querySelector("#createMonitorTaskButton");
const createDownloadChapterTaskButton = document.querySelector("#createDownloadChapterTaskButton");
const createMonitorTaskPopup = document.querySelector("#createMonitorTaskPopup");
const createDownloadChaptersTask = document.querySelector("#createDownloadChaptersTask");
const chapterOutput = document.querySelector("#chapterOutput");
const selectedChapters = document.querySelector("#selectedChapters");
const publicationViewerPopup = document.querySelector("#publicationViewerPopup"); const publicationViewerPopup = document.querySelector("#publicationViewerPopup");
const publicationViewerWindow = document.querySelector("publication-viewer"); const publicationViewerWindow = document.querySelector("publication-viewer");
const publicationViewerDescription = document.querySelector("#publicationViewerDescription"); const publicationViewerDescription = document.querySelector("#publicationViewerDescription");
@ -19,9 +25,7 @@ const publicationViewerTags = document.querySelector("#publicationViewerTags");
const publicationViewerAuthor = document.querySelector("#publicationViewerAuthor"); const publicationViewerAuthor = document.querySelector("#publicationViewerAuthor");
const pubviewcover = document.querySelector("#pubviewcover"); const pubviewcover = document.querySelector("#pubviewcover");
const publicationDelete = document.querySelector("publication-delete"); const publicationDelete = document.querySelector("publication-delete");
const publicationAdd = document.querySelector("publication-add");
const publicationTaskStart = document.querySelector("publication-starttask"); const publicationTaskStart = document.querySelector("publication-starttask");
const closetaskpopup = document.querySelector("#closePopupImg");
const settingDownloadLocation = document.querySelector("#downloadLocation"); const settingDownloadLocation = document.querySelector("#downloadLocation");
const settingKomgaUrl = document.querySelector("#komgaUrl"); const settingKomgaUrl = document.querySelector("#komgaUrl");
const settingKomgaUser = document.querySelector("#komgaUsername"); const settingKomgaUser = document.querySelector("#komgaUsername");
@ -35,18 +39,31 @@ const settingKavitaConfigured = document.querySelector("#kavitaConfigured");
const settingApiUri = document.querySelector("#settingApiUri"); const settingApiUri = document.querySelector("#settingApiUri");
const tagTasksRunning = document.querySelector("#tasksRunningTag"); const tagTasksRunning = document.querySelector("#tasksRunningTag");
const tagTasksQueued = document.querySelector("#tasksQueuedTag"); const tagTasksQueued = document.querySelector("#tasksQueuedTag");
const tagTasksTotal = document.querySelector("#totalTasksTag"); const downloadTasksPopup = document.querySelector("#downloadTasksPopup");
const tagTaskPopup = document.querySelector("footer-tag-popup"); const downloadTasksOutput = downloadTasksPopup.querySelector("popup-content");
const tagTasksPopupContent = document.querySelector("footer-tag-content");
searchbox.addEventListener("keyup", (event) => FilterResults()); searchbox.addEventListener("keyup", (event) => FilterResults());
settingsCog.addEventListener("click", () => OpenSettings()); settingsCog.addEventListener("click", () => OpenSettings());
document.querySelector("#blurBackgroundSettingsPopup").addEventListener("click", () => HideSettings()); document.querySelector("#blurBackgroundSettingsPopup").addEventListener("click", () => settingsPopup.style.display = "none");
closetaskpopup.addEventListener("click", () => HideAddTaskPopup()); document.querySelector("#blurBackgroundTaskPopup").addEventListener("click", () => selectPublicationPopup.style.display = "none");
document.querySelector("#blurBackgroundTaskPopup").addEventListener("click", () => HideAddTaskPopup());
document.querySelector("#blurBackgroundPublicationPopup").addEventListener("click", () => HidePublicationPopup()); document.querySelector("#blurBackgroundPublicationPopup").addEventListener("click", () => HidePublicationPopup());
document.querySelector("#blurBackgroundCreateMonitorTaskPopup").addEventListener("click", () => createMonitorTaskPopup.style.display = "none");
document.querySelector("#blurBackgroundCreateDownloadChaptersTask").addEventListener("click", () => createDownloadChaptersTask.style.display = "none");
document.querySelector("#blurBackgroundTasksQueuePopup").addEventListener("click", () => downloadTasksPopup.style.display = "none");
selectedChapters.addEventListener("keypress", (event) => {
if(event.key === "Enter"){
DownloadChapterTaskClick();
}
})
publicationDelete.addEventListener("click", () => DeleteTaskClick()); publicationDelete.addEventListener("click", () => DeleteTaskClick());
publicationAdd.addEventListener("click", () => AddTaskClick()); createMonitorTaskButton.addEventListener("click", () => {
HidePublicationPopup();
createMonitorTaskPopup.style.display = "block";
});
createDownloadChapterTaskButton.addEventListener("click", () => {
HidePublicationPopup();
OpenDownloadChapterTaskPopup();
});
publicationTaskStart.addEventListener("click", () => StartTaskClick()); publicationTaskStart.addEventListener("click", () => StartTaskClick());
settingApiUri.addEventListener("keypress", (event) => { settingApiUri.addEventListener("keypress", (event) => {
if(event.key === "Enter"){ if(event.key === "Enter"){
@ -60,12 +77,7 @@ searchPublicationQuery.addEventListener("keypress", (event) => {
NewSearch(); NewSearch();
} }
}); });
tagTasksRunning.addEventListener("mouseover", (event) => ShowRunningTasks(event));
tagTasksRunning.addEventListener("mouseout", () => CloseTasksPopup());
tagTasksQueued.addEventListener("mouseover", (event) => ShowQueuedTasks(event));
tagTasksQueued.addEventListener("mouseout", () => CloseTasksPopup());
tagTasksTotal.addEventListener("mouseover", (event) => ShowAllTasks(event));
tagTasksTotal.addEventListener("mouseout", () => CloseTasksPopup());
let availableConnectors; let availableConnectors;
GetAvailableControllers() GetAvailableControllers()
@ -82,18 +94,14 @@ GetAvailableControllers()
function NewSearch(){ function NewSearch(){
//Disable inputs //Disable inputs
selectRecurrence.disabled = true;
connectorSelect.disabled = true; connectorSelect.disabled = true;
searchPublicationQuery.disabled = true; searchPublicationQuery.disabled = true;
//Waitcursor //Waitcursor
document.body.style.cursor = "wait"; document.body.style.cursor = "wait";
selectRecurrence.style.cursor = "wait";
connectorSelect.style.cursor = "wait";
searchPublicationQuery.style.cursor = "wait";
//Empty previous results //Empty previous results
selectPublication.replaceChildren(); selectPublication.replaceChildren();
GetPublication(connectorSelect.value, searchPublicationQuery.value) GetPublicationFromConnector(connectorSelect.value, searchPublicationQuery.value)
.then(json => .then(json =>
json.forEach(publication => { json.forEach(publication => {
var option = CreatePublication(publication, connectorSelect.value); var option = CreatePublication(publication, connectorSelect.value);
@ -105,14 +113,10 @@ function NewSearch(){
)) ))
.then(() => { .then(() => {
//Re-enable inputs //Re-enable inputs
selectRecurrence.disabled = false;
connectorSelect.disabled = false; connectorSelect.disabled = false;
searchPublicationQuery.disabled = false; searchPublicationQuery.disabled = false;
//Cursor //Cursor
document.body.style.cursor = "initial"; document.body.style.cursor = "initial";
selectRecurrence.style.cursor = "initial";
connectorSelect.style.cursor = "initial";
searchPublicationQuery.style.cursor = "initial";
}); });
} }
@ -137,18 +141,59 @@ function CreatePublication(publication, connector){
return publicationElement; return publicationElement;
} }
function AddMonitorTask(){
var hours = document.querySelector("#hours").value;
var minutes = document.querySelector("#minutes").value;
CreateMonitorTask(connectorSelect.value, toEditId, `${hours}:${minutes}:00`, "en");
HidePublicationPopup();
createMonitorTaskPopup.style.display = "none";
selectPublicationPopup.style.display = "none";
}
function OpenDownloadChapterTaskPopup(){
selectedChapters.value = "";
chapterOutput.replaceChildren();
createDownloadChaptersTask.style.display = "block";
GetChapters(toEditId, connectorSelect.value, "en").then((json) => {
var i = 0;
json.forEach(chapter => {
var chapterDom = document.createElement("div");
var indexDom = document.createElement("span");
indexDom.className = "index";
indexDom.innerText = i++;
chapterDom.appendChild(indexDom);
var volDom = document.createElement("span");
volDom.className = "vol";
volDom.innerText = chapter.volumeNumber;
chapterDom.appendChild(volDom);
var chDom = document.createElement("span");
chDom.className = "ch";
chDom.innerText = chapter.chapterNumber;
chapterDom.appendChild(chDom);
var titleDom = document.createElement("span");
titleDom.innerText = chapter.name;
chapterDom.appendChild(titleDom);
chapterOutput.appendChild(chapterDom);
});
});
}
function DownloadChapterTaskClick(){
CreateDownloadChaptersTask(connectorSelect.value, toEditId, selectedChapters.value, "en");
HidePublicationPopup();
createDownloadChaptersTask.style.display = "none";
selectPublicationPopup.style.display = "none";
}
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("DownloadNewChapters", taskToDelete.connectorName, toEditId);
HidePublicationPopup(); HidePublicationPopup();
} }
function AddTaskClick(){
CreateTask("DownloadNewChapters", selectRecurrence.value, connectorSelect.value, toEditId, "en")
HideAddTaskPopup();
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("DownloadNewChapters", toEditTask.connectorName, toEditId);
@ -188,18 +233,20 @@ function ShowPublicationViewerWindow(publicationId, event, add){
publicationViewerName.innerText = publication.sortName; publicationViewerName.innerText = publication.sortName;
publicationViewerTags.innerText = publication.tags.join(", "); publicationViewerTags.innerText = publication.tags.join(", ");
publicationViewerDescription.innerText = publication.description; publicationViewerDescription.innerText = publication.description;
publicationViewerAuthor.innerText = publication.author; publicationViewerAuthor.innerText = publication.authors.join(',');
pubviewcover.src = `imageCache/${publication.coverFileNameInCache}`; pubviewcover.src = `imageCache/${publication.coverFileNameInCache}`;
toEditId = publicationId; toEditId = publicationId;
//Check what action should be listed //Check what action should be listed
if(add){ if(add){
publicationAdd.style.display = "initial"; createMonitorTaskButton.style.display = "initial";
createDownloadChapterTaskButton.style.display = "initial";
publicationDelete.style.display = "none"; publicationDelete.style.display = "none";
publicationTaskStart.style.display = "none"; publicationTaskStart.style.display = "none";
} }
else{ else{
publicationAdd.style.display = "none"; createMonitorTaskButton.style.display = "none";
createDownloadChapterTaskButton.style.display = "none";
publicationDelete.style.display = "initial"; publicationDelete.style.display = "initial";
publicationTaskStart.style.display = "initial"; publicationTaskStart.style.display = "initial";
} }
@ -211,10 +258,8 @@ function HidePublicationPopup(){
function ShowNewTaskWindow(){ function ShowNewTaskWindow(){
selectPublication.replaceChildren(); selectPublication.replaceChildren();
addTaskPopup.style.display = "block"; searchPublicationQuery.value = "";
} selectPublicationPopup.style.display = "flex";
function HideAddTaskPopup(){
addTaskPopup.style.display = "none";
} }
@ -234,10 +279,6 @@ function OpenSettings(){
settingsPopup.style.display = "flex"; settingsPopup.style.display = "flex";
} }
function HideSettings(){
settingsPopup.style.display = "none";
}
function GetSettingsClick(){ function GetSettingsClick(){
settingApiUri.value = ""; settingApiUri.value = "";
settingKomgaUrl.value = ""; settingKomgaUrl.value = "";
@ -288,7 +329,7 @@ function UpdateLibrarySettings(){
if(settingKavitaUrl.value != "" && settingKavitaUser.value != "" && settingKavitaPass.value != ""){ if(settingKavitaUrl.value != "" && settingKavitaUser.value != "" && settingKavitaPass.value != ""){
UpdateSettings("", "", "", settingKavitaUrl.value, settingKavitaUser.value, settingKavitaPass.value); UpdateSettings("", "", "", settingKavitaUrl.value, settingKavitaUser.value, settingKavitaPass.value);
} }
CreateTask("UpdateLibraries", libraryUpdateTime.value, "","",""); CreateUpdateLibraryTask(libraryUpdateTime.value);
setTimeout(() => GetSettingsClick(), 200); setTimeout(() => GetSettingsClick(), 200);
} }
@ -296,66 +337,6 @@ function utf8_to_b64( str ) {
return window.btoa(unescape(encodeURIComponent( str ))); return window.btoa(unescape(encodeURIComponent( str )));
} }
function ShowRunningTasks(event){
GetRunningTasks()
.then(json => {
tagTasksPopupContent.replaceChildren();
json.forEach(task => {
if(task.publication != null){
var taskname = document.createElement("footer-tag-task-name");
if(task.task == 2)
taskname.innerText = `${task.publication.sortName} - ${task.progress.toLocaleString(undefined,{style: 'percent', minimumFractionDigits:2})}`;
else if(task.task == 4)
taskname.innerText = `${task.publication.sortName} Vol.${task.chapter.volumeNumber} Ch.${task.chapter.chapterNumber} - ${task.progress.toLocaleString(undefined,{style: 'percent', minimumFractionDigits:2})}`;
tagTasksPopupContent.appendChild(taskname);
}
});
if(tagTasksPopupContent.children.length > 0){
tagTaskPopup.style.display = "block";
tagTaskPopup.style.left = `${tagTasksRunning.offsetLeft - 20}px`;
}
});
}
function ShowQueuedTasks(event){
GetQueue()
.then(json => {
tagTasksPopupContent.replaceChildren();
json.forEach(task => {
var taskname = document.createElement("footer-tag-task-name");
if(task.task == 2)
taskname.innerText = `${task.publication.sortName}`;
else if(task.task == 4)
taskname.innerText = `${task.publication.sortName} Vol.${task.chapter.volumeNumber} Ch.${task.chapter.chapterNumber}`;
tagTasksPopupContent.appendChild(taskname);
});
if(json.length > 0){
tagTaskPopup.style.display = "block";
tagTaskPopup.style.left = `${tagTasksQueued.offsetLeft- 20}px`;
}
});
}
function ShowAllTasks(event){
GetDownloadTasks()
.then(json => {
tagTasksPopupContent.replaceChildren();
json.forEach(task => {
var taskname = document.createElement("footer-tag-task-name");
taskname.innerText = task.publication.sortName;
tagTasksPopupContent.appendChild(taskname);
});
if(json.length > 0){
tagTaskPopup.style.display = "block";
tagTaskPopup.style.left = `${tagTasksTotal.offsetLeft - 20}px`;
}
});
}
function CloseTasksPopup(){
tagTaskPopup.style.display = "none";
}
function FilterResults(){ function FilterResults(){
if(searchBox.value.length > 0){ if(searchBox.value.length > 0){
tasksContent.childNodes.forEach(publication => { tasksContent.childNodes.forEach(publication => {
@ -376,12 +357,72 @@ function FilterResults(){
}else{ }else{
tasksContent.childNodes.forEach(publication => publication.style.display = "initial"); tasksContent.childNodes.forEach(publication => publication.style.display = "initial");
} }
}
function ShowTasksQueue(){
downloadTasksOutput.replaceChildren();
GetRunningTasks()
.then(json => {
tagTasksRunning.innerText = json.length;
json.forEach(task => {
downloadTasksOutput.appendChild(CreateProgressChild(task));
document.querySelector(`#progress${task.taskId.replaceAll('=','')}`).value = task.progress;
document.querySelector(`#progressStr${task.taskId.replaceAll('=','')}`).innerText = task.progress.toLocaleString(undefined,{style: 'percent', minimumFractionDigits:2});
});
});
GetQueue()
.then(json => {
tagTasksQueued.innerText = json.length;
json.forEach(task => {
downloadTasksOutput.appendChild(CreateProgressChild(task));
});
});
downloadTasksPopup.style.display = "flex";
}
function CreateProgressChild(task){
var child = document.createElement("div");
var img = document.createElement('img');
img.src = `imageCache/${task.publication.coverFileNameInCache}`;
child.appendChild(img);
var name = document.createElement("span");
name.innerText = task.publication.sortName;
name.className = "pubTitle";
child.appendChild(name);
var progress = document.createElement("progress");
progress.id = `progress${task.taskId.replaceAll('=','')}`;
child.appendChild(progress);
var progressStr = document.createElement("span");
progressStr.innerText = "00.00%";
progressStr.className = "progressStr";
progressStr.id = `progressStr${task.taskId.replaceAll('=','')}`;
child.appendChild(progressStr);
if(task.chapter != undefined){
var chapterNumber = document.createElement("span");
chapterNumber.className = "chapterNumber";
chapterNumber.innerText = `Vol.${task.chapter.volumeNumber} Ch.${task.chapter.chapterNumber}`;
child.appendChild(chapterNumber);
var chapterName = document.createElement("span");
chapterName.className = "chapterName";
chapterName.innerText = task.chapter.name;
child.appendChild(chapterName);
}
return child;
} }
//Resets the tasks shown //Resets the tasks shown
ResetContent(); ResetContent();
downloadTasksOutput.replaceChildren();
//Get Tasks and show them //Get Tasks and show them
GetDownloadTasks() GetDownloadTasks()
.then(json => json.forEach(task => { .then(json => json.forEach(task => {
@ -394,16 +435,17 @@ GetDownloadTasks()
GetRunningTasks() GetRunningTasks()
.then(json => { .then(json => {
tagTasksRunning.innerText = json.length; tagTasksRunning.innerText = json.length;
}); json.forEach(task => {
downloadTasksOutput.appendChild(CreateProgressChild(task));
GetDownloadTasks() });
.then(json => {
tagTasksTotal.innerText = json.length;
}); });
GetQueue() GetQueue()
.then(json => { .then(json => {
tagTasksQueued.innerText = json.length; tagTasksQueued.innerText = json.length;
json.forEach(task => {
downloadTasksOutput.appendChild(CreateProgressChild(task));
});
}) })
setInterval(() => { setInterval(() => {
@ -432,15 +474,18 @@ setInterval(() => {
.then(json => { .then(json => {
tagTasksRunning.innerText = json.length; tagTasksRunning.innerText = json.length;
}); });
GetDownloadTasks()
.then(json => {
tagTasksTotal.innerText = json.length;
});
GetQueue() GetQueue()
.then(json => { .then(json => {
tagTasksQueued.innerText = json.length; tagTasksQueued.innerText = json.length;
}) });
}, 1000);
}, 1000);
setInterval(() => {
GetRunningTasks().then((json) => {
json.forEach(task => {
document.querySelector(`#progress${task.taskId.replaceAll('=','')}`).value = task.progress;
document.querySelector(`#progressStr${task.taskId.replaceAll('=','')}`).innerText = task.progress.toLocaleString(undefined,{style: 'percent', minimumFractionDigits:2});
});
});
},500);

View File

@ -147,46 +147,22 @@ content {
align-content: start; align-content: start;
} }
settings {
width: 50%;
background-color: var(--accent-color);
display: flex;
flex-direction: column;
z-index: 10;
position: absolute;
left: 25%;
top: 100px;
border-radius: 5px;
padding: 10px 0;
}
#settingsPopup{ #settingsPopup{
z-index: 10; z-index: 10;
} }
settings > * { #settingsPopup popup-content{
margin: 0 20%;
}
settings input {
margin: 3px 0;
padding: 3px;
border-radius: 3px;
border: 1px solid rgba(0,0,0,0.2);
width: 100%;
}
settings .title {
font-weight: bolder;
font-size: 14pt;
margin: 15px 0 2px 0;
}
komga-settings {
margin-top: 20px;
display: flex;
flex-direction: column; flex-direction: column;
flex-wrap: nowrap; align-items: start;
margin: 15px 10px;
}
#settingsPopup popup-content > * {
margin: 5px 10px;
}
#settingsPopup popup-content .title {
font-weight: bolder;
} }
#addPublication { #addPublication {
@ -281,6 +257,186 @@ popup{
left: 0; left: 0;
position: fixed; position: fixed;
z-index: 2; z-index: 2;
flex-direction: column;
}
popup popup-window {
position: absolute;
z-index: 3;
left: 25%;
top: 100px;
width: 50%;
display: flex;
flex-direction: column;
background-color: var(--second-background-color);
border-radius: 3px;
overflow: hidden;
}
popup popup-window popup-title {
height: 30px;
font-size: 14pt;
font-weight: bolder;
padding: 5px 10px;
margin: 0;
display: flex;
align-items: center;
background-color: var(--primary-color);
color: var(--accent-color)
}
popup popup-window popup-content{
margin: 15px 10px;
display: flex;
align-items: center;
justify-content: space-evenly;
}
popup popup-window popup-content div > * {
margin: 2px 3px 0 0;
}
popup popup-window popup-content input, select {
padding: 3px 4px;
width: 130px;
border: 1px solid lightgrey;
background-color: var(--accent-color);
border-radius: 3px;
}
#selectPublicationPopup publication {
width: 150px;
height: 250px;
}
#createTaskPopup {
z-index: 7;
}
#createTaskPopup input {
height: 30px;
width: 200px;
}
#createMonitorTaskPopup, #createDownloadChaptersTask {
z-index: 9;
}
#createMonitorTaskPopup input[type="number"] {
width: 40px;
}
#createDownloadChaptersTask popup-content {
flex-direction: column;
align-items: start;
}
#createDownloadChaptersTask popup-content > * {
margin: 3px 0;
}
#createDownloadChaptersTask #chapterOutput {
max-height: 50vh;
overflow-y: scroll;
}
#createDownloadChaptersTask #chapterOutput .index{
display: inline-block;
width: 25px;
}
#createDownloadChaptersTask #chapterOutput .index::after{
content: ':';
}
#createDownloadChaptersTask #chapterOutput .vol::before{
content: 'Vol.';
}
#createDownloadChaptersTask #chapterOutput .vol{
display: inline-block;
width: 45px;
}
#createDownloadChaptersTask #chapterOutput .ch::before{
content: 'Ch.';
}
#createDownloadChaptersTask #chapterOutput .ch {
display: inline-block;
width: 60px;
}
#downloadTasksPopup popup-window {
left: 0;
top: 80px;
margin: 0 0 0 10px;
height: calc(100vh - 140px);
width: 400px;
max-width: 95vw;
overflow-y: scroll;
}
#downloadTasksPopup popup-content {
flex-direction: column;
align-items: start;
margin: 5px;
}
#downloadTasksPopup popup-content > div {
display: block;
height: 80px;
position: relative;
margin: 5px 0;
}
#downloadTasksPopup popup-content > div > img {
display: block;
position: absolute;
height: 100%;
width: 60px;
left: 0;
top: 0;
object-fit: cover;
border-radius: 4px;
}
#downloadTasksPopup popup-content > div > span {
display: block;
position: absolute;
width: max-content;
}
#downloadTasksPopup popup-content > div > .pubTitle {
left: 70px;
top: 0;
}
#downloadTasksPopup popup-content > div > .chapterName {
left: 70px;
top: 28pt;
}
#downloadTasksPopup popup-content > div > .chapterNumber {
left: 70px;
top: 14pt;
}
#downloadTasksPopup popup-content > div > progress {
display: block;
position: absolute;
left: 150px;
bottom: 0;
width: 200px;
}
#downloadTasksPopup popup-content > div > .progressStr {
display: block;
position: absolute;
left: 70px;
bottom: 0;
width: 70px;
} }
blur-background { blur-background {
@ -292,79 +448,6 @@ blur-background {
opacity: 0.5; opacity: 0.5;
} }
addtask-window {
display: flex;
flex-direction: column;
flex-wrap: nowrap;
position: absolute;
left: 12.5%;
top: 15%;
width: 75%;
min-height: 70%;
max-height: 80%;
padding: 0;
background-color: var(--accent-color);
border-radius: 5px;
}
window-titlebar {
width: 100%;
height: 60px;
background-color: var(--primary-color);
border-radius: 5px 5px 0 0;
color: var(--accent-color);
display: flex block;
flex-direction: row;
justify-content: space-between;
align-items: center;
}
window-titlebar p {
margin: 0 30px;
font-size: 14pt;
font-weight: bolder;
letter-spacing: 1px;
}
window-titlebar #closePopupImg {
height: 70%;
cursor: pointer;
margin-right: 20px;
filter: invert(100%) sepia(0%) saturate(100%) hue-rotate(115deg) brightness(116%) contrast(101%);
}
window-content {
display: flex;
flex-direction: column;
padding: 20px 5%;
overflow-x: scroll;
}
addtask-settings{
display: flex;
justify-content: center;
align-items: center;
}
addtask-settings select, addtask-settings input{
padding: 5px;
font-size: 10pt;
border: 1px solid rgba(0,0,0,0.2);
border-radius: 3px;
background-color: transparent;
margin: 10px 0;
width: 150px;
}
addtask-settings label {
font-weight: bolder;
margin: 0 5px;
}
addtask-settings addtask-setting{
margin: 0 15px;
}
#taskSelectOutput{ #taskSelectOutput{
display: flex; display: flex;
flex-direction: row; flex-direction: row;
@ -512,7 +595,7 @@ footer-tag-popup::before{
border-left: 10px solid transparent; border-left: 10px solid transparent;
border-top: 10px solid var(--second-background-color); border-top: 10px solid var(--second-background-color);
border-bottom: 10px solid transparent; border-bottom: 10px solid transparent;
left: 0px; left: 0;
bottom: -17px; bottom: -17px;
border-radius: 0 0 0 5px; border-radius: 0 0 0 5px;
} }