7 Commits

10 changed files with 95 additions and 72 deletions

View File

@ -14,9 +14,9 @@ public abstract class Connector
internal string downloadLocation { get; } //Location of local files internal string downloadLocation { get; } //Location of local files
protected DownloadClient downloadClient { get; init; } protected DownloadClient downloadClient { get; init; }
protected Logger? logger; protected readonly Logger? logger;
protected string imageCachePath; protected readonly string imageCachePath;
protected Connector(string downloadLocation, string imageCachePath, Logger? logger) protected Connector(string downloadLocation, string imageCachePath, Logger? logger)
{ {
@ -61,23 +61,25 @@ public abstract class Connector
/// </summary> /// </summary>
/// <param name="publication">Publication to retrieve Cover for</param> /// <param name="publication">Publication to retrieve Cover for</param>
/// <param name="settings">TrangaSettings</param> /// <param name="settings">TrangaSettings</param>
public abstract void CloneCoverFromCache(Publication publication, TrangaSettings settings); public void CloneCoverFromCache(Publication publication, TrangaSettings settings)
/// <summary>
/// Saves the series-info to series.json in the Publication Folder
/// </summary>
/// <param name="publication">Publication to save series.json for</param>
public void SaveSeriesInfo(Publication publication)
{ {
logger?.WriteLine(this.GetType().ToString(), $"Saving series.json for {publication.sortName}"); logger?.WriteLine(this.GetType().ToString(), $"Cloning cover {publication.sortName}");
//Check if Publication already has a Folder and a series.json //Check if Publication already has a Folder and cover
string publicationFolder = Path.Join(downloadLocation, publication.folderName); string publicationFolder = Path.Join(downloadLocation, publication.folderName);
if(!Directory.Exists(publicationFolder)) if(!Directory.Exists(publicationFolder))
Directory.CreateDirectory(publicationFolder); Directory.CreateDirectory(publicationFolder);
DirectoryInfo dirInfo = new (publicationFolder);
if (dirInfo.EnumerateFiles().Any(info => info.Name.Contains("cover.")))
{
logger?.WriteLine(this.GetType().ToString(), $"Cover exists {publication.sortName}");
return;
}
string seriesInfoPath = Path.Join(publicationFolder, "series.json"); string fileInCache = Path.Join(settings.coverImageCache, publication.coverFileNameInCache);
if(!File.Exists(seriesInfoPath)) string newFilePath = Path.Join(publicationFolder, $"cover.{Path.GetFileName(fileInCache).Split('.')[^1]}" );
File.WriteAllText(seriesInfoPath,publication.GetSeriesInfoJson()); logger?.WriteLine(this.GetType().ToString(), $"Cloning cover {fileInCache} -> {newFilePath}");
File.Copy(fileInCache, newFilePath, true);
} }
/// <summary> /// <summary>
@ -85,7 +87,7 @@ public abstract class Connector
/// See ComicInfo.xml /// See ComicInfo.xml
/// </summary> /// </summary>
/// <returns>XML-string</returns> /// <returns>XML-string</returns>
protected static string CreateComicInfo(Publication publication, Chapter chapter, Logger? logger) protected static string GetComicInfoXmlString(Publication publication, Chapter chapter, Logger? logger)
{ {
logger?.WriteLine("Connector", $"Creating ComicInfo.Xml for {publication.sortName} {publication.internalId} {chapter.volumeNumber}-{chapter.chapterNumber}"); logger?.WriteLine("Connector", $"Creating ComicInfo.Xml for {publication.sortName} {publication.internalId} {chapter.volumeNumber}-{chapter.chapterNumber}");
XElement comicInfo = new XElement("ComicInfo", XElement comicInfo = new XElement("ComicInfo",
@ -103,16 +105,16 @@ public abstract class Connector
/// Checks if a chapter-archive is already present /// Checks if a chapter-archive is already present
/// </summary> /// </summary>
/// <returns>true if chapter is present</returns> /// <returns>true if chapter is present</returns>
public bool ChapterIsDownloaded(Publication publication, Chapter chapter) public bool CheckChapterIsDownloaded(Publication publication, Chapter chapter)
{ {
return File.Exists(CreateFullFilepath(publication, chapter)); return File.Exists(GetArchiveFilePath(publication, chapter));
} }
/// <summary> /// <summary>
/// Creates full file path of chapter-archive /// Creates full file path of chapter-archive
/// </summary> /// </summary>
/// <returns>Filepath</returns> /// <returns>Filepath</returns>
protected string CreateFullFilepath(Publication publication, Chapter chapter) protected string GetArchiveFilePath(Publication publication, Chapter chapter)
{ {
return Path.Join(downloadLocation, publication.folderName, $"{chapter.fileName}.cbz"); return Path.Join(downloadLocation, publication.folderName, $"{chapter.fileName}.cbz");
} }
@ -122,9 +124,8 @@ public abstract class Connector
/// </summary> /// </summary>
/// <param name="imageUrl"></param> /// <param name="imageUrl"></param>
/// <param name="fullPath"></param> /// <param name="fullPath"></param>
/// <param name="downloadClient">DownloadClient of the connector</param>
/// <param name="requestType">Requesttype for ratelimit</param> /// <param name="requestType">Requesttype for ratelimit</param>
protected static void DownloadImage(string imageUrl, string fullPath, DownloadClient downloadClient, byte requestType) private void DownloadImage(string imageUrl, string fullPath, byte requestType)
{ {
DownloadClient.RequestResult requestResult = downloadClient.MakeRequest(imageUrl, requestType); DownloadClient.RequestResult requestResult = downloadClient.MakeRequest(imageUrl, requestType);
byte[] buffer = new byte[requestResult.result.Length]; byte[] buffer = new byte[requestResult.result.Length];
@ -137,11 +138,9 @@ public abstract class Connector
/// </summary> /// </summary>
/// <param name="imageUrls">List of URLs to download Images from</param> /// <param name="imageUrls">List of URLs to download Images from</param>
/// <param name="saveArchiveFilePath">Full path to save archive to (without file ending .cbz)</param> /// <param name="saveArchiveFilePath">Full path to save archive to (without file ending .cbz)</param>
/// <param name="downloadClient">DownloadClient of the connector</param>
/// <param name="logger"></param>
/// <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>
protected static void DownloadChapterImages(string[] imageUrls, string saveArchiveFilePath, DownloadClient downloadClient, byte requestType, Logger? logger, string? comicInfoPath = null) protected void DownloadChapterImages(string[] imageUrls, string saveArchiveFilePath, byte requestType, string? comicInfoPath = null)
{ {
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
@ -162,7 +161,7 @@ public abstract class Connector
string[] split = imageUrl.Split('.'); string[] split = imageUrl.Split('.');
string extension = split[^1]; string extension = split[^1];
logger?.WriteLine("Connector", $"Downloading Image {chapter + 1}/{imageUrls.Length}"); logger?.WriteLine("Connector", $"Downloading Image {chapter + 1}/{imageUrls.Length}");
DownloadImage(imageUrl, Path.Join(tempFolder, $"{chapter++}.{extension}"), downloadClient, requestType); DownloadImage(imageUrl, Path.Join(tempFolder, $"{chapter++}.{extension}"), requestType);
} }
if(comicInfoPath is not null) if(comicInfoPath is not null)

View File

@ -38,6 +38,7 @@ public class MangaDex : Connector
int offset = 0; //"Page" int offset = 0; //"Page"
int total = int.MaxValue; //How many total results are there, is updated on first request int total = int.MaxValue; //How many total results are there, is updated on first request
HashSet<Publication> publications = new(); HashSet<Publication> publications = new();
int loadedPublicationData = 0;
while (offset < total) //As long as we haven't requested all "Pages" while (offset < total) //As long as we haven't requested all "Pages"
{ {
//Request next Page //Request next Page
@ -55,10 +56,10 @@ public class MangaDex : Connector
total = result["total"]!.GetValue<int>(); //Update the total number of Publications total = result["total"]!.GetValue<int>(); //Update the total number of Publications
JsonArray mangaInResult = result["data"]!.AsArray(); //Manga-data-Array JsonArray mangaInResult = result["data"]!.AsArray(); //Manga-data-Array
logger?.WriteLine(this.GetType().ToString(), $"Getting publication data.");
//Loop each Manga and extract information from JSON //Loop each Manga and extract information from JSON
foreach (JsonNode? mangeNode in mangaInResult) foreach (JsonNode? mangeNode in mangaInResult)
{ {
logger?.WriteLine(this.GetType().ToString(), $"Getting publication data. {++loadedPublicationData}/{total}");
JsonObject manga = (JsonObject)mangeNode!; JsonObject manga = (JsonObject)mangeNode!;
JsonObject attributes = manga["attributes"]!.AsObject(); JsonObject attributes = manga["attributes"]!.AsObject();
@ -224,10 +225,10 @@ public class MangaDex : Connector
imageUrls.Add($"{baseUrl}/data/{hash}/{image!.GetValue<string>()}"); imageUrls.Add($"{baseUrl}/data/{hash}/{image!.GetValue<string>()}");
string comicInfoPath = Path.GetTempFileName(); string comicInfoPath = Path.GetTempFileName();
File.WriteAllText(comicInfoPath, CreateComicInfo(publication, chapter, logger)); File.WriteAllText(comicInfoPath, GetComicInfoXmlString(publication, chapter, logger));
//Download Chapter-Images //Download Chapter-Images
DownloadChapterImages(imageUrls.ToArray(), CreateFullFilepath(publication, chapter), downloadClient, (byte)RequestType.AtHomeServer, logger, comicInfoPath); DownloadChapterImages(imageUrls.ToArray(), GetArchiveFilePath(publication, chapter), (byte)RequestType.AtHomeServer, comicInfoPath);
} }
private string? GetCoverUrl(string publicationId, string? posterId) private string? GetCoverUrl(string publicationId, string? posterId)
@ -273,27 +274,6 @@ public class MangaDex : Connector
return author; return author;
} }
public override void CloneCoverFromCache(Publication publication, TrangaSettings settings)
{
logger?.WriteLine(this.GetType().ToString(), $"Cloning cover {publication.sortName}");
//Check if Publication already has a Folder and cover
string publicationFolder = Path.Join(downloadLocation, publication.folderName);
if(!Directory.Exists(publicationFolder))
Directory.CreateDirectory(publicationFolder);
DirectoryInfo dirInfo = new (publicationFolder);
if (dirInfo.EnumerateFiles().Any(info => info.Name.Contains("cover.")))
{
logger?.WriteLine(this.GetType().ToString(), $"Cover exists {publication.sortName}");
return;
}
string fileInCache = Path.Join(settings.coverImageCache, publication.coverFileNameInCache);
string newFilePath = Path.Join(publicationFolder, $"cover.{Path.GetFileName(fileInCache).Split('.')[^1]}" );
logger?.WriteLine(this.GetType().ToString(), $"Cloning cover {fileInCache} -> {newFilePath}");
File.Copy(fileInCache, newFilePath, true);
}
private string SaveImage(string url) private string SaveImage(string url)
{ {
string[] split = url.Split('/'); string[] split = url.Split('/');

View File

@ -48,6 +48,16 @@ public readonly struct Publication
this.internalId = Convert.ToBase64String(Encoding.ASCII.GetBytes($"{onlyLowerLetters}{this.year}")); this.internalId = Convert.ToBase64String(Encoding.ASCII.GetBytes($"{onlyLowerLetters}{this.year}"));
} }
public void SaveSeriesInfoJson(string downloadDirectory)
{
string publicationFolder = Path.Join(downloadDirectory, this.folderName);
if(!Directory.Exists(publicationFolder))
Directory.CreateDirectory(publicationFolder);
string seriesInfoPath = Path.Join(publicationFolder, "series.json");
if(!File.Exists(seriesInfoPath))
File.WriteAllText(seriesInfoPath,this.GetSeriesInfoJson());
}
/// <returns>Serialized JSON String for series.json</returns> /// <returns>Serialized JSON String for series.json</returns>
public string GetSeriesInfoJson() public string GetSeriesInfoJson()
{ {

View File

@ -100,9 +100,7 @@ public static class TaskExecutor
connector.CloneCoverFromCache(publication, settings); connector.CloneCoverFromCache(publication, settings);
string seriesInfoPath = Path.Join(publicationFolder, "series.json"); publication.SaveSeriesInfoJson(connector.downloadLocation);
if(!File.Exists(seriesInfoPath))
File.WriteAllText(seriesInfoPath,publication.GetSeriesInfoJson());
foreach(Chapter newChapter in newChapters) foreach(Chapter newChapter in newChapters)
connector.DownloadChapter(publication, newChapter); connector.DownloadChapter(publication, newChapter);
@ -122,7 +120,7 @@ public static class TaskExecutor
chapterCollection.TryAdd(publication, newChaptersList); //To ensure publication is actually in collection chapterCollection.TryAdd(publication, newChaptersList); //To ensure publication is actually in collection
Chapter[] newChapters = connector.GetChapters(publication, language); Chapter[] newChapters = connector.GetChapters(publication, language);
newChaptersList = newChapters.Where(nChapter => !connector.ChapterIsDownloaded(publication, nChapter)).ToList(); newChaptersList = newChapters.Where(nChapter => !connector.CheckChapterIsDownloaded(publication, nChapter)).ToList();
return newChaptersList; return newChaptersList;
} }

View File

@ -48,11 +48,10 @@ public class TaskManager
public void UpdateSettings(string? downloadLocation, string? komgaUrl, string? komgaAuth) public void UpdateSettings(string? downloadLocation, string? komgaUrl, string? komgaAuth)
{ {
Komga? komga = null; if (komgaUrl is not null && komgaAuth is not null && komgaUrl.Length > 0 && komgaAuth.Length > 0)
if (komgaUrl is not null && komgaAuth is not null) settings.komga = new Komga(komgaUrl, komgaAuth, null);
komga = new Komga(komgaUrl, komgaAuth, null); if (downloadLocation is not null && downloadLocation.Length > 0)
settings.downloadLocation = downloadLocation ?? settings.downloadLocation; settings.downloadLocation = downloadLocation;
settings.komga = komga ?? komga;
ExportData(); ExportData();
} }

View File

@ -14,6 +14,8 @@ public class TrangaSettings
public TrangaSettings(string downloadLocation, string workingDirectory, Komga? komga) public TrangaSettings(string downloadLocation, string workingDirectory, Komga? komga)
{ {
if (downloadLocation.Length < 1 || workingDirectory.Length < 1)
throw new ArgumentException("Download-location and working-directory paths can not be empty!");
this.workingDirectory = workingDirectory; this.workingDirectory = workingDirectory;
this.downloadLocation = downloadLocation; this.downloadLocation = downloadLocation;
this.komga = komga; this.komga = komga;

View File

@ -4,6 +4,7 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>Tranga</title> <title>Tranga</title>
<link rel="stylesheet" href="style.css"> <link rel="stylesheet" href="style.css">
<link rel="icon" type="image/x-icon" href="favicon.ico">
</head> </head>
<body> <body>
<wrapper> <wrapper>
@ -60,6 +61,7 @@
<img id="pubviewcover" src="media/cover.jpg" alt="cover"> <img id="pubviewcover" src="media/cover.jpg" alt="cover">
<publication-information> <publication-information>
<publication-name id="publicationViewerName">Tensei Pandemic</publication-name> <publication-name id="publicationViewerName">Tensei Pandemic</publication-name>
<publication-tags id="publicationViewerTags"></publication-tags>
<publication-author id="publicationViewerAuthor">Imamura Hinata</publication-author> <publication-author id="publicationViewerAuthor">Imamura Hinata</publication-author>
<publication-description id="publicationViewerDescription">Imamura Hinata is a high school boy with a cute appearance. <publication-description id="publicationViewerDescription">Imamura Hinata is a high school boy with a cute appearance.
Since his trauma with the first love, he wanted to be more manly than anybody else. But one day he woke up to something different… Since his trauma with the first love, he wanted to be more manly than anybody else. But one day he woke up to something different…

View File

@ -14,6 +14,7 @@ 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");
const publicationViewerName = document.querySelector("#publicationViewerName"); const publicationViewerName = document.querySelector("#publicationViewerName");
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");
@ -78,6 +79,11 @@ function NewSearch(){
selectRecurrence.disabled = true; selectRecurrence.disabled = true;
connectorSelect.disabled = true; connectorSelect.disabled = true;
searchPublicationQuery.disabled = true; searchPublicationQuery.disabled = true;
//Waitcursor
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();
@ -96,6 +102,11 @@ function NewSearch(){
selectRecurrence.disabled = false; selectRecurrence.disabled = false;
connectorSelect.disabled = false; connectorSelect.disabled = false;
searchPublicationQuery.disabled = false; searchPublicationQuery.disabled = false;
//Cursor
document.body.style.cursor = "initial";
selectRecurrence.style.cursor = "initial";
connectorSelect.style.cursor = "initial";
searchPublicationQuery.style.cursor = "initial";
}); });
} }
@ -169,6 +180,7 @@ function ShowPublicationViewerWindow(publicationId, event, add){
//Edit information inside the window //Edit information inside the window
var publication = publications.filter(pub => pub.internalId === publicationId)[0]; var publication = publications.filter(pub => pub.internalId === publicationId)[0];
publicationViewerName.innerText = publication.sortName; publicationViewerName.innerText = publication.sortName;
publicationViewerTags.innerText = publication.tags.join(", ");
publicationViewerDescription.innerText = publication.description; publicationViewerDescription.innerText = publication.description;
publicationViewerAuthor.innerText = publication.author; publicationViewerAuthor.innerText = publication.author;
pubviewcover.src = `imageCache/${publication.coverFileNameInCache}`; pubviewcover.src = `imageCache/${publication.coverFileNameInCache}`;
@ -230,8 +242,11 @@ function GetSettingsClick(){
GetSettings().then(json => { GetSettings().then(json => {
settingDownloadLocation.innerText = json.downloadLocation; settingDownloadLocation.innerText = json.downloadLocation;
if(json.komga != null) if(json.komga != null) {
settingKomgaUrl.placeholder = json.komga.baseUrl; settingKomgaUrl.placeholder = json.komga.baseUrl;
settingKomgaUser.placeholder = "Configured";
settingKomgaPass.placeholder = "***";
}
}); });
GetKomgaTask().then(json => { GetKomgaTask().then(json => {
@ -296,7 +311,6 @@ function ShowQueuedTasks(event){
} }
}); });
} }
function ShowAllTasks(event){ function ShowAllTasks(event){
GetDownloadTasks() GetDownloadTasks()
.then(json => { .then(json => {

View File

@ -53,6 +53,7 @@ titlebox {
} }
titlebox span{ titlebox span{
cursor: default;
font-size: 24pt; font-size: 24pt;
font-weight: bold; font-weight: bold;
background: linear-gradient(150deg, var(--primary-color), var(--accent-color)); background: linear-gradient(150deg, var(--primary-color), var(--accent-color));
@ -64,6 +65,7 @@ titlebox span{
titlebox img { titlebox img {
height: 100%; height: 100%;
margin-right: 10px; margin-right: 10px;
cursor: grab;
} }
spacer{ spacer{
@ -119,6 +121,7 @@ footer > div {
flex-direction: row; flex-direction: row;
flex-wrap: nowrap; flex-wrap: nowrap;
align-items: center; align-items: center;
cursor: pointer;
} }
footer > div > *{ footer > div > *{
@ -130,6 +133,7 @@ footer > div > *{
flex-grow: 1; flex-grow: 1;
text-align: right; text-align: right;
margin-right: 20px; margin-right: 20px;
cursor: url("media/blahaj.png"), grab;
} }
content { content {
@ -376,14 +380,13 @@ addtask-settings addtask-setting{
publication-viewer{ publication-viewer{
display: block; display: block;
width: 450px; width: 450px;
height: 300px;
position: absolute; position: absolute;
top: 200px; top: 200px;
left: 400px; left: 400px;
background-color: var(--accent-color); background-color: var(--accent-color);
border-radius: 5px; border-radius: 5px;
overflow: hidden; overflow: hidden;
padding: 30px; padding: 15px;
} }
publication-viewer::after{ publication-viewer::after{
@ -391,7 +394,8 @@ publication-viewer::after{
position: absolute; position: absolute;
left: 0; top: 0; left: 0; top: 0;
border-radius: 5px; border-radius: 5px;
width: 100%; height: 100%; width: 100%;
height: 100%;
background: rgba(0,0,0,0.8); background: rgba(0,0,0,0.8);
backdrop-filter: blur(3px); backdrop-filter: blur(3px);
} }
@ -407,16 +411,32 @@ publication-viewer img {
z-index: 0; z-index: 0;
} }
publication-viewer publication-information publication-name{ publication-viewer publication-information > * {
margin: 5px 0; margin: 5px 0;
} }
publication-viewer publication-information publication-author { publication-viewer publication-information publication-name {
margin: 5px 0; width: initial;
overflow-x: scroll;
white-space: nowrap;
scrollbar-width: none;
}
publication-viewer publication-information publication-tags::before {
content: "Tags";
display: block;
font-weight: bolder;
}
publication-viewer publication-information publication-tags {
overflow-x: scroll;
white-space: nowrap;
scrollbar-width: none;
} }
publication-viewer publication-information publication-author::before { publication-viewer publication-information publication-author::before {
content: "Author: "; content: "Author: ";
font-weight: bolder;
} }
publication-viewer publication-information publication-description::before { publication-viewer publication-information publication-description::before {
@ -428,24 +448,22 @@ publication-viewer publication-information publication-description::before {
publication-viewer publication-information publication-description { publication-viewer publication-information publication-description {
font-size: 12pt; font-size: 12pt;
margin: 5px 0; margin: 5px 0;
max-height: 200px; height: 145px;
overflow-x: scroll; overflow-x: scroll;
} }
publication-viewer publication-information publication-interactions { publication-viewer publication-information publication-interactions {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
position: absolute;
justify-content: end; justify-content: end;
align-items: start; align-items: start;
bottom: 0;
left: 0;
width: 100%; width: 100%;
} }
publication-viewer publication-information publication-interactions > * { publication-viewer publication-information publication-interactions > * {
margin: 0 10px 10px 10px; margin: 0 10px;
font-size: 16pt; font-size: 16pt;
cursor: pointer;
} }
publication-viewer publication-information publication-interactions publication-starttask { publication-viewer publication-information publication-interactions publication-starttask {

View File

@ -1,4 +1,5 @@
services: version: '3'
services:
tranga-api: tranga-api:
image: glax/tranga-api:latest image: glax/tranga-api:latest
container_name: tranga-api container_name: tranga-api