diff --git a/Dockerfile b/Dockerfile index 6bab183..b495a44 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,7 +10,20 @@ RUN dotnet restore /src/Tranga/Tranga.csproj RUN dotnet publish -c Release -o /publish FROM glax/tranga-base:latest as runtime +EXPOSE 6531 +ARG UNAME=tranga +ARG UID=1000 +ARG GID=1000 +RUN groupadd -g $GID -o $UNAME +RUN useradd -m -u $UID -g $GID -o -s /bin/bash $UNAME +RUN mkdir /usr/share/tranga-api +RUN mkdir /Manga +RUN chown 1000:1000 /usr/share/tranga-api +RUN chown 1000:1000 /Manga +USER $UNAME + WORKDIR /publish COPY --from=build-env /publish . -EXPOSE 6531 +USER 0 +RUN chown 1000:1000 /publish ENTRYPOINT ["dotnet", "/publish/Tranga.dll", "-c"] diff --git a/Tranga/Jobs/DownloadChapter.cs b/Tranga/Jobs/DownloadChapter.cs index 8a27929..9d0fe10 100644 --- a/Tranga/Jobs/DownloadChapter.cs +++ b/Tranga/Jobs/DownloadChapter.cs @@ -31,6 +31,7 @@ public class DownloadChapter : Job { Task downloadTask = new(delegate { + mangaConnector.CopyCoverFromCacheToDownloadLocation(chapter.parentManga); mangaConnector.DownloadChapter(chapter, this.progressToken); UpdateLibraries(); SendNotifications("Chapter downloaded", $"{chapter.parentManga.sortName} - {chapter.chapterNumber}"); diff --git a/Tranga/Jobs/DownloadNewChapters.cs b/Tranga/Jobs/DownloadNewChapters.cs index a386b98..6e2e3f2 100644 --- a/Tranga/Jobs/DownloadNewChapters.cs +++ b/Tranga/Jobs/DownloadNewChapters.cs @@ -36,6 +36,7 @@ public class DownloadNewChapters : Job Chapter[] chapters = mangaConnector.GetNewChapters(manga, this.translatedLanguage); this.progressToken.increments = chapters.Length; List jobs = new(); + mangaConnector.CopyCoverFromCacheToDownloadLocation(manga); foreach (Chapter chapter in chapters) { DownloadChapter downloadChapterJob = new(this, this.mangaConnector, chapter, parentJobId: this.id); diff --git a/Tranga/Jobs/Job.cs b/Tranga/Jobs/Job.cs index 86d5b19..f76253c 100644 --- a/Tranga/Jobs/Job.cs +++ b/Tranga/Jobs/Job.cs @@ -59,13 +59,13 @@ public abstract class Job : GlobalBase public void ResetProgress() { - this.progressToken.increments = this.progressToken.increments - this.progressToken.incrementsCompleted; + this.progressToken.increments -= progressToken.incrementsCompleted; this.lastExecution = DateTime.Now; } public void ExecutionEnqueue() { - this.progressToken.increments = this.progressToken.increments - this.progressToken.incrementsCompleted; + this.progressToken.increments -= progressToken.incrementsCompleted; this.lastExecution = recurrenceTime is not null ? DateTime.Now.Subtract((TimeSpan)recurrenceTime) : DateTime.UnixEpoch; this.progressToken.Standby(); } diff --git a/Tranga/Jobs/JobBoss.cs b/Tranga/Jobs/JobBoss.cs index a54a023..6ecd8ea 100644 --- a/Tranga/Jobs/JobBoss.cs +++ b/Tranga/Jobs/JobBoss.cs @@ -14,6 +14,7 @@ public class JobBoss : GlobalBase this.jobs = new(); LoadJobsList(connectors); this.mangaConnectorJobQueue = new(); + Log($"Next job in {jobs.MinBy(job => job.nextExecution)?.nextExecution.Subtract(DateTime.Now)} {jobs.MinBy(job => job.nextExecution)?.id}"); } public void AddJob(Job job) @@ -26,7 +27,7 @@ public class JobBoss : GlobalBase { Log($"Added {job}"); this.jobs.Add(job); - ExportJobsList(); + ExportJob(job); } } @@ -56,7 +57,7 @@ public class JobBoss : GlobalBase this.jobs.Remove(job); if(job.subJobs is not null) RemoveJobs(job.subJobs); - ExportJobsList(); + ExportJob(job); } public void RemoveJobs(IEnumerable jobsToRemove) @@ -161,21 +162,38 @@ public class JobBoss : GlobalBase cachedPublications.Add(ncJob.manga); } + public void ExportJob(Job job) + { + string jobFilePath = Path.Join(settings.jobsFolderPath, $"{job.id}.json"); + Log($"Exporting Job {jobFilePath}"); + + if (!this.jobs.Any(jjob => jjob.id == job.id)) + { + try + { + while(IsFileInUse(jobFilePath)) + Thread.Sleep(10); + File.Delete(jobFilePath); + } + catch (Exception e) + { + Log(e.ToString()); + } + } + else + { + string jobStr = JsonConvert.SerializeObject(job); + while(IsFileInUse(jobFilePath)) + Thread.Sleep(10); + File.WriteAllText(jobFilePath, jobStr); + } + } + public void ExportJobsList() { Log("Exporting Jobs"); foreach (Job job in this.jobs) - { - string jobFilePath = Path.Join(settings.jobsFolderPath, $"{job.id}.json"); - if (!File.Exists(jobFilePath)) - { - string jobStr = JsonConvert.SerializeObject(job); - while(IsFileInUse(jobFilePath)) - Thread.Sleep(10); - Log($"Exporting Job {jobFilePath}"); - File.WriteAllText(jobFilePath, jobStr); - } - } + ExportJob(job); //Remove files with jobs not in this.jobs-list Regex idRex = new (@"(.*)\.json"); @@ -220,7 +238,10 @@ public class JobBoss : GlobalBase queueHead.progressToken.Complete(); break; } + queueHead.ResetProgress(); jobQueue.Dequeue(); + ExportJob(queueHead); + Log($"Next job in {jobs.MinBy(job => job.nextExecution)?.nextExecution.Subtract(DateTime.Now)} {jobs.MinBy(job => job.nextExecution)?.id}"); }else if (queueHead.progressToken.state is ProgressToken.State.Standby) { Job[] subJobs = jobQueue.Peek().ExecuteReturnSubTasks().ToArray(); diff --git a/Tranga/LibraryConnectors/LibraryConnector.cs b/Tranga/LibraryConnectors/LibraryConnector.cs index e3e3caa..cbf4446 100644 --- a/Tranga/LibraryConnectors/LibraryConnector.cs +++ b/Tranga/LibraryConnectors/LibraryConnector.cs @@ -1,5 +1,6 @@ using System.Net; using System.Net.Http.Headers; +using System.Text.RegularExpressions; using Logging; namespace Tranga.LibraryConnectors; @@ -20,7 +21,10 @@ public abstract class LibraryConnector : GlobalBase protected LibraryConnector(GlobalBase clone, string baseUrl, string auth, LibraryType libraryType) : base(clone) { - this.baseUrl = baseUrl; + Regex urlRex = new(@"https?:\/\/[0-9A-z\.]*"); + if (!urlRex.IsMatch(baseUrl)) + throw new ArgumentException("Base url does not match pattern"); + this.baseUrl = urlRex.Match(baseUrl).Value; this.auth = auth; this.libraryType = libraryType; } diff --git a/Tranga/MangaConnectors/MangaConnector.cs b/Tranga/MangaConnectors/MangaConnector.cs index 74b49e9..09fb7f2 100644 --- a/Tranga/MangaConnectors/MangaConnector.cs +++ b/Tranga/MangaConnectors/MangaConnector.cs @@ -162,7 +162,7 @@ public abstract class MangaConnector : GlobalBase Log($"Cloning cover {fileInCache} -> {newFilePath}"); File.Copy(fileInCache, newFilePath, true); if(RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) - File.SetUnixFileMode(newFilePath, GroupRead | GroupWrite | OtherRead | OtherWrite | UserRead | UserWrite); + File.SetUnixFileMode(newFilePath, GroupRead | GroupWrite | UserRead | UserWrite); } /// @@ -193,7 +193,11 @@ public abstract class MangaConnector : GlobalBase //Check if Publication Directory already exists string directoryPath = Path.GetDirectoryName(saveArchiveFilePath)!; if (!Directory.Exists(directoryPath)) - Directory.CreateDirectory(directoryPath); + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + Directory.CreateDirectory(directoryPath, + UserRead | UserWrite | UserExecute | GroupRead | GroupWrite | GroupExecute ); + else + Directory.CreateDirectory(directoryPath); if (File.Exists(saveArchiveFilePath)) //Don't download twice. return HttpStatusCode.OK; @@ -229,7 +233,7 @@ public abstract class MangaConnector : GlobalBase //ZIP-it and ship-it ZipFile.CreateFromDirectory(tempFolder, saveArchiveFilePath); if(RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) - File.SetUnixFileMode(saveArchiveFilePath, GroupRead | GroupWrite | OtherRead | OtherWrite | UserRead | UserWrite); + File.SetUnixFileMode(saveArchiveFilePath, UserRead | UserWrite | UserExecute | GroupRead | GroupWrite | GroupExecute); Directory.Delete(tempFolder, true); //Cleanup progressToken?.Complete(); diff --git a/Tranga/MangaConnectors/MangaDex.cs b/Tranga/MangaConnectors/MangaDex.cs index f0203bd..cd0226e 100644 --- a/Tranga/MangaConnectors/MangaDex.cs +++ b/Tranga/MangaConnectors/MangaDex.cs @@ -225,17 +225,27 @@ public class MangaDex : MangaConnector public override HttpStatusCode DownloadChapter(Chapter chapter, ProgressToken? progressToken = null) { if (progressToken?.cancellationRequested ?? false) + { + progressToken?.Cancel(); return HttpStatusCode.RequestTimeout; + } + Manga chapterParentManga = chapter.parentManga; Log($"Retrieving chapter-info {chapter} {chapterParentManga}"); //Request URLs for Chapter-Images DownloadClient.RequestResult requestResult = downloadClient.MakeRequest($"https://api.mangadex.org/at-home/server/{chapter.url}?forcePort443=false'", (byte)RequestType.AtHomeServer); if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300) + { + progressToken?.Cancel(); return requestResult.statusCode; + } JsonObject? result = JsonSerializer.Deserialize(requestResult.result); if (result is null) + { + progressToken?.Cancel(); return HttpStatusCode.NoContent; + } string baseUrl = result["baseUrl"]!.GetValue(); string hash = result["chapter"]!["hash"]!.GetValue(); diff --git a/Tranga/MangaConnectors/MangaKatana.cs b/Tranga/MangaConnectors/MangaKatana.cs index 12f15cd..70c60b2 100644 --- a/Tranga/MangaConnectors/MangaKatana.cs +++ b/Tranga/MangaConnectors/MangaKatana.cs @@ -186,7 +186,11 @@ public class MangaKatana : MangaConnector public override HttpStatusCode DownloadChapter(Chapter chapter, ProgressToken? progressToken = null) { if (progressToken?.cancellationRequested ?? false) + { + progressToken?.Cancel(); return HttpStatusCode.RequestTimeout; + } + Manga chapterParentManga = chapter.parentManga; Log($"Retrieving chapter-info {chapter} {chapterParentManga}"); string requestUrl = chapter.url; @@ -194,7 +198,10 @@ public class MangaKatana : MangaConnector DownloadClient.RequestResult requestResult = downloadClient.MakeRequest(requestUrl, 1); if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300) + { + progressToken?.Cancel(); return requestResult.statusCode; + } string[] imageUrls = ParseImageUrlsFromHtml(requestUrl); diff --git a/Tranga/MangaConnectors/Manganato.cs b/Tranga/MangaConnectors/Manganato.cs index 6b3cd23..d56a298 100644 --- a/Tranga/MangaConnectors/Manganato.cs +++ b/Tranga/MangaConnectors/Manganato.cs @@ -172,17 +172,28 @@ public class Manganato : MangaConnector public override HttpStatusCode DownloadChapter(Chapter chapter, ProgressToken? progressToken = null) { if (progressToken?.cancellationRequested ?? false) + { + progressToken?.Cancel(); return HttpStatusCode.RequestTimeout; + } + Manga chapterParentManga = chapter.parentManga; Log($"Retrieving chapter-info {chapter} {chapterParentManga}"); string requestUrl = chapter.url; DownloadClient.RequestResult requestResult = downloadClient.MakeRequest(requestUrl, 1); if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300) + { + progressToken?.Cancel(); return requestResult.statusCode; + } if (requestResult.htmlDocument is null) + { + progressToken?.Cancel(); return HttpStatusCode.InternalServerError; + } + string[] imageUrls = ParseImageUrlsFromHtml(requestResult.htmlDocument); string comicInfoPath = Path.GetTempFileName(); diff --git a/Tranga/MangaConnectors/Mangasee.cs b/Tranga/MangaConnectors/Mangasee.cs index aeae553..382b1c2 100644 --- a/Tranga/MangaConnectors/Mangasee.cs +++ b/Tranga/MangaConnectors/Mangasee.cs @@ -179,16 +179,27 @@ public class Mangasee : MangaConnector public override HttpStatusCode DownloadChapter(Chapter chapter, ProgressToken? progressToken = null) { if (progressToken?.cancellationRequested ?? false) + { + progressToken?.Cancel(); return HttpStatusCode.RequestTimeout; + } + Manga chapterParentManga = chapter.parentManga; - if (progressToken?.cancellationRequested??false) + if (progressToken?.cancellationRequested ?? false) + { + progressToken?.Cancel(); return HttpStatusCode.RequestTimeout; - + } + Log($"Retrieving chapter-info {chapter} {chapterParentManga}"); DownloadClient.RequestResult requestResult = this.downloadClient.MakeRequest(chapter.url, 1); - if(requestResult.htmlDocument is null) + if (requestResult.htmlDocument is null) + { + progressToken?.Cancel(); return HttpStatusCode.RequestTimeout; + } + HtmlDocument document = requestResult.htmlDocument; HtmlNode gallery = document.DocumentNode.Descendants("div").First(div => div.HasClass("ImageGallery")); diff --git a/Tranga/MangaConnectors/Mangaworld.cs b/Tranga/MangaConnectors/Mangaworld.cs index ae61871..9501b21 100644 --- a/Tranga/MangaConnectors/Mangaworld.cs +++ b/Tranga/MangaConnectors/Mangaworld.cs @@ -153,17 +153,28 @@ public class Mangaworld: MangaConnector public override HttpStatusCode DownloadChapter(Chapter chapter, ProgressToken? progressToken = null) { if (progressToken?.cancellationRequested ?? false) + { + progressToken?.Cancel(); return HttpStatusCode.RequestTimeout; + } + Manga chapterParentManga = chapter.parentManga; Log($"Retrieving chapter-info {chapter} {chapterParentManga}"); string requestUrl = $"{chapter.url}?style=list"; DownloadClient.RequestResult requestResult = downloadClient.MakeRequest(requestUrl, 1); if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300) + { + progressToken?.Cancel(); return requestResult.statusCode; + } if (requestResult.htmlDocument is null) + { + progressToken?.Cancel(); return HttpStatusCode.InternalServerError; + } + string[] imageUrls = ParseImageUrlsFromHtml(requestResult.htmlDocument); string comicInfoPath = Path.GetTempFileName(); diff --git a/Tranga/Server.cs b/Tranga/Server.cs index bf71ffc..bd9625e 100644 --- a/Tranga/Server.cs +++ b/Tranga/Server.cs @@ -192,10 +192,10 @@ public class Server : GlobalBase SendResponse(HttpStatusCode.OK, response, _parent.jobBoss.jobs.Where(jjob => jjob.progressToken.state is ProgressToken.State.Running)); break; case "Jobs/Waiting": - SendResponse(HttpStatusCode.OK, response, _parent.jobBoss.jobs.Where(jjob => jjob.progressToken.state is ProgressToken.State.Standby)); + SendResponse(HttpStatusCode.OK, response, _parent.jobBoss.jobs.Where(jjob => jjob.progressToken.state is ProgressToken.State.Standby).OrderBy(jjob => jjob.nextExecution)); break; case "Jobs/MonitorJobs": - SendResponse(HttpStatusCode.OK, response, _parent.jobBoss.jobs.Where(jjob => jjob is DownloadNewChapters)); + SendResponse(HttpStatusCode.OK, response, _parent.jobBoss.jobs.Where(jjob => jjob is DownloadNewChapters).OrderBy(jjob => ((DownloadNewChapters)jjob).manga.sortName)); break; case "Settings": SendResponse(HttpStatusCode.OK, response, settings);