64 Commits

Author SHA1 Message Date
8b99a98e24 Merge pull request 'api-testing' (#5) from api-testing into master
Reviewed-on: #5
2023-05-21 22:04:06 +02:00
cf171d5c38 Bring CLI in line with new Methods 2023-05-21 22:02:35 +02:00
6d49b4b934 Swagger 2023-05-21 22:02:19 +02:00
b55d2a2d06 no duplicate keys 2023-05-21 22:02:05 +02:00
737eebf599 bring /settings/update in line with new methods 2023-05-21 22:01:56 +02:00
aef01b684c Fixed null on settings.komga 2023-05-21 22:01:40 +02:00
53bff61174 Added Swagger 2023-05-21 22:01:28 +02:00
431a602a40 Added Method UpdateSettings to SettingsData
Added Method UpdateSettings to TaskManager (to export data after update)
2023-05-21 22:01:04 +02:00
9afb81cee2 string and json 2023-05-21 21:24:18 +02:00
ea69b355b5 No duplicate keys 2023-05-21 21:24:04 +02:00
84dbc36bbf dont add duplicates 2023-05-21 21:23:51 +02:00
455c87b2e1 New API 2023-05-21 21:12:32 +02:00
df991e3da6 Remove APi for testing 2023-05-21 17:59:24 +02:00
13c96fd09a Create Appdata Directories for API 2023-05-21 16:51:14 +02:00
6f1a6a43ee API: Edit Settings 2023-05-21 16:49:55 +02:00
e2afc09c4a API: GetSettings 2023-05-21 16:46:34 +02:00
e9db7cfacc API: List Settings 2023-05-21 16:41:02 +02:00
755167c39a API: StartTask
API: Get Task Queue
API: Task Enqueue
API: Task Dequeue
2023-05-21 16:39:54 +02:00
1cff93fbac Use settings-file for API
Added API-call to list TaskTypes
Working? CreateTask API-call
Working? RemoveTask API-call
2023-05-21 16:23:35 +02:00
6c775d6e0c Moved check into if statement 2023-05-21 16:22:40 +02:00
876b1ab78b Added internalId to Publication 2023-05-21 16:22:14 +02:00
a321ecb1bc string 2023-05-21 15:36:12 +02:00
674c8fc37b FIX Bug where menu wouldnt work 2023-05-21 15:34:59 +02:00
e24652b83e Added logfile-count limit 2023-05-21 15:33:01 +02:00
5dee13c402 FIX bug with incorrect importPath 2023-05-21 15:26:53 +02:00
942a552c8e Reduced update time for more responsiveness in CLI
Added statement "Exiting." when exiting for feedback to userinput.
2023-05-21 15:26:29 +02:00
b5bd5d6126 Fixed some bugs relating to new Filepath of Applicationdata 2023-05-21 15:14:25 +02:00
715cf1f4f3 Use SettingsData in TaskManager 2023-05-21 15:05:53 +02:00
168bf5a358 Made CLI auto-update on menu screen (task count)
And tail the logfile
2023-05-21 14:44:33 +02:00
636d17d287 Only list tasks that are not already running when asking to execute now. 2023-05-21 03:21:34 +02:00
294b819ff0 Created SelectTask menu
Created method to enqueue task
Added option to enqueue task to CLI
2023-05-21 03:18:56 +02:00
d763610383 Menu formatting 2023-05-21 03:08:36 +02:00
2910473fec Only list tasks that are enqueued when showing remove task menu 2023-05-21 03:06:50 +02:00
ca2d13226f Menu formatting 2023-05-21 03:05:29 +02:00
95c65c981e Added "Remove task from queue"-Menu
Added "Remove task from queue" to TaskManager

Better naming for deleting tasks and the taskqueue
2023-05-21 03:04:32 +02:00
e72efa3731 Corrected string 2023-05-21 02:18:39 +02:00
597eedb6d4 Added menu to show loglines 2023-05-21 02:17:38 +02:00
8829132046 Cleanup code 2023-05-21 02:13:19 +02:00
32467191f6 Added New CLI Options to list enqueued task and view last 20 loglines 2023-05-21 02:11:47 +02:00
fe52d2c3b5 Always create and use MemoryLogger 2023-05-21 02:10:32 +02:00
554f6b4acc TaskCheckerThread new logic 2023-05-21 01:58:24 +02:00
9d0fc18051 Delete old data.json 2023-05-21 01:58:07 +02:00
e02b00e0ef Better/More logging 2023-05-21 01:57:56 +02:00
06a8e4e895 Make caller right aligned 2023-05-21 01:57:18 +02:00
a557f8cab5 Export Data when starting new task 2023-05-20 23:12:15 +02:00
e564be08f5 Search query length now at least 4 characters 2023-05-20 23:08:16 +02:00
b8bf7bdf30 "Fixed" Issue with Filelogger, where program would crash if file could not be written 2023-05-20 22:56:05 +02:00
d6af014cb7 string 2023-05-20 22:43:39 +02:00
2dcaaf4d66 Merge remote-tracking branch 'origin/master'
# Conflicts:
#	Tranga/TaskManager.cs
2023-05-20 22:21:26 +02:00
e3ec5420c0 Fixed bug for enqueued tasks constantly being triggered to execute 2023-05-20 22:21:00 +02:00
5d66bce5b6 Fixed bug for enqueued tasks constantly being triggered to execute 2023-05-20 22:15:31 +02:00
07ae4af209 Changed Log message to long timestring 2023-05-20 22:13:25 +02:00
d62b0bdf34 Changed Logger to accept string as caller
Added Logger to all relevant methods
2023-05-20 22:10:24 +02:00
a367ebb230 Use Logger to log CLI-Inputs 2023-05-20 21:48:08 +02:00
4d3861d31b Created Logger 2023-05-20 21:47:54 +02:00
497ec74b9a Readme 2023-05-20 17:48:53 +02:00
18b5d17994 Merge remote-tracking branch 'origin/master'
# Conflicts:
#	Tranga-CLI/Tranga_Cli.cs
2023-05-20 17:19:17 +02:00
1916018fba Some work on API-side 2023-05-20 17:18:22 +02:00
a6a2d20981 More fancy CLI 2023-05-20 16:35:45 +02:00
1449292e53 More fancy CLI 2023-05-20 16:35:08 +02:00
67f3695be8 CLI: When listing Task add headers for values 2023-05-20 16:27:30 +02:00
086d72565a Formatting of trangaTask string with fixed-with instead of tabs 2023-05-20 16:23:25 +02:00
e54e83c2ae Moved "Press any key" 2023-05-20 16:15:17 +02:00
73f19c3989 Clear console when aborting. 2023-05-20 16:13:19 +02:00
21 changed files with 825 additions and 258 deletions

32
Logging/FileLogger.cs Normal file
View File

@ -0,0 +1,32 @@
using System.Text;
using System.Text.Json.Serialization;
namespace Logging;
public class FileLogger : LoggerBase
{
private string logFilePath { get; }
private const int MaxNumberOfLogFiles = 5;
public FileLogger(string logFilePath, TextWriter? stdOut, Encoding? encoding = null) : base (stdOut, encoding)
{
this.logFilePath = logFilePath;
//Remove oldest logfile if more than MaxNumberOfLogFiles
string parentFolderPath = Path.GetDirectoryName(logFilePath)!;
for (int fileCount = new DirectoryInfo(parentFolderPath).EnumerateFiles().Count(); fileCount > MaxNumberOfLogFiles - 1; fileCount--) //-1 because we create own logfile later
File.Delete(new DirectoryInfo(parentFolderPath).EnumerateFiles().MinBy(file => file.LastWriteTime)!.FullName);
}
protected override void Write(LogMessage logMessage)
{
try
{
File.AppendAllText(logFilePath, logMessage.ToString());
}
catch (Exception e)
{
stdOut?.WriteLine(e);
}
}
}

View File

@ -0,0 +1,17 @@
using System.Text;
namespace Logging;
public class FormattedConsoleLogger : LoggerBase
{
public FormattedConsoleLogger(TextWriter? stdOut, Encoding? encoding = null) : base(stdOut, encoding)
{
}
protected override void Write(LogMessage message)
{
//Nothing to do yet
}
}

63
Logging/Logger.cs Normal file
View File

@ -0,0 +1,63 @@
using System.Net.Mime;
using System.Text;
namespace Logging;
public class Logger : TextWriter
{
public override Encoding Encoding { get; }
public enum LoggerType
{
FileLogger,
ConsoleLogger
}
private FileLogger? _fileLogger;
private FormattedConsoleLogger? _formattedConsoleLogger;
private MemoryLogger _memoryLogger;
private TextWriter? stdOut;
public Logger(LoggerType[] enabledLoggers, TextWriter? stdOut, Encoding? encoding, string? logFilePath)
{
this.Encoding = encoding ?? Encoding.ASCII;
this.stdOut = stdOut ?? null;
if (enabledLoggers.Contains(LoggerType.FileLogger) && logFilePath is not null)
_fileLogger = new FileLogger(logFilePath, null, encoding);
else
{
_fileLogger = null;
throw new ArgumentException($"logFilePath can not be null for LoggerType {LoggerType.FileLogger}");
}
_formattedConsoleLogger = enabledLoggers.Contains(LoggerType.ConsoleLogger) ? new FormattedConsoleLogger(null, encoding) : null;
_memoryLogger = new MemoryLogger(null, encoding);
}
public void WriteLine(string caller, string? value)
{
value = value is null ? Environment.NewLine : string.Concat(value, Environment.NewLine);
Write(caller, value);
}
public void Write(string caller, string? value)
{
if (value is null)
return;
_fileLogger?.Write(caller, value);
_formattedConsoleLogger?.Write(caller, value);
_memoryLogger.Write(caller, value);
stdOut?.Write(value);
}
public string[] Tail(uint? lines)
{
return _memoryLogger.Tail(lines);
}
public string[] GetNewLines()
{
return _memoryLogger.GetNewLines();
}
}

58
Logging/LoggerBase.cs Normal file
View File

@ -0,0 +1,58 @@
using System.Text;
namespace Logging;
public abstract class LoggerBase : TextWriter
{
public override Encoding Encoding { get; }
protected TextWriter? stdOut { get; }
public LoggerBase(TextWriter? stdOut, Encoding? encoding = null)
{
this.Encoding = encoding ?? Encoding.ASCII;
this.stdOut = stdOut;
}
public void WriteLine(string caller, string? value)
{
value = value is null ? Environment.NewLine : string.Join(value, Environment.NewLine);
LogMessage message = new LogMessage(DateTime.Now, caller, value);
Write(message);
}
public void Write(string caller, string? value)
{
if (value is null)
return;
LogMessage message = new LogMessage(DateTime.Now, caller, value);
stdOut?.Write(message.ToString());
Write(message);
}
protected abstract void Write(LogMessage message);
public class LogMessage
{
public DateTime logTime { get; }
public string caller { get; }
public string value { get; }
public LogMessage(DateTime now, string caller, string value)
{
this.logTime = now;
this.caller = caller;
this.value = value;
}
public override string ToString()
{
string dateTimeString = $"{logTime.ToShortDateString()} {logTime.ToLongTimeString()}";
return $"[{dateTimeString}] {caller,30} | {value}";
}
}
}

9
Logging/Logging.csproj Normal file
View File

@ -0,0 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>

57
Logging/MemoryLogger.cs Normal file
View File

@ -0,0 +1,57 @@
using System.Text;
namespace Logging;
public class MemoryLogger : LoggerBase
{
private readonly SortedList<DateTime, LogMessage> _logMessages = new();
private int _lastLogMessageIndex = 0;
public MemoryLogger(TextWriter? stdOut, Encoding? encoding = null) : base(stdOut, encoding)
{
}
protected override void Write(LogMessage value)
{
_logMessages.Add(value.logTime, value);
}
public string[] GetLogMessage()
{
return Tail(Convert.ToUInt32(_logMessages.Count));
}
public string[] Tail(uint? length)
{
int retLength;
if (length is null || length > _logMessages.Count)
retLength = _logMessages.Count;
else
retLength = (int)length;
string[] ret = new string[retLength];
for (int retIndex = 0; retIndex < ret.Length; retIndex++)
{
ret[retIndex] = _logMessages.GetValueAtIndex(_logMessages.Count - retLength + retIndex).ToString();
}
_lastLogMessageIndex = _logMessages.Count - 1;
return ret;
}
public string[] GetNewLines()
{
int logMessageCount = _logMessages.Count;
string[] ret = new string[logMessageCount - _lastLogMessageIndex];
for (int retIndex = 0; retIndex < ret.Length; retIndex++)
{
ret[retIndex] = _logMessages.GetValueAtIndex(_lastLogMessageIndex + retIndex).ToString();
}
_lastLogMessageIndex = logMessageCount;
return ret;
}
}

View File

@ -56,11 +56,9 @@ and automatically start updates in [Komga](https://komga.org/) to import them.
### Inspiration: ### Inspiration:
Because [Kaizoku](https://github.com/oae/kaizoku) was relying on [mangal](https://github.com/metafates/mangal) on hasn't Because [Kaizoku](https://github.com/oae/kaizoku) was relying on [mangal](https://github.com/metafates/mangal) and mangal
received bugfixes for it's issues with Titles not showing up, or throwing errors because of illegal characters, there hasn't received bugfixes for it's issues with Titles not showing up, or throwing errors because of illegal characters,
were no alternatives for automatic downloads. there were no alternatives for automatic downloads. However [Kaizoku](https://github.com/oae/kaizoku) certainly had a great Web-UI.
[Kaizoku](https://github.com/oae/kaizoku) certainly had a great Web-UI, but just wasn't working.
That is why I wanted to create my own project, in a language I understand, and that I am able to maintain myself. That is why I wanted to create my own project, in a language I understand, and that I am able to maintain myself.
@ -135,8 +133,6 @@ Distributed under the GNU GPLv3 License. See `LICENSE.txt` for more information
<!-- ACKNOWLEDGMENTS --> <!-- ACKNOWLEDGMENTS -->
## Acknowledgments ## Acknowledgments
Use this space to list resources you find helpful and would like to give credit to. I've included a few of my favorites to kick things off!
* [Choose an Open Source License](https://choosealicense.com) * [Choose an Open Source License](https://choosealicense.com)
* [Font Awesome](https://fontawesome.com) * [Font Awesome](https://fontawesome.com)
* [Best-README-Template](https://github.com/othneildrew/Best-README-Template/tree/master) * [Best-README-Template](https://github.com/othneildrew/Best-README-Template/tree/master)

View File

@ -1,49 +1,122 @@
using System.Text.Json;
using Logging;
using Tranga; using Tranga;
using Tranga.Connectors;
TaskManager taskManager = new TaskManager(Directory.GetCurrentDirectory()); string applicationFolderPath = Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "Tranga-API");
var builder = WebApplication.CreateBuilder(args); string logsFolderPath = Path.Join(applicationFolderPath, "logs");
var app = builder.Build(); string logFilePath = Path.Join(logsFolderPath, $"log-{DateTime.Now:dd-M-yyyy-HH-mm-ss}.txt");
string settingsFilePath = Path.Join(applicationFolderPath, "data.json");
app.MapGet("/GetConnectors", () => JsonSerializer.Serialize(taskManager.GetAvailableConnectors().Values.ToArray())); Directory.CreateDirectory(applicationFolderPath);
Directory.CreateDirectory(logsFolderPath);
app.MapGet("/GetPublications", (string connectorName, string? title) => Console.WriteLine($"Logfile-Path: {logFilePath}");
{ Console.WriteLine($"Settings-File-Path: {settingsFilePath}");
Connector connector = taskManager.GetConnector(connectorName);
Publication[] publications; Logger logger = new(new[] { Logger.LoggerType.FileLogger }, null, null, logFilePath);
if (title is not null)
publications = connector.GetPublications(title); logger.WriteLine("Tranga_CLI", "Loading Taskmanager.");
TaskManager.SettingsData settings;
if (File.Exists(settingsFilePath))
settings = TaskManager.LoadData(settingsFilePath);
else else
publications = connector.GetPublications(); settings = new TaskManager.SettingsData(Directory.GetCurrentDirectory(), settingsFilePath, null, new HashSet<TrangaTask>());
return JsonSerializer.Serialize(publications); TaskManager taskManager = new (settings, logger);
});
app.MapGet("/ListTasks", () => JsonSerializer.Serialize(taskManager.GetAllTasks())); var builder = WebApplication.CreateBuilder(args);
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddControllers().AddNewtonsoftJson();
var app = builder.Build();
app.UseSwagger();
app.UseSwaggerUI();
app.UseSwagger();
app.UseSwaggerUI();
app.MapGet("/CreateTask", app.MapGet("/GetAvailableControllers", () => taskManager.GetAvailableConnectors());
(TrangaTask.Task task, string connectorName, string? publicationName, TimeSpan reoccurrence, string language) =>
app.MapGet("/GetKnownPublications", () => taskManager.GetAllPublications());
app.MapGet("/GetPublicationsFromConnector", (string connectorName, string title) =>
{ {
Publication? publication = Connector? connector = taskManager.GetAvailableConnectors().FirstOrDefault(con => con.Key == connectorName).Value;
taskManager.GetAllPublications().FirstOrDefault(pub => pub.downloadUrl == publicationName); if (connector is null)
if (publication is null) return Array.Empty<Publication>();
JsonSerializer.Serialize($"Publication {publicationName} is unknown."); if(title.Length < 4)
return Array.Empty<Publication>();
taskManager.AddTask(task, connectorName, publication, reoccurrence, language); return taskManager.GetPublicationsFromConnector(connector, title);
JsonSerializer.Serialize("Success");
}); });
app.MapGet("/RemoveTask", (TrangaTask.Task task, string connector, string? publicationName) => app.MapGet("/Tasks/GetTaskTypes", () => Enum.GetNames(typeof(TrangaTask.Task)));
app.MapPost("/Tasks/Create", (string taskType, string? connectorName, string? publicationId, string reoccurrenceTime, string? language) =>
{ {
Publication? publication = Publication? publication = taskManager.GetAllPublications().FirstOrDefault(pub => pub.internalId == publicationId);
taskManager.GetAllPublications().FirstOrDefault(pub => pub.downloadUrl == publicationName); TrangaTask.Task task = Enum.Parse<TrangaTask.Task>(taskType);
if (publication is null) taskManager.AddTask(task, connectorName, publication, TimeSpan.Parse(reoccurrenceTime), language??"");
JsonSerializer.Serialize($"Publication {publicationName} is unknown.");
taskManager.RemoveTask(task, connector, publication);
JsonSerializer.Serialize("Success");
}); });
app.MapPost("/Tasks/Delete", (string taskType, string? connectorName, string? publicationId) =>
{
Publication? publication = taskManager.GetAllPublications().FirstOrDefault(pub => pub.internalId == publicationId);
TrangaTask.Task task = Enum.Parse<TrangaTask.Task>(taskType);
taskManager.DeleteTask(task, connectorName, publication);
});
app.MapGet("/Tasks/GetList", () => taskManager.GetAllTasks());
app.MapPost("/Tasks/Start", (string taskType, string? connectorName, string? publicationId) =>
{
TrangaTask.Task pTask = Enum.Parse<TrangaTask.Task>(taskType);
TrangaTask? task = taskManager.GetAllTasks().FirstOrDefault(tTask =>
tTask.task == pTask && tTask.publication?.internalId == publicationId && tTask.connectorName == connectorName);
if (task is null)
return;
taskManager.ExecuteTaskNow(task);
});
app.MapGet("/Tasks/GetRunningTasks",
() => taskManager.GetAllTasks().Where(task => task.state is TrangaTask.ExecutionState.Running));
app.MapGet("/Queue/GetList",
() => taskManager.GetAllTasks().Where(task => task.state is TrangaTask.ExecutionState.Enqueued));
app.MapPost("/Queue/Enqueue", (string taskType, string? connectorName, string? publicationId) =>
{
TrangaTask.Task pTask = Enum.Parse<TrangaTask.Task>(taskType);
TrangaTask? task = taskManager.GetAllTasks().FirstOrDefault(tTask =>
tTask.task == pTask && tTask.publication?.internalId == publicationId && tTask.connectorName == connectorName);
if (task is null)
return;
taskManager.AddTaskToQueue(task);
});
app.MapPost("/Queue/Dequeue", (string taskType, string? connectorName, string? publicationId) =>
{
TrangaTask.Task pTask = Enum.Parse<TrangaTask.Task>(taskType);
TrangaTask? task = taskManager.GetAllTasks().FirstOrDefault(tTask =>
tTask.task == pTask && tTask.publication?.internalId == publicationId && tTask.connectorName == connectorName);
if (task is null)
return;
taskManager.RemoveTaskFromQueue(task);
});
app.MapGet("/Settings/Get", () => new Settings(taskManager.settings));
app.MapPost("/Settings/Update", (string? downloadLocation, string? komgaUrl, string? komgaAuth) => taskManager.UpdateSettings(downloadLocation, komgaUrl, komgaAuth) );
app.Run(); app.Run();
class Settings
{
public string downloadLocation { get; }
public Komga? komga { get; }
public Settings(TaskManager.SettingsData settings)
{
this.downloadLocation = settings.downloadLocation;
this.komga = settings.komga;
}
}

View File

@ -3,8 +3,8 @@
"windowsAuthentication": false, "windowsAuthentication": false,
"anonymousAuthentication": true, "anonymousAuthentication": true,
"iisExpress": { "iisExpress": {
"applicationUrl": "http://localhost:14826", "applicationUrl": "http://localhost:1716",
"sslPort": 44333 "sslPort": 44391
} }
}, },
"profiles": { "profiles": {
@ -12,7 +12,7 @@
"commandName": "Project", "commandName": "Project",
"dotnetRunMessages": true, "dotnetRunMessages": true,
"launchBrowser": true, "launchBrowser": true,
"applicationUrl": "http://localhost:5119", "applicationUrl": "http://localhost:5177",
"environmentVariables": { "environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development" "ASPNETCORE_ENVIRONMENT": "Development"
} }
@ -21,7 +21,7 @@
"commandName": "Project", "commandName": "Project",
"dotnetRunMessages": true, "dotnetRunMessages": true,
"launchBrowser": true, "launchBrowser": true,
"applicationUrl": "https://localhost:7070;http://localhost:5119", "applicationUrl": "https://localhost:7036;http://localhost:5177",
"environmentVariables": { "environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development" "ASPNETCORE_ENVIRONMENT": "Development"
} }

View File

@ -15,7 +15,14 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Logging\Logging.csproj" />
<ProjectReference Include="..\Tranga\Tranga.csproj" /> <ProjectReference Include="..\Tranga\Tranga.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="7.0.5" />
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="7.0.6" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
</ItemGroup>
</Project> </Project>

View File

@ -1 +0,0 @@
[{"reoccurrence":"00:00:00","lastExecuted":"2023-05-19T17:34:40.5349215+02:00","connectorName":"MangaDex","task":0,"publication":{"sortName":null,"description":null,"tags":null,"posterUrl":null,"year":null,"originalLanguage":null,"status":null,"folderName":null,"downloadUrl":null},"language":"en"}]

View File

@ -1,6 +1,6 @@
using System.Globalization; using System.Globalization;
using Logging;
using Tranga; using Tranga;
using Tranga.Connectors;
namespace Tranga_CLI; namespace Tranga_CLI;
@ -14,20 +14,34 @@ public static class Tranga_Cli
{ {
public static void Main(string[] args) public static void Main(string[] args)
{ {
string applicationFolderPath = Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "Tranga");
string logsFolderPath = Path.Join(applicationFolderPath, "logs");
string logFilePath = Path.Join(logsFolderPath, $"log-{DateTime.Now:dd-M-yyyy-HH-mm-ss}.txt");
string settingsFilePath = Path.Join(applicationFolderPath, "data.json");
Directory.CreateDirectory(applicationFolderPath);
Directory.CreateDirectory(logsFolderPath);
Console.WriteLine($"Logfile-Path: {logFilePath}");
Console.WriteLine($"Settings-File-Path: {settingsFilePath}");
Logger logger = new(new[] { Logger.LoggerType.FileLogger }, null, null, logFilePath);
logger.WriteLine("Tranga_CLI", "Loading Taskmanager.");
TaskManager.SettingsData settings; TaskManager.SettingsData settings;
string settingsPath = Path.Join(Directory.GetCurrentDirectory(), "data.json"); if (File.Exists(settingsFilePath))
if (File.Exists(settingsPath)) settings = TaskManager.LoadData(settingsFilePath);
settings = TaskManager.LoadData(Directory.GetCurrentDirectory());
else else
settings = new TaskManager.SettingsData(Directory.GetCurrentDirectory(), null, new HashSet<TrangaTask>()); settings = new TaskManager.SettingsData(Directory.GetCurrentDirectory(), settingsFilePath, null, new HashSet<TrangaTask>());
logger.WriteLine("Tranga_CLI", "User Input");
Console.WriteLine($"Output folder path [{settings.downloadLocation}]:"); Console.WriteLine($"Output folder path [{settings.downloadLocation}]:");
string? tmpPath = Console.ReadLine(); string? tmpPath = Console.ReadLine();
while(tmpPath is null) while(tmpPath is null)
tmpPath = Console.ReadLine(); tmpPath = Console.ReadLine();
if(tmpPath.Length > 0) if(tmpPath.Length > 0)
settings.downloadLocation = tmpPath; settings.UpdateSettings(pDownloadLocation: tmpPath, null);
Console.WriteLine($"Komga BaseURL [{settings.komga?.baseUrl}]:"); Console.WriteLine($"Komga BaseURL [{settings.komga?.baseUrl}]:");
string? tmpUrl = Console.ReadLine(); string? tmpUrl = Console.ReadLine();
@ -60,44 +74,94 @@ public static class Tranga_Cli
} }
} while (key != ConsoleKey.Enter); } while (key != ConsoleKey.Enter);
settings.komga = new Komga(tmpUrl, tmpUser, tmpPass); settings.UpdateSettings(null, new Komga(tmpUrl, tmpUser, tmpPass, logger));
} }
TaskMode(settings); logger.WriteLine("Tranga_CLI", "Loaded.");
TaskMode(settings, logger);
} }
private static void TaskMode(TaskManager.SettingsData settings) private static void TaskMode(TaskManager.SettingsData settings, Logger logger)
{ {
TaskManager taskManager = new (settings); TaskManager taskManager = new (settings, logger);
ConsoleKey selection = PrintMenu(taskManager, settings.downloadLocation); ConsoleKey selection = ConsoleKey.EraseEndOfFile;
PrintMenu(taskManager, taskManager.settings.downloadLocation, logger);
while (selection != ConsoleKey.Q) while (selection != ConsoleKey.Q)
{ {
int taskCount = taskManager.GetAllTasks().Length;
int taskRunningCount = taskManager.GetAllTasks().Count(task => task.state == TrangaTask.ExecutionState.Running);
int taskEnqueuedCount =
taskManager.GetAllTasks().Count(task => task.state == TrangaTask.ExecutionState.Enqueued);
Console.SetCursorPosition(0,1);
Console.WriteLine($"Tasks (Running/Queue/Total)): {taskRunningCount}/{taskEnqueuedCount}/{taskCount}");
if (Console.KeyAvailable)
{
selection = Console.ReadKey().Key;
switch (selection) switch (selection)
{ {
case ConsoleKey.L: case ConsoleKey.L:
PrintTasks(taskManager.GetAllTasks()); PrintTasks(taskManager.GetAllTasks(), logger);
break;
case ConsoleKey.C:
CreateTask(taskManager, settings);
break;
case ConsoleKey.D:
RemoveTask (taskManager);
break;
case ConsoleKey.E:
ExecuteTaskNow(taskManager);
break;
case ConsoleKey.S:
SearchTasks(taskManager);
break;
case ConsoleKey.R:
PrintTasks(taskManager.GetAllTasks().Where(eTask => eTask.state == TrangaTask.ExecutionState.Running).ToArray());
break;
}
Console.WriteLine("Press any key."); Console.WriteLine("Press any key.");
Console.ReadKey(); Console.ReadKey();
selection = PrintMenu(taskManager, settings.downloadLocation); break;
case ConsoleKey.C:
CreateTask(taskManager, taskManager.settings, logger);
Console.WriteLine("Press any key.");
Console.ReadKey();
break;
case ConsoleKey.D:
DeleteTask(taskManager, logger);
Console.WriteLine("Press any key.");
Console.ReadKey();
break;
case ConsoleKey.E:
ExecuteTaskNow(taskManager, logger);
Console.WriteLine("Press any key.");
Console.ReadKey();
break;
case ConsoleKey.S:
SearchTasks(taskManager, logger);
Console.WriteLine("Press any key.");
Console.ReadKey();
break;
case ConsoleKey.R:
PrintTasks(
taskManager.GetAllTasks().Where(eTask => eTask.state == TrangaTask.ExecutionState.Running)
.ToArray(), logger);
Console.WriteLine("Press any key.");
Console.ReadKey();
break;
case ConsoleKey.K:
PrintTasks(
taskManager.GetAllTasks().Where(qTask => qTask.state is TrangaTask.ExecutionState.Enqueued)
.ToArray(), logger);
Console.WriteLine("Press any key.");
Console.ReadKey();
break;
case ConsoleKey.F:
TailLog(logger);
Console.ReadKey();
break;
case ConsoleKey.M:
RemoveTaskFromQueue(taskManager, logger);
Console.WriteLine("Press any key.");
Console.ReadKey();
break;
case ConsoleKey.B:
AddTaskToQueue(taskManager, logger);
Console.WriteLine("Press any key.");
Console.ReadKey();
break;
}
PrintMenu(taskManager, taskManager.settings.downloadLocation, logger);
}
Thread.Sleep(200);
} }
logger.WriteLine("Tranga_CLI", "Exiting.");
Console.Clear();
Console.WriteLine("Exiting.");
if (taskManager.GetAllTasks().Any(task => task.state == TrangaTask.ExecutionState.Running)) if (taskManager.GetAllTasks().Any(task => task.state == TrangaTask.ExecutionState.Running))
{ {
Console.WriteLine("Force quit (Even with running tasks?) y/N"); Console.WriteLine("Force quit (Even with running tasks?) y/N");
@ -110,42 +174,136 @@ public static class Tranga_Cli
taskManager.Shutdown(false); taskManager.Shutdown(false);
} }
private static ConsoleKey PrintMenu(TaskManager taskManager, string folderPath) private static void PrintMenu(TaskManager taskManager, string folderPath, Logger logger)
{ {
int taskCount = taskManager.GetAllTasks().Length; int taskCount = taskManager.GetAllTasks().Length;
int taskRunningCount = taskManager.GetAllTasks().Count(task => task.state == TrangaTask.ExecutionState.Running); int taskRunningCount = taskManager.GetAllTasks().Count(task => task.state == TrangaTask.ExecutionState.Running);
int taskEnqueuedCount = int taskEnqueuedCount =
taskManager.GetAllTasks().Count(task => task.state == TrangaTask.ExecutionState.Enqueued); taskManager.GetAllTasks().Count(task => task.state == TrangaTask.ExecutionState.Enqueued);
Console.Clear(); Console.Clear();
Console.WriteLine($"Download Folder: {folderPath} Tasks (Running/Queue/Total): {taskRunningCount}/{taskEnqueuedCount}/{taskCount}"); Console.WriteLine($"Download Folder: {folderPath}");
Console.WriteLine("U: Update this Screen"); Console.WriteLine($"Tasks (Running/Queue/Total)): {taskRunningCount}/{taskEnqueuedCount}/{taskCount}");
Console.WriteLine("L: List tasks");
Console.WriteLine("C: Create Task");
Console.WriteLine("D: Delete Task");
Console.WriteLine("E: Execute Task now");
Console.WriteLine("S: Search Task");
Console.WriteLine("R: Running Tasks");
Console.WriteLine("Q: Exit");
ConsoleKey selection = Console.ReadKey().Key;
Console.WriteLine(); Console.WriteLine();
return selection; Console.WriteLine($"{"C: Create Task",-30}{"L: List tasks",-30}{"B: Enqueue Task", -30}");
Console.WriteLine($"{"D: Delete Task",-30}{"S: Search Tasks", -30}{"K: List Task Queue", -30}");
Console.WriteLine($"{"E: Execute Task now",-30}{"R: List Running Tasks", -30}{"M: Remove Task from Queue", -30}");
Console.WriteLine();
Console.WriteLine($"{"",-30}{"F: Show Log",-30}{"Q: Exit",-30}");
} }
private static void PrintTasks(TrangaTask[] tasks) private static void PrintTasks(TrangaTask[] tasks, Logger logger)
{ {
logger.WriteLine("Tranga_CLI", "Printing Tasks");
int taskCount = tasks.Length; int taskCount = tasks.Length;
int taskRunningCount = tasks.Count(task => task.state == TrangaTask.ExecutionState.Running); int taskRunningCount = tasks.Count(task => task.state == TrangaTask.ExecutionState.Running);
int taskEnqueuedCount = tasks.Count(task => task.state == TrangaTask.ExecutionState.Enqueued); int taskEnqueuedCount = tasks.Count(task => task.state == TrangaTask.ExecutionState.Enqueued);
Console.Clear(); Console.Clear();
int tIndex = 0; int tIndex = 0;
Console.WriteLine($"Tasks (Running/Queue/Total): {taskRunningCount}/{taskEnqueuedCount}/{taskCount}"); Console.WriteLine($"Tasks (Running/Queue/Total): {taskRunningCount}/{taskEnqueuedCount}/{taskCount}");
string header =
$"{"",-5}{"Task",-20} | {"Last Executed",-20} | {"Reoccurrence",-12} | {"State",-10} | {"Connector",-15} | Publication/Manga";
Console.WriteLine(header);
Console.WriteLine(new string('-', header.Length));
foreach(TrangaTask trangaTask in tasks) foreach(TrangaTask trangaTask in tasks)
Console.WriteLine($"{tIndex++:000}: {trangaTask}"); Console.WriteLine($"{tIndex++:000}: {trangaTask}");
} }
private static void CreateTask(TaskManager taskManager, TaskManager.SettingsData settings) private static TrangaTask? SelectTask(TrangaTask[] tasks, Logger logger)
{ {
TrangaTask.Task? tmpTask = SelectTaskType(); logger.WriteLine("Tranga_CLI", "Menu: Select task");
if (tasks.Length < 1)
{
Console.Clear();
Console.WriteLine("There are no available Tasks.");
logger.WriteLine("Tranga_CLI", "No available Tasks.");
return null;
}
PrintTasks(tasks, logger);
logger.WriteLine("Tranga_CLI", "Selecting Task to Remove (from queue)");
Console.WriteLine("Enter q to abort");
Console.WriteLine($"Select Task (0-{tasks.Length - 1}):");
string? selectedTask = Console.ReadLine();
while(selectedTask is null || selectedTask.Length < 1)
selectedTask = Console.ReadLine();
if (selectedTask.Length == 1 && selectedTask.ToLower() == "q")
{
Console.Clear();
Console.WriteLine("aborted.");
logger.WriteLine("Tranga_CLI", "aborted");
return null;
}
try
{
int selectedTaskIndex = Convert.ToInt32(selectedTask);
logger.WriteLine("Tranga_CLI", "Sending Task to TaskManager");
return tasks[selectedTaskIndex];
}
catch (Exception e)
{
Console.WriteLine($"Exception: {e.Message}");
logger.WriteLine("Tranga_CLI", e.Message);
}
return null;
}
private static void AddTaskToQueue(TaskManager taskManager, Logger logger)
{
Console.Clear();
logger.WriteLine("Tranga_CLI", "Menu: Add Task to queue");
TrangaTask[] tasks = taskManager.GetAllTasks().Where(rTask =>
rTask.state is not TrangaTask.ExecutionState.Enqueued and not TrangaTask.ExecutionState.Running).ToArray();
TrangaTask? selectedTask = SelectTask(tasks, logger);
if (selectedTask is null)
return;
logger.WriteLine("Tranga_CLI", "Sending Task to TaskManager");
taskManager.AddTaskToQueue(selectedTask);
}
private static void RemoveTaskFromQueue(TaskManager taskManager, Logger logger)
{
Console.Clear();
logger.WriteLine("Tranga_CLI", "Menu: Remove Task from queue");
TrangaTask[] tasks = taskManager.GetAllTasks().Where(rTask => rTask.state is TrangaTask.ExecutionState.Enqueued).ToArray();
TrangaTask? selectedTask = SelectTask(tasks, logger);
if (selectedTask is null)
return;
logger.WriteLine("Tranga_CLI", "Sending Task to TaskManager");
taskManager.RemoveTaskFromQueue(selectedTask);
}
private static void TailLog(Logger logger)
{
logger.WriteLine("Tranga_CLI", "Menu: Show Log-lines");
Console.Clear();
string[] lines = logger.Tail(20);
foreach (string message in lines)
Console.Write(message);
while (!Console.KeyAvailable)
{
string[] newLines = logger.GetNewLines();
foreach(string message in newLines)
Console.Write(message);
Thread.Sleep(40);
}
}
private static void CreateTask(TaskManager taskManager, TaskManager.SettingsData settings, Logger logger)
{
logger.WriteLine("Tranga_CLI", "Menu: Creating Task");
TrangaTask.Task? tmpTask = SelectTaskType(logger);
if (tmpTask is null) if (tmpTask is null)
return; return;
TrangaTask.Task task = (TrangaTask.Task)tmpTask!; TrangaTask.Task task = (TrangaTask.Task)tmpTask!;
@ -153,7 +311,7 @@ public static class Tranga_Cli
Connector? connector = null; Connector? connector = null;
if (task != TrangaTask.Task.UpdateKomgaLibrary) if (task != TrangaTask.Task.UpdateKomgaLibrary)
{ {
connector = SelectConnector(settings.downloadLocation, taskManager.GetAvailableConnectors().Values.ToArray()); connector = SelectConnector(settings.downloadLocation, taskManager.GetAvailableConnectors().Values.ToArray(), logger);
if (connector is null) if (connector is null)
return; return;
} }
@ -161,89 +319,46 @@ public static class Tranga_Cli
Publication? publication = null; Publication? publication = null;
if (task != TrangaTask.Task.UpdatePublications && task != TrangaTask.Task.UpdateKomgaLibrary) if (task != TrangaTask.Task.UpdatePublications && task != TrangaTask.Task.UpdateKomgaLibrary)
{ {
publication = SelectPublication(connector!); publication = SelectPublication(connector!, logger);
if (publication is null) if (publication is null)
return; return;
} }
TimeSpan reoccurrence = SelectReoccurrence(); TimeSpan reoccurrence = SelectReoccurrence(logger);
logger.WriteLine("Tranga_CLI", "Sending Task to TaskManager");
TrangaTask newTask = taskManager.AddTask(task, connector?.name, publication, reoccurrence, "en"); TrangaTask newTask = taskManager.AddTask(task, connector?.name, publication, reoccurrence, "en");
Console.WriteLine(newTask); Console.WriteLine(newTask);
} }
private static void ExecuteTaskNow(TaskManager taskManager) private static void ExecuteTaskNow(TaskManager taskManager, Logger logger)
{ {
logger.WriteLine("Tranga_CLI", "Menu: Executing Task");
TrangaTask[] tasks = taskManager.GetAllTasks().Where(nTask => nTask.state is not TrangaTask.ExecutionState.Running).ToArray();
TrangaTask? selectedTask = SelectTask(tasks, logger);
if (selectedTask is null)
return;
logger.WriteLine("Tranga_CLI", "Sending Task to TaskManager");
taskManager.ExecuteTaskNow(selectedTask);
}
private static void DeleteTask(TaskManager taskManager, Logger logger)
{
logger.WriteLine("Tranga_CLI", "Menu: Delete Task");
TrangaTask[] tasks = taskManager.GetAllTasks(); TrangaTask[] tasks = taskManager.GetAllTasks();
if (tasks.Length < 1)
{ TrangaTask? selectedTask = SelectTask(tasks, logger);
Console.Clear(); if (selectedTask is null)
Console.WriteLine("There are no available Tasks.");
return; return;
logger.WriteLine("Tranga_CLI", "Sending Task to TaskManager");
taskManager.DeleteTask(selectedTask.task, selectedTask.connectorName, selectedTask.publication);
} }
PrintTasks(tasks);
Console.WriteLine("Enter q to abort"); private static TrangaTask.Task? SelectTaskType(Logger logger)
Console.WriteLine($"Select Task (0-{tasks.Length - 1}):");
string? selectedTask = Console.ReadLine();
while(selectedTask is null || selectedTask.Length < 1)
selectedTask = Console.ReadLine();
if (selectedTask.Length == 1 && selectedTask.ToLower() == "q")
{
Console.WriteLine("aborted.");
return;
}
try
{
int selectedTaskIndex = Convert.ToInt32(selectedTask);
taskManager.ExecuteTaskNow(tasks[selectedTaskIndex]);
}
catch (Exception e)
{
Console.WriteLine($"Exception: {e.Message}");
}
}
private static void RemoveTask(TaskManager taskManager)
{
TrangaTask[] tasks = taskManager.GetAllTasks();
if (tasks.Length < 1)
{
Console.Clear();
Console.WriteLine("There are no available Tasks.");
return;
}
PrintTasks(tasks);
Console.WriteLine("Enter q to abort");
Console.WriteLine($"Select Task (0-{tasks.Length - 1}):");
string? selectedTask = Console.ReadLine();
while(selectedTask is null || selectedTask.Length < 1)
selectedTask = Console.ReadLine();
if (selectedTask.Length == 1 && selectedTask.ToLower() == "q")
{
Console.WriteLine("aborted.");
return;
}
try
{
int selectedTaskIndex = Convert.ToInt32(selectedTask);
taskManager.RemoveTask(tasks[selectedTaskIndex].task, tasks[selectedTaskIndex].connectorName, tasks[selectedTaskIndex].publication);
}
catch (Exception e)
{
Console.WriteLine($"Exception: {e.Message}");
}
}
private static TrangaTask.Task? SelectTaskType()
{ {
logger.WriteLine("Tranga_CLI", "Menu: Select TaskType");
Console.Clear(); Console.Clear();
string[] taskNames = Enum.GetNames<TrangaTask.Task>(); string[] taskNames = Enum.GetNames<TrangaTask.Task>();
@ -261,7 +376,9 @@ public static class Tranga_Cli
if (selectedTask.Length == 1 && selectedTask.ToLower() == "q") if (selectedTask.Length == 1 && selectedTask.ToLower() == "q")
{ {
Console.Clear();
Console.WriteLine("aborted."); Console.WriteLine("aborted.");
logger.WriteLine("Tranga_CLI", "aborted.");
return null; return null;
} }
@ -274,19 +391,22 @@ public static class Tranga_Cli
catch (Exception e) catch (Exception e)
{ {
Console.WriteLine($"Exception: {e.Message}"); Console.WriteLine($"Exception: {e.Message}");
logger.WriteLine("Tranga_CLI", e.Message);
} }
return null; return null;
} }
private static TimeSpan SelectReoccurrence() private static TimeSpan SelectReoccurrence(Logger logger)
{ {
logger.WriteLine("Tranga_CLI", "Menu: Select Reoccurrence");
Console.WriteLine("Select reoccurrence Timer (Format hh:mm:ss):"); Console.WriteLine("Select reoccurrence Timer (Format hh:mm:ss):");
return TimeSpan.Parse(Console.ReadLine()!, new CultureInfo("en-US")); return TimeSpan.Parse(Console.ReadLine()!, new CultureInfo("en-US"));
} }
private static Connector? SelectConnector(string folderPath, Connector[] connectors) private static Connector? SelectConnector(string folderPath, Connector[] connectors, Logger logger)
{ {
logger.WriteLine("Tranga_CLI", "Menu: Select Connector");
Console.Clear(); Console.Clear();
int cIndex = 0; int cIndex = 0;
@ -303,7 +423,9 @@ public static class Tranga_Cli
if (selectedConnector.Length == 1 && selectedConnector.ToLower() == "q") if (selectedConnector.Length == 1 && selectedConnector.ToLower() == "q")
{ {
Console.Clear();
Console.WriteLine("aborted."); Console.WriteLine("aborted.");
logger.WriteLine("Tranga_CLI", "aborted.");
return null; return null;
} }
@ -315,13 +437,16 @@ public static class Tranga_Cli
catch (Exception e) catch (Exception e)
{ {
Console.WriteLine($"Exception: {e.Message}"); Console.WriteLine($"Exception: {e.Message}");
logger.WriteLine("Tranga_CLI", e.Message);
} }
return null; return null;
} }
private static Publication? SelectPublication(Connector connector) private static Publication? SelectPublication(Connector connector, Logger logger)
{ {
logger.WriteLine("Tranga_CLI", "Menu: Select Publication");
Console.Clear(); Console.Clear();
Console.WriteLine($"Connector: {connector.name}"); Console.WriteLine($"Connector: {connector.name}");
Console.WriteLine("Publication search query (leave empty for all):"); Console.WriteLine("Publication search query (leave empty for all):");
@ -343,7 +468,9 @@ public static class Tranga_Cli
if (selectedPublication.Length == 1 && selectedPublication.ToLower() == "q") if (selectedPublication.Length == 1 && selectedPublication.ToLower() == "q")
{ {
Console.Clear();
Console.WriteLine("aborted."); Console.WriteLine("aborted.");
logger.WriteLine("Tranga_CLI", "aborted.");
return null; return null;
} }
@ -355,17 +482,21 @@ public static class Tranga_Cli
catch (Exception e) catch (Exception e)
{ {
Console.WriteLine($"Exception: {e.Message}"); Console.WriteLine($"Exception: {e.Message}");
logger.WriteLine("Tranga_CLI", e.Message);
} }
return null; return null;
} }
private static void SearchTasks(TaskManager taskManager) private static void SearchTasks(TaskManager taskManager, Logger logger)
{ {
logger.WriteLine("Tranga_CLI", "Menu: Search task");
Console.Clear();
Console.WriteLine("Enter search query:");
string? query = Console.ReadLine(); string? query = Console.ReadLine();
while (query is null || query.Length < 1) while (query is null || query.Length < 4)
query = Console.ReadLine(); query = Console.ReadLine();
PrintTasks(taskManager.GetAllTasks().Where(qTask => PrintTasks(taskManager.GetAllTasks().Where(qTask =>
qTask.ToString().ToLower().Contains(query, StringComparison.OrdinalIgnoreCase)).ToArray()); qTask.ToString().ToLower().Contains(query, StringComparison.OrdinalIgnoreCase)).ToArray(), logger);
} }
} }

