diff --git a/Tranga-API/Dockerfile b/Tranga-API/Dockerfile new file mode 100644 index 0000000..6a0e453 --- /dev/null +++ b/Tranga-API/Dockerfile @@ -0,0 +1,20 @@ +FROM mcr.microsoft.com/dotnet/aspnet:7.0 AS base +WORKDIR /app +EXPOSE 80 +EXPOSE 443 + +FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build +WORKDIR /src +COPY ["Tranga-API/Tranga-API.csproj", "Tranga-API/"] +RUN dotnet restore "Tranga-API/Tranga-API.csproj" +COPY . . +WORKDIR "/src/Tranga-API" +RUN dotnet build "Tranga-API.csproj" -c Release -o /app/build + +FROM build AS publish +RUN dotnet publish "Tranga-API.csproj" -c Release -o /app/publish /p:UseAppHost=false + +FROM base AS final +WORKDIR /app +COPY --from=publish /app/publish . +ENTRYPOINT ["dotnet", "Tranga-API.dll"] diff --git a/Tranga-API/Program.cs b/Tranga-API/Program.cs new file mode 100644 index 0000000..b7f0a70 --- /dev/null +++ b/Tranga-API/Program.cs @@ -0,0 +1,122 @@ + +using Logging; +using Tranga; + +string applicationFolderPath = Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "Tranga-API"); +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; +if (File.Exists(settingsFilePath)) + settings = TaskManager.LoadData(settingsFilePath); +else + settings = new TaskManager.SettingsData(Directory.GetCurrentDirectory(), settingsFilePath, null, new HashSet()); + +TaskManager taskManager = new (settings, logger); + +var builder = WebApplication.CreateBuilder(args); +builder.Services.AddControllers().AddNewtonsoftJson(); +var app = builder.Build(); + +app.MapGet("/GetAvailableControllers", () => taskManager.GetAvailableConnectors()); + +app.MapGet("/GetKnownPublications", () => taskManager.GetAllPublications()); + +app.MapGet("/GetPublicationsFromConnector", (string connectorName, string title) => +{ + Connector? connector = taskManager.GetAvailableConnectors().FirstOrDefault(con => con.Key == connectorName).Value; + if (connector is null) + return Array.Empty(); + if(title.Length < 4) + return Array.Empty(); + return taskManager.GetPublicationsFromConnector(connector, title); +}); + +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 = taskManager.GetAllPublications().FirstOrDefault(pub => pub.internalId == publicationId); + TrangaTask.Task task = Enum.Parse(taskType); + taskManager.AddTask(task, connectorName, publication, TimeSpan.Parse(reoccurrenceTime), language??""); +}); + +app.MapPost("/Tasks/Delete", (string taskType, string? connectorName, string? publicationId) => +{ + Publication? publication = taskManager.GetAllPublications().FirstOrDefault(pub => pub.internalId == publicationId); + TrangaTask.Task task = Enum.Parse(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(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(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(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("/Setting/Update", (string? downloadLocation, string? komgaUrl, string? komgaAuth) => +{ + if(downloadLocation is not null) + taskManager.settings.downloadLocation = downloadLocation; + if(komgaUrl is not null && komgaAuth is not null) + taskManager.settings.komga = new Komga(komgaUrl, komgaAuth, logger); +}); + +app.Run(); + +class Settings +{ + public string downloadLocation; + public Komga? komga; + + public Settings(TaskManager.SettingsData settings) + { + this.downloadLocation = settings.downloadLocation; + this.komga = komga; + } +} \ No newline at end of file diff --git a/Tranga-API/Properties/launchSettings.json b/Tranga-API/Properties/launchSettings.json new file mode 100644 index 0000000..fd6862f --- /dev/null +++ b/Tranga-API/Properties/launchSettings.json @@ -0,0 +1,37 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:1716", + "sslPort": 44391 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:5177", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "https://localhost:7036;http://localhost:5177", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/Tranga-API/Tranga-API.csproj b/Tranga-API/Tranga-API.csproj new file mode 100644 index 0000000..3b1d193 --- /dev/null +++ b/Tranga-API/Tranga-API.csproj @@ -0,0 +1,26 @@ + + + + net7.0 + enable + enable + Tranga_API + Linux + + + + + .dockerignore + + + + + + + + + + + + + diff --git a/Tranga-API/appsettings.Development.json b/Tranga-API/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/Tranga-API/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/Tranga-API/appsettings.json b/Tranga-API/appsettings.json new file mode 100644 index 0000000..10f68b8 --- /dev/null +++ b/Tranga-API/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/Tranga.sln b/Tranga.sln index d67d5c6..7bf8e4f 100644 --- a/Tranga.sln +++ b/Tranga.sln @@ -6,6 +6,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tranga-CLI", "Tranga-CLI\Tr EndProject 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 Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -24,5 +26,9 @@ Global {415BE889-BB7D-426F-976F-8D977876A462}.Debug|Any CPU.Build.0 = Debug|Any CPU {415BE889-BB7D-426F-976F-8D977876A462}.Release|Any CPU.ActiveCfg = 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 EndGlobal diff --git a/Tranga/Connectors/MangaDex.cs b/Tranga/Connectors/MangaDex.cs index 56dcbee..251bc30 100644 --- a/Tranga/Connectors/MangaDex.cs +++ b/Tranga/Connectors/MangaDex.cs @@ -58,14 +58,12 @@ public class MangaDex : Connector : null; JsonArray altTitlesObject = attributes["altTitles"]!.AsArray(); - string[,] altTitles = new string[altTitlesObject.Count, 2]; - int titleIndex = 0; + Dictionary altTitlesDict = new(); foreach (JsonNode? altTitleNode in altTitlesObject) { JsonObject altTitleObject = (JsonObject)altTitleNode!; string key = ((IDictionary)altTitleObject).Keys.ToArray()[0]; - altTitles[titleIndex, 0] = key; - altTitles[titleIndex++, 1] = altTitleObject[key]!.GetValue(); + altTitlesDict.Add(key, altTitleObject[key]!.GetValue()); } JsonArray tagsObject = attributes["tags"]!.AsArray(); @@ -84,16 +82,14 @@ public class MangaDex : Connector poster = relationships.FirstOrDefault(relationship => relationship!["type"]!.GetValue() == "cover_art")!["id"]!.GetValue(); } + Dictionary linksDict = new(); string[,]? links = null; if (attributes.ContainsKey("links") && attributes["links"] is not null) { JsonObject linksObject = attributes["links"]!.AsObject(); - links = new string[linksObject.Count, 2]; - int linkIndex = 0; foreach (string key in ((IDictionary)linksObject).Keys) { - links[linkIndex, 0] = key; - links[linkIndex++, 1] = linksObject[key]!.GetValue(); + linksDict.Add(key, linksObject[key]!.GetValue()); } } @@ -110,14 +106,13 @@ public class MangaDex : Connector Publication pub = new Publication( title, description, - altTitles, + altTitlesDict, tags.ToArray(), poster, - links, + linksDict, year, originalLanguage, status, - manga["id"]!.GetValue(), manga["id"]!.GetValue() ); publications.Add(pub); //Add Publication (Manga) to result diff --git a/Tranga/Publication.cs b/Tranga/Publication.cs index b5c18a2..e3470a0 100644 --- a/Tranga/Publication.cs +++ b/Tranga/Publication.cs @@ -9,34 +9,38 @@ public readonly struct Publication { public string sortName { get; } // ReSharper disable UnusedAutoPropertyAccessor.Global we need it, trust - [JsonIgnore]public string[,] altTitles { get; } + [JsonIgnore]public Dictionary altTitles { get; } // ReSharper disable trice MemberCanBePrivate.Global, trust public string? description { get; } public string[] tags { get; } public string? posterUrl { get; } - [JsonIgnore]public string[,]? links { get; } + [JsonIgnore]public Dictionary links { get; } public int? year { get; } public string? originalLanguage { get; } public string status { get; } public string folderName { 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, string internalId) + public readonly struct ValueTuple + { + + } + + public Publication(string sortName, string? description, Dictionary altTitles, string[] tags, string? posterUrl, Dictionary? links, int? year, string? originalLanguage, string status, string downloadUrl) { this.sortName = sortName; this.description = description; this.altTitles = altTitles; this.tags = tags; this.posterUrl = posterUrl; - this.links = links; + this.links = links ?? new Dictionary(); this.year = year; this.originalLanguage = originalLanguage; this.status = status; this.downloadUrl = downloadUrl; this.folderName = string.Concat(sortName.Split(Path.GetInvalidPathChars().Concat(Path.GetInvalidFileNameChars()).ToArray())); - this.internalId = internalId; + this.internalId = Guid.NewGuid().ToString(); } /// Serialized JSON String for series.json diff --git a/Tranga/TaskExecutor.cs b/Tranga/TaskExecutor.cs index aa8ca7a..8f2c0d0 100644 --- a/Tranga/TaskExecutor.cs +++ b/Tranga/TaskExecutor.cs @@ -17,7 +17,7 @@ public static class TaskExecutor /// Current chapterCollection to update /// /// Is thrown when there is no Connector available with the name of the TrangaTask.connectorName - public static void Execute(TaskManager taskManager, TrangaTask trangaTask, Dictionary> chapterCollection, Logger? logger) + public static void Execute(TaskManager taskManager, TrangaTask trangaTask, Logger? logger) { //Only execute task if it is not already being executed. if (trangaTask.state == TrangaTask.ExecutionState.Running) @@ -37,13 +37,13 @@ public static class TaskExecutor switch (trangaTask.task) { case TrangaTask.Task.DownloadNewChapters: - DownloadNewChapters(connector!, (Publication)trangaTask.publication!, trangaTask.language, chapterCollection); + DownloadNewChapters(connector!, (Publication)trangaTask.publication!, trangaTask.language, ref taskManager._chapterCollection); break; case TrangaTask.Task.UpdateChapters: - UpdateChapters(connector!, (Publication)trangaTask.publication!, trangaTask.language, chapterCollection); + UpdateChapters(connector!, (Publication)trangaTask.publication!, trangaTask.language, ref taskManager._chapterCollection); break; case TrangaTask.Task.UpdatePublications: - UpdatePublications(connector!, chapterCollection); + UpdatePublications(connector!, ref taskManager._chapterCollection); break; case TrangaTask.Task.UpdateKomgaLibrary: UpdateKomgaLibrary(taskManager); @@ -75,7 +75,7 @@ public static class TaskExecutor /// /// Connector to receive Publications from /// - private static void UpdatePublications(Connector connector, Dictionary> chapterCollection) + private static void UpdatePublications(Connector connector, ref Dictionary> chapterCollection) { Publication[] publications = connector.GetPublications(); foreach (Publication publication in publications) @@ -90,9 +90,9 @@ public static class TaskExecutor /// Publication to check /// Language to receive chapters for /// - private static void DownloadNewChapters(Connector connector, Publication publication, string language, Dictionary> chapterCollection) + private static void DownloadNewChapters(Connector connector, Publication publication, string language, ref Dictionary> chapterCollection) { - List newChapters = UpdateChapters(connector, publication, language, chapterCollection); + List newChapters = UpdateChapters(connector, publication, language, ref chapterCollection); connector.DownloadCover(publication); //Check if Publication already has a Folder and a series.json @@ -116,7 +116,7 @@ public static class TaskExecutor /// Language to receive chapters for /// /// List of Chapters that were previously not in collection - private static List UpdateChapters(Connector connector, Publication publication, string language, Dictionary> chapterCollection) + private static List UpdateChapters(Connector connector, Publication publication, string language, ref Dictionary> chapterCollection) { List newChaptersList = new(); chapterCollection.TryAdd(publication, newChaptersList); //To ensure publication is actually in collection diff --git a/Tranga/TaskManager.cs b/Tranga/TaskManager.cs index 23f577a..2fb421d 100644 --- a/Tranga/TaskManager.cs +++ b/Tranga/TaskManager.cs @@ -10,7 +10,7 @@ namespace Tranga; /// public class TaskManager { - private readonly Dictionary> _chapterCollection = new(); + public Dictionary> _chapterCollection = new(); private readonly HashSet _allTasks; private bool _continueRunning = true; private readonly Connector[] _connectors; @@ -106,7 +106,7 @@ public class TaskManager logger?.WriteLine(this.GetType().ToString(), $"Forcing Execution: {task}"); Task t = new Task(() => { - TaskExecutor.Execute(this, task, this._chapterCollection, logger); + TaskExecutor.Execute(this, task, logger); }); t.Start(); } @@ -227,6 +227,14 @@ public class TaskManager _allTasks.CopyTo(ret); return ret; } + + public Publication[] GetPublicationsFromConnector(Connector connector, string? title = null) + { + Publication[] ret = connector.GetPublications(title ?? ""); + foreach(Publication publication in ret) + this._chapterCollection.TryAdd(publication, new List()); + return ret; + } /// All added Publications public Publication[] GetAllPublications() @@ -290,8 +298,11 @@ public class TaskManager private void ExportData() { logger?.WriteLine(this.GetType().ToString(), $"Exporting data to {settings.settingsFilePath}"); + + string serializedData = JsonConvert.SerializeObject(settings); + File.WriteAllText(settings.settingsFilePath, serializedData); }