View File

@ -4,7 +4,9 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tranga", ".\Tranga\Tranga.c
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tranga-CLI", "Tranga-CLI\Tranga-CLI.csproj", "{4899E3B2-B259-479A-B43E-042D043E9501}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tranga-CLI", "Tranga-CLI\Tranga-CLI.csproj", "{4899E3B2-B259-479A-B43E-042D043E9501}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tranga-API", "Tranga-API\Tranga-API.csproj", "{6284C936-4E90-486B-BC46-0AFAD85AD8EE}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Logging", "Logging\Logging.csproj", "{415BE889-BB7D-426F-976F-8D977876A462}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tranga-API", "Tranga-API\Tranga-API.csproj", "{48F4E495-75BC-4402-8E03-DEC5B79D7E83}"
EndProject EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
@ -20,9 +22,13 @@ Global
{4899E3B2-B259-479A-B43E-042D043E9501}.Debug|Any CPU.Build.0 = Debug|Any CPU {4899E3B2-B259-479A-B43E-042D043E9501}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4899E3B2-B259-479A-B43E-042D043E9501}.Release|Any CPU.ActiveCfg = Release|Any CPU {4899E3B2-B259-479A-B43E-042D043E9501}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4899E3B2-B259-479A-B43E-042D043E9501}.Release|Any CPU.Build.0 = Release|Any CPU {4899E3B2-B259-479A-B43E-042D043E9501}.Release|Any CPU.Build.0 = Release|Any CPU
{6284C936-4E90-486B-BC46-0AFAD85AD8EE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {415BE889-BB7D-426F-976F-8D977876A462}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6284C936-4E90-486B-BC46-0AFAD85AD8EE}.Debug|Any CPU.Build.0 = Debug|Any CPU {415BE889-BB7D-426F-976F-8D977876A462}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6284C936-4E90-486B-BC46-0AFAD85AD8EE}.Release|Any CPU.ActiveCfg = Release|Any CPU {415BE889-BB7D-426F-976F-8D977876A462}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6284C936-4E90-486B-BC46-0AFAD85AD8EE}.Release|Any CPU.Build.0 = Release|Any CPU {415BE889-BB7D-426F-976F-8D977876A462}.Release|Any CPU.Build.0 = Release|Any CPU
{48F4E495-75BC-4402-8E03-DEC5B79D7E83}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{48F4E495-75BC-4402-8E03-DEC5B79D7E83}.Debug|Any CPU.Build.0 = Debug|Any CPU
{48F4E495-75BC-4402-8E03-DEC5B79D7E83}.Release|Any CPU.ActiveCfg = Release|Any CPU
{48F4E495-75BC-4402-8E03-DEC5B79D7E83}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
EndGlobal EndGlobal

View File

@ -1,6 +1,7 @@
using System.IO.Compression; using System.IO.Compression;
using System.Net; using System.Net;
using System.Xml.Linq; using System.Xml.Linq;
using Logging;
namespace Tranga; namespace Tranga;
@ -13,10 +14,13 @@ public abstract class Connector
internal string downloadLocation { get; } //Location of local files internal string downloadLocation { get; } //Location of local files
protected DownloadClient downloadClient { get; } protected DownloadClient downloadClient { get; }
protected Connector(string downloadLocation, uint downloadDelay) protected Logger? logger;
protected Connector(string downloadLocation, uint downloadDelay, Logger? logger)
{ {
this.downloadLocation = downloadLocation; this.downloadLocation = downloadLocation;
this.downloadClient = new DownloadClient(downloadDelay); this.downloadClient = new DownloadClient(downloadDelay);
this.logger = logger;
} }
public abstract string name { get; } //Name of the Connector (e.g. Website) public abstract string name { get; } //Name of the Connector (e.g. Website)
@ -58,6 +62,7 @@ public abstract class Connector
/// <param name="publication">Publication to save series.json for</param> /// <param name="publication">Publication to save series.json for</param>
public void SaveSeriesInfo(Publication publication) public void SaveSeriesInfo(Publication publication)
{ {
logger?.WriteLine(this.GetType().ToString(), $"Saving series.json for {publication.sortName}");
//Check if Publication already has a Folder and a series.json //Check if Publication already has a Folder and a series.json
string publicationFolder = Path.Join(downloadLocation, publication.folderName); string publicationFolder = Path.Join(downloadLocation, publication.folderName);
if(!Directory.Exists(publicationFolder)) if(!Directory.Exists(publicationFolder))
@ -73,8 +78,9 @@ 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) protected static string CreateComicInfo(Publication publication, Chapter chapter, Logger? logger)
{ {
logger?.WriteLine("Connector", $"Creating ComicInfo.Xml for {publication.sortName} Chapter {chapter.volumeNumber} {chapter.chapterNumber}");
XElement comicInfo = new XElement("ComicInfo", XElement comicInfo = new XElement("ComicInfo",
new XElement("Tags", string.Join(',',publication.tags)), new XElement("Tags", string.Join(',',publication.tags)),
new XElement("LanguageISO", publication.originalLanguage), new XElement("LanguageISO", publication.originalLanguage),
@ -124,8 +130,9 @@ public abstract class Connector
/// <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="downloadClient">DownloadClient of the connector</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>
protected static void DownloadChapterImages(string[] imageUrls, string saveArchiveFilePath, DownloadClient downloadClient, string? comicInfoPath = null) protected static void DownloadChapterImages(string[] imageUrls, string saveArchiveFilePath, DownloadClient downloadClient, Logger? logger, string? comicInfoPath = null)
{ {
logger?.WriteLine("Connector", "Downloading Images");
//Check if Publication Directory already exists //Check if Publication Directory already exists
string[] splitPath = saveArchiveFilePath.Split(Path.DirectorySeparatorChar); string[] splitPath = saveArchiveFilePath.Split(Path.DirectorySeparatorChar);
string directoryPath = Path.Combine(splitPath.Take(splitPath.Length - 1).ToArray()); string directoryPath = Path.Combine(splitPath.Take(splitPath.Length - 1).ToArray());
@ -151,6 +158,7 @@ public abstract class Connector
if(comicInfoPath is not null) if(comicInfoPath is not null)
File.Copy(comicInfoPath, Path.Join(tempFolder, "ComicInfo.xml")); File.Copy(comicInfoPath, Path.Join(tempFolder, "ComicInfo.xml"));
logger?.WriteLine("Connector", "Creating archive");
//ZIP-it and ship-it //ZIP-it and ship-it
ZipFile.CreateFromDirectory(tempFolder, fullPath); ZipFile.CreateFromDirectory(tempFolder, fullPath);
Directory.Delete(tempFolder, true); //Cleanup Directory.Delete(tempFolder, true); //Cleanup

View File

@ -2,24 +2,26 @@
using System.Net; using System.Net;
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Nodes; using System.Text.Json.Nodes;
using Logging;
namespace Tranga.Connectors; namespace Tranga.Connectors;
public class MangaDex : Connector public class MangaDex : Connector
{ {
public override string name { get; } public override string name { get; }
public MangaDex(string downloadLocation, uint downloadDelay) : base(downloadLocation, downloadDelay) public MangaDex(string downloadLocation, uint downloadDelay, Logger? logger) : base(downloadLocation, downloadDelay, logger)
{ {
name = "MangaDex"; name = "MangaDex";
} }
public MangaDex(string downloadLocation) : base(downloadLocation, 750) public MangaDex(string downloadLocation, Logger? logger) : base(downloadLocation, 750, logger)
{ {
name = "MangaDex"; name = "MangaDex";
} }
public override Publication[] GetPublications(string publicationTitle = "") public override Publication[] GetPublications(string publicationTitle = "")
{ {
logger?.WriteLine(this.GetType().ToString(), $"Getting Publications (title={publicationTitle})");
const int limit = 100; //How many values we want returned at once const int limit = 100; //How many values we want returned at once
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
@ -56,14 +58,12 @@ public class MangaDex : Connector
: null; : null;
JsonArray altTitlesObject = attributes["altTitles"]!.AsArray(); JsonArray altTitlesObject = attributes["altTitles"]!.AsArray();
string[,] altTitles = new string[altTitlesObject.Count, 2]; Dictionary<string, string> altTitlesDict = new();
int titleIndex = 0;
foreach (JsonNode? altTitleNode in altTitlesObject) foreach (JsonNode? altTitleNode in altTitlesObject)
{ {
JsonObject altTitleObject = (JsonObject)altTitleNode!; JsonObject altTitleObject = (JsonObject)altTitleNode!;
string key = ((IDictionary<string, JsonNode?>)altTitleObject).Keys.ToArray()[0]; string key = ((IDictionary<string, JsonNode?>)altTitleObject).Keys.ToArray()[0];
altTitles[titleIndex, 0] = key; altTitlesDict.TryAdd(key, altTitleObject[key]!.GetValue<string>());
altTitles[titleIndex++, 1] = altTitleObject[key]!.GetValue<string>();
} }
JsonArray tagsObject = attributes["tags"]!.AsArray(); JsonArray tagsObject = attributes["tags"]!.AsArray();
@ -82,16 +82,14 @@ public class MangaDex : Connector
poster = relationships.FirstOrDefault(relationship => relationship!["type"]!.GetValue<string>() == "cover_art")!["id"]!.GetValue<string>(); poster = relationships.FirstOrDefault(relationship => relationship!["type"]!.GetValue<string>() == "cover_art")!["id"]!.GetValue<string>();
} }
Dictionary<string, string> linksDict = new();
string[,]? links = null; string[,]? links = null;
if (attributes.ContainsKey("links") && attributes["links"] is not null) if (attributes.ContainsKey("links") && attributes["links"] is not null)
{ {
JsonObject linksObject = attributes["links"]!.AsObject(); JsonObject linksObject = attributes["links"]!.AsObject();
links = new string[linksObject.Count, 2];
int linkIndex = 0;
foreach (string key in ((IDictionary<string, JsonNode?>)linksObject).Keys) foreach (string key in ((IDictionary<string, JsonNode?>)linksObject).Keys)
{ {
links[linkIndex, 0] = key; linksDict.Add(key, linksObject[key]!.GetValue<string>());
links[linkIndex++, 1] = linksObject[key]!.GetValue<string>();
} }
} }
@ -108,10 +106,10 @@ public class MangaDex : Connector
Publication pub = new Publication( Publication pub = new Publication(
title, title,
description, description,
altTitles, altTitlesDict,
tags.ToArray(), tags.ToArray(),
poster, poster,
links, linksDict,
year, year,
originalLanguage, originalLanguage,
status, status,
@ -126,6 +124,7 @@ public class MangaDex : Connector
public override Chapter[] GetChapters(Publication publication, string language = "") public override Chapter[] GetChapters(Publication publication, string language = "")
{ {
logger?.WriteLine(this.GetType().ToString(), $"Getting Chapters {publication.sortName} (language={language})");
const int limit = 100; //How many values we want returned at once const int limit = 100; //How many values we want returned at once
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
@ -180,6 +179,7 @@ public class MangaDex : Connector
public override void DownloadChapter(Publication publication, Chapter chapter) public override void DownloadChapter(Publication publication, Chapter chapter)
{ {
logger?.WriteLine(this.GetType().ToString(), $"Download Chapter {publication.sortName} {chapter.volumeNumber}-{chapter.chapterNumber}");
//Request URLs for Chapter-Images //Request URLs for Chapter-Images
DownloadClient.RequestResult requestResult = DownloadClient.RequestResult requestResult =
downloadClient.MakeRequest($"https://api.mangadex.org/at-home/server/{chapter.url}?forcePort443=false'"); downloadClient.MakeRequest($"https://api.mangadex.org/at-home/server/{chapter.url}?forcePort443=false'");
@ -198,14 +198,15 @@ 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)); File.WriteAllText(comicInfoPath, CreateComicInfo(publication, chapter, logger));
//Download Chapter-Images //Download Chapter-Images
DownloadChapterImages(imageUrls.ToArray(), CreateFullFilepath(publication, chapter), downloadClient, comicInfoPath); DownloadChapterImages(imageUrls.ToArray(), CreateFullFilepath(publication, chapter), downloadClient, logger, comicInfoPath);
} }
public override void DownloadCover(Publication publication) public override void DownloadCover(Publication publication)
{ {
logger?.WriteLine(this.GetType().ToString(), $"Download cover {publication.sortName}");
//Check if Publication already has a Folder and cover //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))

View File

@ -1,5 +1,6 @@
using System.Net.Http.Headers; using System.Net.Http.Headers;
using System.Text.Json.Nodes; using System.Text.Json.Nodes;
using Logging;
using Newtonsoft.Json; using Newtonsoft.Json;
using JsonSerializer = System.Text.Json.JsonSerializer; using JsonSerializer = System.Text.Json.JsonSerializer;
@ -14,22 +15,26 @@ public class Komga
public string baseUrl { get; } public string baseUrl { get; }
public string auth { get; } //Base64 encoded, if you use your password everywhere, you have problems public string auth { get; } //Base64 encoded, if you use your password everywhere, you have problems
private Logger? logger;
/// <param name="baseUrl">Base-URL of Komga instance, no trailing slashes(/)</param> /// <param name="baseUrl">Base-URL of Komga instance, no trailing slashes(/)</param>
/// <param name="username">Komga Username</param> /// <param name="username">Komga Username</param>
/// <param name="password">Komga password, will be base64 encoded. yea</param> /// <param name="password">Komga password, will be base64 encoded. yea</param>
public Komga(string baseUrl, string username, string password) public Komga(string baseUrl, string username, string password, Logger? logger)
{ {
this.baseUrl = baseUrl; this.baseUrl = baseUrl;
this.auth = Convert.ToBase64String(System.Text.Encoding.ASCII.GetBytes($"{username}:{password}")); this.auth = Convert.ToBase64String(System.Text.Encoding.ASCII.GetBytes($"{username}:{password}"));
this.logger = logger;
} }
/// <param name="baseUrl">Base-URL of Komga instance, no trailing slashes(/)</param> /// <param name="baseUrl">Base-URL of Komga instance, no trailing slashes(/)</param>
/// <param name="auth">Base64 string of username and password (username):(password)</param> /// <param name="auth">Base64 string of username and password (username):(password)</param>
[JsonConstructor] [JsonConstructor]
public Komga(string baseUrl, string auth) public Komga(string baseUrl, string auth, Logger? logger)
{ {
this.baseUrl = baseUrl; this.baseUrl = baseUrl;
this.auth = auth; this.auth = auth;
this.logger = logger;
} }
/// <summary> /// <summary>
@ -38,6 +43,7 @@ public class Komga
/// <returns>Array of KomgaLibraries</returns> /// <returns>Array of KomgaLibraries</returns>
public KomgaLibrary[] GetLibraries() public KomgaLibrary[] GetLibraries()
{ {
logger?.WriteLine(this.GetType().ToString(), $"Getting Libraries");
Stream data = NetClient.MakeRequest($"{baseUrl}/api/v1/libraries", auth); Stream data = NetClient.MakeRequest($"{baseUrl}/api/v1/libraries", auth);
JsonArray? result = JsonSerializer.Deserialize<JsonArray>(data); JsonArray? result = JsonSerializer.Deserialize<JsonArray>(data);
if (result is null) if (result is null)
@ -63,6 +69,7 @@ public class Komga
/// <returns>true if successful</returns> /// <returns>true if successful</returns>
public bool UpdateLibrary(string libraryId) public bool UpdateLibrary(string libraryId)
{ {
logger?.WriteLine(this.GetType().ToString(), $"Updating Libraries");
return NetClient.MakePost($"{baseUrl}/api/v1/libraries/{libraryId}/scan", auth); return NetClient.MakePost($"{baseUrl}/api/v1/libraries/{libraryId}/scan", auth);
} }

View File

@ -9,31 +9,38 @@ public readonly struct Publication
{ {
public string sortName { get; } public string sortName { get; }
// ReSharper disable UnusedAutoPropertyAccessor.Global we need it, trust // ReSharper disable UnusedAutoPropertyAccessor.Global we need it, trust
[JsonIgnore]public string[,] altTitles { get; } [JsonIgnore]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; }
public string[] tags { get; } public string[] tags { get; }
public string? posterUrl { get; } public string? posterUrl { get; }
[JsonIgnore]public string[,]? links { get; } [JsonIgnore]public Dictionary<string,string> links { get; }
public int? year { get; } public int? year { get; }
public string? originalLanguage { get; } public string? originalLanguage { get; }
public string status { get; } public string status { get; }
public string folderName { get; } public string folderName { get; }
public string downloadUrl { get; } public string downloadUrl { get; }
public string internalId { get; }
public Publication(string sortName, string? description, string[,] altTitles, string[] tags, string? posterUrl, string[,]? links, int? year, string? originalLanguage, string status, string downloadUrl) public readonly struct ValueTuple
{
}
public Publication(string sortName, string? description, Dictionary<string,string> altTitles, string[] tags, string? posterUrl, Dictionary<string,string>? links, int? year, string? originalLanguage, string status, string downloadUrl)
{ {
this.sortName = sortName; this.sortName = sortName;
this.description = description; this.description = description;
this.altTitles = altTitles; this.altTitles = altTitles;
this.tags = tags; this.tags = tags;
this.posterUrl = posterUrl; this.posterUrl = posterUrl;
this.links = links; this.links = links ?? new Dictionary<string, string>();
this.year = year; this.year = year;
this.originalLanguage = originalLanguage; this.originalLanguage = originalLanguage;
this.status = status; this.status = status;
this.downloadUrl = downloadUrl; this.downloadUrl = downloadUrl;
this.folderName = string.Concat(sortName.Split(Path.GetInvalidPathChars().Concat(Path.GetInvalidFileNameChars()).ToArray())); this.folderName = string.Concat(sortName.Split(Path.GetInvalidPathChars().Concat(Path.GetInvalidFileNameChars()).ToArray()));
this.internalId = Guid.NewGuid().ToString();
} }
/// <returns>Serialized JSON String for series.json</returns> /// <returns>Serialized JSON String for series.json</returns>

View File

@ -1,4 +1,6 @@
namespace Tranga; using Logging;
namespace Tranga;
/// <summary> /// <summary>
/// Executes TrangaTasks /// Executes TrangaTasks
@ -13,13 +15,18 @@ public static class TaskExecutor
/// <param name="taskManager">Parent</param> /// <param name="taskManager">Parent</param>
/// <param name="trangaTask">Task to execute</param> /// <param name="trangaTask">Task to execute</param>
/// <param name="chapterCollection">Current chapterCollection to update</param> /// <param name="chapterCollection">Current chapterCollection to update</param>
/// <param name="logger"></param>
/// <exception cref="ArgumentException">Is thrown when there is no Connector available with the name of the TrangaTask.connectorName</exception> /// <exception cref="ArgumentException">Is thrown when there is no Connector available with the name of the TrangaTask.connectorName</exception>
public static void Execute(TaskManager taskManager, TrangaTask trangaTask, Dictionary<Publication, List<Chapter>> chapterCollection) public static void Execute(TaskManager taskManager, TrangaTask trangaTask, Logger? logger)
{ {
//Only execute task if it is not already being executed. //Only execute task if it is not already being executed.
if (trangaTask.state == TrangaTask.ExecutionState.Running) if (trangaTask.state == TrangaTask.ExecutionState.Running)
{
logger?.WriteLine("TaskExecutor", $"Task already running {trangaTask}");
return; return;
}
trangaTask.state = TrangaTask.ExecutionState.Running; trangaTask.state = TrangaTask.ExecutionState.Running;
logger?.WriteLine("TaskExecutor", $"Starting Task {trangaTask}");
//Connector is not needed for all tasks //Connector is not needed for all tasks
Connector? connector = null; Connector? connector = null;
@ -30,21 +37,22 @@ public static class TaskExecutor
switch (trangaTask.task) switch (trangaTask.task)
{ {
case TrangaTask.Task.DownloadNewChapters: case TrangaTask.Task.DownloadNewChapters:
DownloadNewChapters(connector!, (Publication)trangaTask.publication!, trangaTask.language, chapterCollection); DownloadNewChapters(connector!, (Publication)trangaTask.publication!, trangaTask.language, ref taskManager._chapterCollection);
break; break;
case TrangaTask.Task.UpdateChapters: case TrangaTask.Task.UpdateChapters:
UpdateChapters(connector!, (Publication)trangaTask.publication!, trangaTask.language, chapterCollection); UpdateChapters(connector!, (Publication)trangaTask.publication!, trangaTask.language, ref taskManager._chapterCollection);
break; break;
case TrangaTask.Task.UpdatePublications: case TrangaTask.Task.UpdatePublications:
UpdatePublications(connector!, chapterCollection); UpdatePublications(connector!, ref taskManager._chapterCollection);
break; break;
case TrangaTask.Task.UpdateKomgaLibrary: case TrangaTask.Task.UpdateKomgaLibrary:
UpdateKomgaLibrary(taskManager); UpdateKomgaLibrary(taskManager);
break; break;
} }
trangaTask.state = TrangaTask.ExecutionState.Waiting; logger?.WriteLine("TaskExecutor", $"Task finished! {trangaTask}");
trangaTask.lastExecuted = DateTime.Now; trangaTask.lastExecuted = DateTime.Now;
trangaTask.state = TrangaTask.ExecutionState.Waiting;
} }
/// <summary> /// <summary>
@ -67,7 +75,7 @@ public static class TaskExecutor
/// </summary> /// </summary>
/// <param name="connector">Connector to receive Publications from</param> /// <param name="connector">Connector to receive Publications from</param>
/// <param name="chapterCollection"></param> /// <param name="chapterCollection"></param>
private static void UpdatePublications(Connector connector, Dictionary<Publication, List<Chapter>> chapterCollection) private static void UpdatePublications(Connector connector, ref Dictionary<Publication, List<Chapter>> chapterCollection)
{ {
Publication[] publications = connector.GetPublications(); Publication[] publications = connector.GetPublications();
foreach (Publication publication in publications) foreach (Publication publication in publications)
@ -82,9 +90,9 @@ public static class TaskExecutor
/// <param name="publication">Publication to check</param> /// <param name="publication">Publication to check</param>
/// <param name="language">Language to receive chapters for</param> /// <param name="language">Language to receive chapters for</param>
/// <param name="chapterCollection"></param> /// <param name="chapterCollection"></param>
private static void DownloadNewChapters(Connector connector, Publication publication, string language, Dictionary<Publication, List<Chapter>> chapterCollection) private static void DownloadNewChapters(Connector connector, Publication publication, string language, ref Dictionary<Publication, List<Chapter>> chapterCollection)
{ {
List<Chapter> newChapters = UpdateChapters(connector, publication, language, chapterCollection); List<Chapter> newChapters = UpdateChapters(connector, publication, language, ref chapterCollection);
connector.DownloadCover(publication); connector.DownloadCover(publication);
//Check if Publication already has a Folder and a series.json //Check if Publication already has a Folder and a series.json
@ -108,7 +116,7 @@ public static class TaskExecutor
/// <param name="language">Language to receive chapters for</param> /// <param name="language">Language to receive chapters for</param>
/// <param name="chapterCollection"></param> /// <param name="chapterCollection"></param>
/// <returns>List of Chapters that were previously not in collection</returns> /// <returns>List of Chapters that were previously not in collection</returns>
private static List<Chapter> UpdateChapters(Connector connector, Publication publication, string language, Dictionary<Publication, List<Chapter>> chapterCollection) private static List<Chapter> UpdateChapters(Connector connector, Publication publication, string language, ref Dictionary<Publication, List<Chapter>> chapterCollection)
{ {
List<Chapter> newChaptersList = new(); List<Chapter> newChaptersList = new();
chapterCollection.TryAdd(publication, newChaptersList); //To ensure publication is actually in collection chapterCollection.TryAdd(publication, newChaptersList); //To ensure publication is actually in collection

View File

@ -1,4 +1,5 @@
using Newtonsoft.Json; using Logging;
using Newtonsoft.Json;
using Tranga.Connectors; using Tranga.Connectors;
namespace Tranga; namespace Tranga;
@ -9,40 +10,57 @@ namespace Tranga;
/// </summary> /// </summary>
public class TaskManager public class TaskManager
{ {
private readonly Dictionary<Publication, List<Chapter>> _chapterCollection = new(); public Dictionary<Publication, List<Chapter>> _chapterCollection = new();
private readonly HashSet<TrangaTask> _allTasks; private readonly HashSet<TrangaTask> _allTasks;
private bool _continueRunning = true; private bool _continueRunning = true;
private readonly Connector[] _connectors; private readonly Connector[] _connectors;
private Dictionary<Connector, List<TrangaTask>> tasksToExecute = new(); private readonly Dictionary<Connector, List<TrangaTask>> _taskQueue = new();
private string downloadLocation { get; } public SettingsData settings { get; }
private Logger? logger { get; }
public Komga? komga { get; }
public Komga? komga { get; private set; } /// <param name="downloadFolderPath">Local path to save data (Manga) to</param>
/// <param name="settingsFilePath">Path to the settings file (data.json)</param>
/// <param name="folderPath">Local path to save data (Manga) to</param>
/// <param name="komgaBaseUrl">The Url of the Komga-instance that you want to update</param> /// <param name="komgaBaseUrl">The Url of the Komga-instance that you want to update</param>
/// <param name="komgaUsername">The Komga username</param> /// <param name="komgaUsername">The Komga username</param>
/// <param name="komgaPassword">The Komga password</param> /// <param name="komgaPassword">The Komga password</param>
public TaskManager(string folderPath, string? komgaBaseUrl = null, string? komgaUsername = null, string? komgaPassword = null) /// <param name="logger"></param>
public TaskManager(string downloadFolderPath, string? settingsFilePath = null, string? komgaBaseUrl = null, string? komgaUsername = null, string? komgaPassword = null, Logger? logger = null)
{ {
this.downloadLocation = folderPath; this.logger = logger;
if (komgaBaseUrl != null && komgaUsername != null && komgaPassword != null)
this.komga = new Komga(komgaBaseUrl, komgaUsername, komgaPassword);
this._connectors = new Connector[]{ new MangaDex(folderPath) };
foreach(Connector cConnector in this._connectors)
tasksToExecute.Add(cConnector, new List<TrangaTask>());
_allTasks = new HashSet<TrangaTask>(); _allTasks = new HashSet<TrangaTask>();
if (komgaBaseUrl != null && komgaUsername != null && komgaPassword != null)
this.komga = new Komga(komgaBaseUrl, komgaUsername, komgaPassword, logger);
this.settings = new SettingsData(downloadFolderPath, settingsFilePath, this.komga, this._allTasks);
ExportData();
this._connectors = new Connector[]{ new MangaDex(downloadFolderPath, logger) };
foreach(Connector cConnector in this._connectors)
_taskQueue.Add(cConnector, new List<TrangaTask>());
Thread taskChecker = new(TaskCheckerThread); Thread taskChecker = new(TaskCheckerThread);
taskChecker.Start(); taskChecker.Start();
} }
public TaskManager(SettingsData settings) public void UpdateSettings(string? downloadLocation, string? komgaUrl, string? komgaAuth)
{ {
this._connectors = new Connector[]{ new MangaDex(settings.downloadLocation) }; Komga? komga = null;
if (komgaUrl is not null && komgaAuth is not null)
komga = new Komga(komgaUrl, komgaAuth, null);
settings.UpdateSettings(downloadLocation, komga);
ExportData();
}
public TaskManager(SettingsData settings, Logger? logger = null)
{
this.logger = logger;
this._connectors = new Connector[]{ new MangaDex(settings.downloadLocation, logger) };
this.settings = settings;
ExportData();
foreach(Connector cConnector in this._connectors) foreach(Connector cConnector in this._connectors)
tasksToExecute.Add(cConnector, new List<TrangaTask>()); _taskQueue.Add(cConnector, new List<TrangaTask>());
this.downloadLocation = settings.downloadLocation;
this.komga = settings.komga; this.komga = settings.komga;
_allTasks = settings.allTasks; _allTasks = settings.allTasks;
Thread taskChecker = new(TaskCheckerThread); Thread taskChecker = new(TaskCheckerThread);
@ -55,13 +73,16 @@ public class TaskManager
/// </summary> /// </summary>
private void TaskCheckerThread() private void TaskCheckerThread()
{ {
logger?.WriteLine(this.GetType().ToString(), "Starting TaskCheckerThread.");
while (_continueRunning) while (_continueRunning)
{ {
//Check if previous tasks have finished and execute new tasks //Check if previous tasks have finished and execute new tasks
foreach (KeyValuePair<Connector, List<TrangaTask>> connectorTaskQueue in tasksToExecute) foreach (KeyValuePair<Connector, List<TrangaTask>> connectorTaskQueue in _taskQueue)
{ {
connectorTaskQueue.Value.RemoveAll(task => task.state == TrangaTask.ExecutionState.Waiting); if(connectorTaskQueue.Value.RemoveAll(task => task.state == TrangaTask.ExecutionState.Waiting) > 0)
if (connectorTaskQueue.Value.Count > 0 && !connectorTaskQueue.Value.All(task => task.state is TrangaTask.ExecutionState.Running or TrangaTask.ExecutionState.Waiting)) ExportData();
if (connectorTaskQueue.Value.Count > 0 && connectorTaskQueue.Value.All(task => task.state is TrangaTask.ExecutionState.Enqueued))
ExecuteTaskNow(connectorTaskQueue.Value.First()); ExecuteTaskNow(connectorTaskQueue.Value.First());
} }
@ -74,7 +95,8 @@ public class TaskManager
ExecuteTaskNow(task); ExecuteTaskNow(task);
else else
{ {
tasksToExecute[GetConnector(task.connectorName!)].Add(task); logger?.WriteLine(this.GetType().ToString(), $"Task due: {task}");
_taskQueue[GetConnector(task.connectorName!)].Add(task);
} }
} }
Thread.Sleep(1000); Thread.Sleep(1000);
@ -90,9 +112,10 @@ public class TaskManager
if (!this._allTasks.Contains(task)) if (!this._allTasks.Contains(task))
return; return;
logger?.WriteLine(this.GetType().ToString(), $"Forcing Execution: {task}");
Task t = new Task(() => Task t = new Task(() =>
{ {
TaskExecutor.Execute(this, task, this._chapterCollection); TaskExecutor.Execute(this, task, logger);
}); });
t.Start(); t.Start();
} }
@ -109,8 +132,7 @@ public class TaskManager
public TrangaTask AddTask(TrangaTask.Task task, string? connectorName, Publication? publication, TimeSpan reoccurrence, public TrangaTask AddTask(TrangaTask.Task task, string? connectorName, Publication? publication, TimeSpan reoccurrence,
string language = "") string language = "")
{ {
if (task != TrangaTask.Task.UpdateKomgaLibrary && connectorName is null) logger?.WriteLine(this.GetType().ToString(), $"Adding new Task {task} {connectorName} {publication?.sortName}");
throw new ArgumentException($"connectorName can not be null for task {task}");
TrangaTask newTask; TrangaTask newTask;
if (task == TrangaTask.Task.UpdateKomgaLibrary) if (task == TrangaTask.Task.UpdateKomgaLibrary)
@ -126,6 +148,9 @@ public class TaskManager
} }
else else
{ {
if(connectorName is null)
throw new ArgumentException($"connectorName can not be null for task {task}");
//Get appropriate Connector from available Connectors for TrangaTask //Get appropriate Connector from available Connectors for TrangaTask
Connector? connector = _connectors.FirstOrDefault(c => c.name == connectorName); Connector? connector = _connectors.FirstOrDefault(c => c.name == connectorName);
if (connector is null) if (connector is null)
@ -138,11 +163,12 @@ public class TaskManager
trangaTask.publication?.downloadUrl == publication?.downloadUrl)) trangaTask.publication?.downloadUrl == publication?.downloadUrl))
{ {
if(task != TrangaTask.Task.UpdatePublications) if(task != TrangaTask.Task.UpdatePublications)
_chapterCollection.Add((Publication)publication!, new List<Chapter>()); _chapterCollection.TryAdd((Publication)publication!, new List<Chapter>());
_allTasks.Add(newTask); _allTasks.Add(newTask);
} }
} }
ExportData(Directory.GetCurrentDirectory()); logger?.WriteLine(this.GetType().ToString(), $"Added new Task {newTask.ToString()}");
ExportData();
return newTask; return newTask;
} }
@ -153,17 +179,48 @@ public class TaskManager
/// <param name="task">TrangaTask.Task type</param> /// <param name="task">TrangaTask.Task type</param>
/// <param name="connectorName">Name of Connector that was used</param> /// <param name="connectorName">Name of Connector that was used</param>
/// <param name="publication">Publication that was used</param> /// <param name="publication">Publication that was used</param>
public void RemoveTask(TrangaTask.Task task, string? connectorName, Publication? publication) public void DeleteTask(TrangaTask.Task task, string? connectorName, Publication? publication)
{ {
logger?.WriteLine(this.GetType().ToString(), $"Removing Task {task} {publication?.sortName}");
if (task == TrangaTask.Task.UpdateKomgaLibrary) if (task == TrangaTask.Task.UpdateKomgaLibrary)
{
_allTasks.RemoveWhere(uTask => uTask.task == TrangaTask.Task.UpdateKomgaLibrary); _allTasks.RemoveWhere(uTask => uTask.task == TrangaTask.Task.UpdateKomgaLibrary);
logger?.WriteLine(this.GetType().ToString(), $"Removed Task {task}");
}
else if (connectorName is null) else if (connectorName is null)
throw new ArgumentException($"connectorName can not be null for Task {task}"); throw new ArgumentException($"connectorName can not be null for Task {task}");
else else
_allTasks.RemoveWhere(trangaTask => {
if(_allTasks.RemoveWhere(trangaTask =>
trangaTask.task == task && trangaTask.connectorName == connectorName && trangaTask.task == task && trangaTask.connectorName == connectorName &&
trangaTask.publication?.downloadUrl == publication?.downloadUrl); trangaTask.publication?.downloadUrl == publication?.downloadUrl) > 0)
ExportData(Directory.GetCurrentDirectory()); logger?.WriteLine(this.GetType().ToString(), $"Removed Task {task} {publication?.sortName} {publication?.downloadUrl}.");
else
logger?.WriteLine(this.GetType().ToString(), $"No Task {task} {publication?.sortName} {publication?.downloadUrl} could be found.");
}
ExportData();
}
/// <summary>
/// Removes a Task from the queue
/// </summary>
/// <param name="task"></param>
public void RemoveTaskFromQueue(TrangaTask task)
{
task.lastExecuted = DateTime.Now;
foreach (List<TrangaTask> taskList in this._taskQueue.Values)
taskList.Remove(task);
task.state = TrangaTask.ExecutionState.Waiting;
}
/// <summary>
/// Sets last execution time to start of time
/// Let taskManager handle enqueuing
/// </summary>
/// <param name="task"></param>
public void AddTaskToQueue(TrangaTask task)
{
task.lastExecuted = DateTime.UnixEpoch;
} }
/// <returns>All available Connectors</returns> /// <returns>All available Connectors</returns>
@ -180,6 +237,17 @@ public class TaskManager
return ret; return ret;
} }
public Publication[] GetPublicationsFromConnector(Connector connector, string? title = null)
{
Publication[] ret = connector.GetPublications(title ?? "");
foreach (Publication publication in ret)
{
if(!_chapterCollection.Any(pub => pub.Key.sortName == publication.sortName))
this._chapterCollection.TryAdd(publication, new List<Chapter>());
}
return ret;
}
/// <returns>All added Publications</returns> /// <returns>All added Publications</returns>
public Publication[] GetAllPublications() public Publication[] GetAllPublications()
{ {
@ -191,8 +259,10 @@ public class TaskManager
/// </summary> /// </summary>
/// <param name="connectorName">Connector-name (exact)</param> /// <param name="connectorName">Connector-name (exact)</param>
/// <exception cref="Exception">If Connector is not available</exception> /// <exception cref="Exception">If Connector is not available</exception>
public Connector GetConnector(string connectorName) public Connector GetConnector(string? connectorName)
{ {
if(connectorName is null)
throw new Exception($"connectorName can not be null");
Connector? ret = this._connectors.FirstOrDefault(connector => connector.name == connectorName); Connector? ret = this._connectors.FirstOrDefault(connector => connector.name == connectorName);
if (ret is null) if (ret is null)
throw new Exception($"Connector {connectorName} is not an available Connector."); throw new Exception($"Connector {connectorName} is not an available Connector.");
@ -205,8 +275,9 @@ public class TaskManager
/// <param name="force">If force is true, tasks are aborted.</param> /// <param name="force">If force is true, tasks are aborted.</param>
public void Shutdown(bool force = false) public void Shutdown(bool force = false)
{ {
logger?.WriteLine(this.GetType().ToString(), $"Shutting down (forced={force})");
_continueRunning = false; _continueRunning = false;
ExportData(Directory.GetCurrentDirectory()); ExportData();
if(force) if(force)
Environment.Exit(_allTasks.Count(task => task.state is TrangaTask.ExecutionState.Enqueued or TrangaTask.ExecutionState.Running)); Environment.Exit(_allTasks.Count(task => task.state is TrangaTask.ExecutionState.Enqueued or TrangaTask.ExecutionState.Running));
@ -214,20 +285,20 @@ public class TaskManager
//Wait for tasks to finish //Wait for tasks to finish
while(_allTasks.Any(task => task.state is TrangaTask.ExecutionState.Running or TrangaTask.ExecutionState.Enqueued)) while(_allTasks.Any(task => task.state is TrangaTask.ExecutionState.Running or TrangaTask.ExecutionState.Enqueued))
Thread.Sleep(10); Thread.Sleep(10);
logger?.WriteLine(this.GetType().ToString(), "Tasks finished. Bye!");
Environment.Exit(0); Environment.Exit(0);
} }
/// <summary> /// <summary>
/// Loads stored data (settings, tasks) from file /// Loads stored data (settings, tasks) from file
/// </summary> /// </summary>
/// <param name="importFolderPath">working directory, filename has to be data.json</param> /// <param name="importFilePath">working directory, filename has to be data.json</param>
public static SettingsData LoadData(string importFolderPath) public static SettingsData LoadData(string importFilePath)
{ {
string importPath = Path.Join(importFolderPath, "data.json"); if (!File.Exists(importFilePath))
if (!File.Exists(importPath)) return new SettingsData(Directory.GetCurrentDirectory(), null, null, new HashSet<TrangaTask>());
return new SettingsData("", null, new HashSet<TrangaTask>());
string toRead = File.ReadAllText(importPath); string toRead = File.ReadAllText(importFilePath);
SettingsData data = JsonConvert.DeserializeObject<SettingsData>(toRead)!; SettingsData data = JsonConvert.DeserializeObject<SettingsData>(toRead)!;
return data; return data;
@ -236,27 +307,40 @@ public class TaskManager
/// <summary> /// <summary>
/// Exports data (settings, tasks) to file /// Exports data (settings, tasks) to file
/// </summary> /// </summary>
/// <param name="exportFolderPath">Folder path, filename will be data.json</param> private void ExportData()
private void ExportData(string exportFolderPath)
{ {
SettingsData data = new SettingsData(this.downloadLocation, this.komga, this._allTasks); logger?.WriteLine(this.GetType().ToString(), $"Exporting data to {settings.settingsFilePath}");
string exportPath = Path.Join(exportFolderPath, "data.json");
string serializedData = JsonConvert.SerializeObject(data);
File.WriteAllText(exportPath, serializedData); string serializedData = JsonConvert.SerializeObject(settings);
File.WriteAllText(settings.settingsFilePath, serializedData);
} }
public class SettingsData public class SettingsData
{ {
public string downloadLocation { get; set; } public string downloadLocation { get; private set; }
public Komga? komga { get; set; } public string settingsFilePath { get; }
public Komga? komga { get; private set; }
public HashSet<TrangaTask> allTasks { get; } public HashSet<TrangaTask> allTasks { get; }
public SettingsData(string downloadLocation, Komga? komga, HashSet<TrangaTask> allTasks) public SettingsData(string downloadLocation, string? settingsFilePath, Komga? komga, HashSet<TrangaTask> allTasks)
{ {
this.settingsFilePath = settingsFilePath ??
Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
"Tranga", "data.json");
this.downloadLocation = downloadLocation; this.downloadLocation = downloadLocation;
this.komga = komga; this.komga = komga;
this.allTasks = allTasks; this.allTasks = allTasks;
} }
public void UpdateSettings(string? pDownloadLocation, Komga? pKomga)
{
if(pDownloadLocation is not null)
this.downloadLocation = pDownloadLocation;
if(pKomga is not null)
this.komga = pKomga;
}
} }
} }

View File

@ -10,4 +10,8 @@
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Logging\Logging.csproj" />
</ItemGroup>
</Project> </Project>

View File

@ -56,6 +56,6 @@ public class TrangaTask
public override string ToString() public override string ToString()
{ {
return $"{task}\t{lastExecuted}\t{reoccurrence}\t{state}\t{connectorName}\t{publication?.sortName}"; return $"{task,-20} | {lastExecuted,-20} | {reoccurrence,-12} | {state,-10} | {connectorName,-15} | {publication?.sortName}";
} }
} }