25 Commits

Author SHA1 Message Date
6b4317834d StartNewChapterDownloadsWorker interval 1 minute 2025-07-03 23:06:35 +02:00
88fef8417c Fix request path 2025-07-03 23:04:32 +02:00
eb9fc08b2d Fix Scope/Context for Workers 2025-07-03 23:02:37 +02:00
9743bb6e8e Fix Worker-Cycle:
Periodic set last execution,
Print Running Worker-Names when done
2025-07-03 22:48:06 +02:00
e8d612557f Fix TrangaBaseContext.Sync 2025-07-03 22:39:06 +02:00
cf2dbeaf6a Fix RemoveOldNotificationsWorker.cs: RemoveRange 2025-07-03 21:57:07 +02:00
84940c414c Add Migrations
Add RemoveOldNotificationsWorker.cs
2025-07-03 21:55:38 +02:00
ea627081b8 Add default Tranga-Workers 2025-07-03 21:17:08 +02:00
a90a6fb200 Enable Manga Downloading 2025-07-03 21:11:33 +02:00
c3a0bb03e9 SettingsController set download language 2025-07-03 20:53:20 +02:00
f8ccd2d69e Tranga WorkerCycle 2025-07-03 20:51:06 +02:00
ad224190a2 UpdateMetadataWorker.cs 2025-07-03 20:38:29 +02:00
f05f2cc8e0 ToString overrides 2025-07-03 20:38:18 +02:00
d6f0630a99 StartNewChapterDownloadsWorker.cs 2025-07-03 20:21:48 +02:00
0ac4c23ac9 SendNotificationsWorker, CleanupMangaCoversWorker, UpdateChaptersDownloadedWorker add optional interval parameter 2025-07-03 20:14:18 +02:00
d6847d769e CheckForNewChaptersWorker 2025-07-03 20:11:18 +02:00
f6f5e21151 Move AddMangaToContext to Tranga.cs 2025-07-03 19:44:11 +02:00
da3b5078af SendNotificationsWorker.cs 2025-07-03 19:43:23 +02:00
681d56710a TrangaSettings as static field in Tranga instead of Static class 2025-07-03 17:30:58 +02:00
6f5823596a Tranga CheckRunning Workers 2025-07-02 22:34:12 +02:00
8a06ed648c BaseWorker Logging 2025-07-02 22:34:00 +02:00
4dcd6ee035 DbContext never null 2025-07-02 22:17:51 +02:00
e327e93163 BaseWorker, BaseWorkerWithContext DoWork, call: Scope setting
TrangaBaseContext Sync return with success state and exception message
2025-07-02 22:15:34 +02:00
6cd836540a IPeriodic non-generic 2025-07-02 21:12:47 +02:00
91c91e4989 Refactor Controllers
SettingsController.cs

SearchController.cs

QueryController.cs

NotificationConnectorController.cs

MetadataFetcherController.cs

MangaConnectorController.cs

FileLibraryController

LibraryConnectors

WorkerController
2025-07-02 21:12:47 +02:00
63 changed files with 2982 additions and 1141 deletions

View File

@ -1,14 +1,14 @@
namespace API.APIEndpointRecords; namespace API.APIEndpointRecords;
public record GotifyRecord(string endpoint, string appToken, int priority) public record GotifyRecord(string Name, string Endpoint, string AppToken, int Priority)
{ {
public bool Validate() public bool Validate()
{ {
if (endpoint == string.Empty) if (Endpoint == string.Empty)
return false; return false;
if (appToken == string.Empty) if (AppToken == string.Empty)
return false; return false;
if (priority < 0 || priority > 10) if (Priority < 0 || Priority > 10)
return false; return false;
return true; return true;

View File

@ -1,3 +0,0 @@
namespace API.APIEndpointRecords;
public record ModifyJobRecord(ulong? RecurrenceMs, bool? Enabled);

View File

@ -0,0 +1,3 @@
namespace API.APIEndpointRecords;
public record ModifyWorkerRecord(ulong? IntervalMs);

View File

@ -1,13 +0,0 @@
namespace API.APIEndpointRecords;
public record NewLibraryRecord(string path, string name)
{
public bool Validate()
{
if (path.Length < 1) //TODO Better Path validation
return false;
if (name.Length < 1)
return false;
return true;
}
}

View File

@ -1,16 +1,16 @@
namespace API.APIEndpointRecords; namespace API.APIEndpointRecords;
public record NtfyRecord(string endpoint, string username, string password, string topic, int priority) public record NtfyRecord(string Name, string Endpoint, string Username, string Password, string Topic, int Priority)
{ {
public bool Validate() public bool Validate()
{ {
if (endpoint == string.Empty) if (Endpoint == string.Empty)
return false; return false;
if (username == string.Empty) if (Username == string.Empty)
return false; return false;
if (password == string.Empty) if (Password == string.Empty)
return false; return false;
if (priority < 1 || priority > 5) if (Priority < 1 || Priority > 5)
return false; return false;
return true; return true;
} }

View File

@ -1,12 +1,12 @@
namespace API.APIEndpointRecords; namespace API.APIEndpointRecords;
public record PushoverRecord(string apptoken, string user) public record PushoverRecord(string Name, string AppToken, string User)
{ {
public bool Validate() public bool Validate()
{ {
if (apptoken == string.Empty) if (AppToken == string.Empty)
return false; return false;
if (user == string.Empty) if (User == string.Empty)
return false; return false;
return true; return true;
} }

View File

@ -0,0 +1,134 @@
using API.Schema.MangaContext;
using Asp.Versioning;
using Microsoft.AspNetCore.Mvc;
using static Microsoft.AspNetCore.Http.StatusCodes;
// ReSharper disable InconsistentNaming
namespace API.Controllers;
[ApiVersion(2)]
[ApiController]
[Route("v{v:apiVersion}/[controller]")]
public class FileLibraryController(MangaContext context) : Controller
{
/// <summary>
/// Returns all <see cref="FileLibrary"/>
/// </summary>
/// <response code="200"></response>
[HttpGet]
[ProducesResponseType<FileLibrary[]>(Status200OK, "application/json")]
public IActionResult GetFileLibraries()
{
return Ok(context.FileLibraries.ToArray());
}
/// <summary>
/// Returns <see cref="FileLibrary"/> with <paramref name="FileLibraryId"/>
/// </summary>
/// <param name="FileLibraryId"><see cref="FileLibrary"/>.Key</param>
/// <response code="200"></response>
/// <response code="404"><see cref="FileLibrary"/> with <paramref name="FileLibraryId"/> not found.</response>
[HttpGet("{FileLibraryId}")]
[ProducesResponseType<FileLibrary>(Status200OK, "application/json")]
[ProducesResponseType(Status404NotFound)]
public IActionResult GetFileLibrary(string FileLibraryId)
{
if (context.FileLibraries.Find(FileLibraryId) is not { } library)
return NotFound();
return Ok(library);
}
/// <summary>
/// Changes the <see cref="FileLibraryId"/>.BasePath with <paramref name="FileLibraryId"/>
/// </summary>
/// <param name="FileLibraryId"><see cref="FileLibrary"/>.Key</param>
/// <param name="newBasePath">New <see cref="FileLibraryId"/>.BasePath</param>
/// <response code="200"></response>
/// <response code="404"><see cref="FileLibrary"/> with <paramref name="FileLibraryId"/> not found.</response>
/// <response code="500">Error during Database Operation</response>
[HttpPatch("{FileLibraryId}/ChangeBasePath")]
[ProducesResponseType(Status200OK)]
[ProducesResponseType(Status404NotFound)]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public IActionResult ChangeLibraryBasePath(string FileLibraryId, [FromBody]string newBasePath)
{
if (context.FileLibraries.Find(FileLibraryId) is not { } library)
return NotFound();
//TODO Path check
library.BasePath = newBasePath;
if(context.Sync() is { success: false } result)
return StatusCode(Status500InternalServerError, result.exceptionMessage);
return Ok();
}
/// <summary>
/// Changes the <see cref="FileLibraryId"/>.LibraryName with <paramref name="FileLibraryId"/>
/// </summary>
/// <param name="FileLibraryId"><see cref="FileLibrary"/>.Key</param>
/// <param name="newName">New <see cref="FileLibraryId"/>.LibraryName</param>
/// <response code="200"></response>
/// <response code="404"><see cref="FileLibrary"/> with <paramref name="FileLibraryId"/> not found.</response>
/// <response code="500">Error during Database Operation</response>
[HttpPatch("{FileLibraryId}/ChangeName")]
[ProducesResponseType(Status200OK)]
[ProducesResponseType(Status404NotFound)]
[ProducesResponseType(Status400BadRequest)]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public IActionResult ChangeLibraryName(string FileLibraryId, [FromBody] string newName)
{
if (context.FileLibraries.Find(FileLibraryId) is not { } library)
return NotFound();
//TODO Name check
library.LibraryName = newName;
if(context.Sync() is { success: false } result)
return StatusCode(Status500InternalServerError, result.exceptionMessage);
return Ok();
}
/// <summary>
/// Creates new <see cref="FileLibraryId"/>
/// </summary>
/// <param name="library">New <see cref="FileLibrary"/> to add</param>
/// <response code="200"></response>
/// <response code="500">Error during Database Operation</response>
[HttpPut]
[ProducesResponseType(Status201Created)]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public IActionResult CreateNewLibrary([FromBody]FileLibrary library)
{
//TODO Parameter check
context.FileLibraries.Add(library);
if(context.Sync() is { success: false } result)
return StatusCode(Status500InternalServerError, result.exceptionMessage);
return Created();
}
/// <summary>
/// Deletes the <see cref="FileLibraryId"/>.LibraryName with <paramref name="FileLibraryId"/>
/// </summary>
/// <param name="FileLibraryId"><see cref="FileLibrary"/>.Key</param>
/// <response code="200"></response>
/// <response code="500">Error during Database Operation</response>
[HttpDelete("{FileLibraryId}")]
[ProducesResponseType(Status200OK)]
[ProducesResponseType(Status404NotFound)]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public IActionResult DeleteLocalLibrary(string FileLibraryId)
{
if (context.FileLibraries.Find(FileLibraryId) is not { } library)
return NotFound();
context.FileLibraries.Remove(library);
if(context.Sync() is { success: false } result)
return StatusCode(Status500InternalServerError, result.exceptionMessage);
return Ok();
}
}

View File

@ -1,19 +1,19 @@
using API.Schema.LibraryContext; using API.Schema.LibraryContext;
using API.Schema.LibraryContext.LibraryConnectors; using API.Schema.LibraryContext.LibraryConnectors;
using Asp.Versioning; using Asp.Versioning;
using log4net;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using static Microsoft.AspNetCore.Http.StatusCodes; using static Microsoft.AspNetCore.Http.StatusCodes;
// ReSharper disable InconsistentNaming
namespace API.Controllers; namespace API.Controllers;
[ApiVersion(2)] [ApiVersion(2)]
[ApiController] [ApiController]
[Route("v{v:apiVersion}/[controller]")] [Route("v{v:apiVersion}/[controller]")]
public class LibraryConnectorController(LibraryContext context, ILog Log) : Controller public class LibraryConnectorController(LibraryContext context) : Controller
{ {
/// <summary> /// <summary>
/// Gets all configured ToFileLibrary-Connectors /// Gets all configured <see cref="LibraryConnector"/>
/// </summary> /// </summary>
/// <response code="200"></response> /// <response code="200"></response>
[HttpGet] [HttpGet]
@ -21,32 +21,31 @@ public class LibraryConnectorController(LibraryContext context, ILog Log) : Cont
public IActionResult GetAllConnectors() public IActionResult GetAllConnectors()
{ {
LibraryConnector[] connectors = context.LibraryConnectors.ToArray(); LibraryConnector[] connectors = context.LibraryConnectors.ToArray();
return Ok(connectors); return Ok(connectors);
} }
/// <summary> /// <summary>
/// Returns ToFileLibrary-Connector with requested ID /// Returns <see cref="LibraryConnector"/> with <paramref name="LibraryConnectorId"/>
/// </summary> /// </summary>
/// <param name="LibraryControllerId">ToFileLibrary-Connector-ID</param> /// <param name="LibraryConnectorId"><see cref="LibraryConnector"/>.Key</param>
/// <response code="200"></response> /// <response code="200"></response>
/// <response code="404">Connector with ID not found.</response> /// <response code="404"><see cref="LibraryConnector"/> with <paramref name="LibraryConnectorId"/> not found.</response>
[HttpGet("{LibraryControllerId}")] [HttpGet("{LibraryConnectorId}")]
[ProducesResponseType<LibraryConnector>(Status200OK, "application/json")] [ProducesResponseType<LibraryConnector>(Status200OK, "application/json")]
[ProducesResponseType(Status404NotFound)] [ProducesResponseType(Status404NotFound)]
public IActionResult GetConnector(string LibraryControllerId) public IActionResult GetConnector(string LibraryConnectorId)
{ {
LibraryConnector? ret = context.LibraryConnectors.Find(LibraryControllerId); if (context.LibraryConnectors.Find(LibraryConnectorId) is not { } connector)
return (ret is not null) switch return NotFound();
{
true => Ok(ret), return Ok(connector);
false => NotFound()
};
} }
/// <summary> /// <summary>
/// Creates a new ToFileLibrary-Connector /// Creates a new <see cref="LibraryConnector"/>
/// </summary> /// </summary>
/// <param name="libraryConnector">ToFileLibrary-Connector</param> /// <param name="libraryConnector"></param>
/// <response code="201"></response> /// <response code="201"></response>
/// <response code="500">Error during Database Operation</response> /// <response code="500">Error during Database Operation</response>
[HttpPut] [HttpPut]
@ -54,46 +53,34 @@ public class LibraryConnectorController(LibraryContext context, ILog Log) : Cont
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")] [ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public IActionResult CreateConnector([FromBody]LibraryConnector libraryConnector) public IActionResult CreateConnector([FromBody]LibraryConnector libraryConnector)
{ {
try
{ context.LibraryConnectors.Add(libraryConnector);
context.LibraryConnectors.Add(libraryConnector);
context.SaveChanges(); if(context.Sync() is { success: false } result)
return Created(); return StatusCode(Status500InternalServerError, result.exceptionMessage);
} return Created();
catch (Exception e)
{
Log.Error(e);
return StatusCode(500, e.Message);
}
} }
/// <summary> /// <summary>
/// Deletes the ToFileLibrary-Connector with the requested ID /// Deletes <see cref="LibraryConnector"/> with <paramref name="LibraryConnectorId"/>
/// </summary> /// </summary>
/// <param name="LibraryControllerId">ToFileLibrary-Connector-ID</param> /// <param name="LibraryConnectorId">ToFileLibrary-Connector-ID</param>
/// <response code="200"></response> /// <response code="200"></response>
/// <response code="404">Connector with ID not found.</response> /// <response code="404"><see cref="LibraryConnector"/> with <<paramref name="LibraryConnectorId"/> not found.</response>
/// <response code="500">Error during Database Operation</response> /// <response code="500">Error during Database Operation</response>
[HttpDelete("{LibraryControllerId}")] [HttpDelete("{LibraryConnectorId}")]
[ProducesResponseType(Status200OK)] [ProducesResponseType(Status200OK)]
[ProducesResponseType(Status404NotFound)] [ProducesResponseType(Status404NotFound)]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")] [ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public IActionResult DeleteConnector(string LibraryControllerId) public IActionResult DeleteConnector(string LibraryConnectorId)
{ {
try if (context.LibraryConnectors.Find(LibraryConnectorId) is not { } connector)
{ return NotFound();
LibraryConnector? ret = context.LibraryConnectors.Find(LibraryControllerId);
if (ret is null) context.LibraryConnectors.Remove(connector);
return NotFound();
if(context.Sync() is { success: false } result)
context.Remove(ret); return StatusCode(Status500InternalServerError, result.exceptionMessage);
context.SaveChanges(); return Ok();
return Ok();
}
catch (Exception e)
{
Log.Error(e);
return StatusCode(500, e.Message);
}
} }
} }

View File

@ -1,163 +0,0 @@
using API.APIEndpointRecords;
using API.Schema.MangaContext;
using Asp.Versioning;
using log4net;
using Microsoft.AspNetCore.Mvc;
using static Microsoft.AspNetCore.Http.StatusCodes;
namespace API.Controllers;
[ApiVersion(2)]
[ApiController]
[Route("v{v:apiVersion}/[controller]")]
public class LocalLibrariesController(MangaContext context, ILog Log) : Controller
{
[HttpGet]
[ProducesResponseType<FileLibrary[]>(Status200OK, "application/json")]
public IActionResult GetLocalLibraries()
{
return Ok(context.LocalLibraries);
}
[HttpGet("{LibraryId}")]
[ProducesResponseType<FileLibrary>(Status200OK, "application/json")]
[ProducesResponseType(Status404NotFound)]
public IActionResult GetLocalLibrary(string LibraryId)
{
FileLibrary? library = context.LocalLibraries.Find(LibraryId);
if (library is null)
return NotFound();
return Ok(library);
}
[HttpPatch("{LibraryId}")]
[ProducesResponseType(Status200OK)]
[ProducesResponseType(Status404NotFound)]
[ProducesResponseType(Status400BadRequest)]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public IActionResult UpdateLocalLibrary(string LibraryId, [FromBody]NewLibraryRecord record)
{
FileLibrary? library = context.LocalLibraries.Find(LibraryId);
if (library is null)
return NotFound();
if (record.Validate() == false)
return BadRequest();
try
{
library.LibraryName = record.name;
library.BasePath = record.path;
context.SaveChanges();
return Ok();
}
catch (Exception e)
{
Log.Error(e);
return StatusCode(500, e.Message);
}
}
[HttpPatch("{LibraryId}/ChangeBasePath")]
[ProducesResponseType(Status200OK)]
[ProducesResponseType(Status404NotFound)]
[ProducesResponseType(Status400BadRequest)]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public IActionResult ChangeLibraryBasePath(string LibraryId, [FromBody] string newBasePath)
{
try
{
FileLibrary? library = context.LocalLibraries.Find(LibraryId);
if (library is null)
return NotFound();
if (false) //TODO implement path check
return BadRequest();
library.BasePath = newBasePath;
context.SaveChanges();
return Ok();
}
catch (Exception e)
{
Log.Error(e);
return StatusCode(500, e.Message);
}
}
[HttpPatch("{LibraryId}/ChangeName")]
[ProducesResponseType(Status200OK)]
[ProducesResponseType(Status404NotFound)]
[ProducesResponseType(Status400BadRequest)]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public IActionResult ChangeLibraryName(string LibraryId, [FromBody] string newName)
{
try
{
FileLibrary? library = context.LocalLibraries.Find(LibraryId);
if (library is null)
return NotFound();
if(newName.Length < 1)
return BadRequest();
library.LibraryName = newName;
context.SaveChanges();
return Ok();
}
catch (Exception e)
{
Log.Error(e);
return StatusCode(500, e.Message);
}
}
[HttpPut]
[ProducesResponseType<FileLibrary>(Status200OK, "application/json")]
[ProducesResponseType(Status400BadRequest)]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public IActionResult CreateNewLibrary([FromBody]NewLibraryRecord library)
{
if (library.Validate() == false)
return BadRequest();
try
{
FileLibrary newFileLibrary = new (library.path, library.name);
context.LocalLibraries.Add(newFileLibrary);
context.SaveChanges();
return Ok(newFileLibrary);
}
catch (Exception e)
{
Log.Error(e);
return StatusCode(500, e.Message);
}
}
[HttpDelete("{LibraryId}")]
[ProducesResponseType(Status200OK)]
[ProducesResponseType(Status404NotFound)]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public IActionResult DeleteLocalLibrary(string LibraryId)
{
try
{
FileLibrary? library = context.LocalLibraries.Find(LibraryId);
if (library is null)
return NotFound();
context.Remove(library);
context.SaveChanges();
return Ok();
}
catch (Exception e)
{
Log.Error(e);
return StatusCode(500, e.Message);
}
}
}

View File

@ -1,107 +1,90 @@
using API.Schema.MangaContext; using API.Schema.MangaContext;
using API.Schema.MangaContext.MangaConnectors; using API.Schema.MangaContext.MangaConnectors;
using Asp.Versioning; using Asp.Versioning;
using log4net;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using static Microsoft.AspNetCore.Http.StatusCodes; using static Microsoft.AspNetCore.Http.StatusCodes;
// ReSharper disable InconsistentNaming
namespace API.Controllers; namespace API.Controllers;
[ApiVersion(2)] [ApiVersion(2)]
[ApiController] [ApiController]
[Route("v{v:apiVersion}/[controller]")] [Route("v{v:apiVersion}/[controller]")]
public class MangaConnectorController(MangaContext context, ILog Log) : Controller public class MangaConnectorController(MangaContext context) : Controller
{ {
/// <summary> /// <summary>
/// Get all available Connectors (Scanlation-Sites) /// Get all <see cref="MangaConnector"/> (Scanlation-Sites)
/// </summary> /// </summary>
/// <response code="200"></response> /// <response code="200">Names of <see cref="MangaConnector"/> (Scanlation-Sites)</response>
[HttpGet] [HttpGet]
[ProducesResponseType<MangaConnector[]>(Status200OK, "application/json")] [ProducesResponseType<MangaConnector[]>(Status200OK, "application/json")]
public IActionResult GetConnectors() public IActionResult GetConnectors()
{ {
MangaConnector[] connectors = context.MangaConnectors.ToArray(); return Ok(context.MangaConnectors.Select(c => c.Name).ToArray());
return Ok(connectors);
} }
/// <summary> /// <summary>
/// Returns the MangaConnector with the requested Name /// Returns the <see cref="MangaConnector"/> (Scanlation-Sites) with the requested Name
/// </summary> /// </summary>
/// <param name="MangaConnectorName"></param> /// <param name="MangaConnectorName"><see cref="MangaConnector"/>.Name</param>
/// <response code="200"></response> /// <response code="200"></response>
/// <response code="404">Connector with ID not found.</response> /// <response code="404"><see cref="MangaConnector"/> (Scanlation-Sites) with Name not found.</response>
/// <response code="500">Error during Database Operation</response>
[HttpGet("{MangaConnectorName}")] [HttpGet("{MangaConnectorName}")]
[ProducesResponseType<MangaConnector>(Status200OK, "application/json")] [ProducesResponseType<MangaConnector>(Status200OK, "application/json")]
[ProducesResponseType(Status404NotFound)]
public IActionResult GetConnector(string MangaConnectorName) public IActionResult GetConnector(string MangaConnectorName)
{ {
try if(context.MangaConnectors.Find(MangaConnectorName) is not { } connector)
{ return NotFound();
if(context.MangaConnectors.Find(MangaConnectorName) is not { } connector)
return NotFound(); return Ok(connector);
return Ok(connector);
}
catch (Exception e)
{
Log.Error(e);
return StatusCode(500, e.Message);
}
} }
/// <summary> /// <summary>
/// Get all enabled Connectors (Scanlation-Sites) /// Get all enabled <see cref="MangaConnector"/> (Scanlation-Sites)
/// </summary> /// </summary>
/// <response code="200"></response> /// <response code="200"></response>
[HttpGet("enabled")] [HttpGet("Enabled")]
[ProducesResponseType<MangaConnector[]>(Status200OK, "application/json")] [ProducesResponseType<MangaConnector[]>(Status200OK, "application/json")]
public IActionResult GetEnabledConnectors() public IActionResult GetEnabledConnectors()
{ {
MangaConnector[] connectors = context.MangaConnectors.Where(c => c.Enabled == true).ToArray();
return Ok(connectors); return Ok(context.MangaConnectors.Where(c => c.Enabled).ToArray());
} }
/// <summary> /// <summary>
/// Get all disabled Connectors (Scanlation-Sites) /// Get all disabled <see cref="MangaConnector"/> (Scanlation-Sites)
/// </summary> /// </summary>
/// <response code="200"></response> /// <response code="200"></response>
[HttpGet("disabled")] [HttpGet("Disabled")]
[ProducesResponseType<MangaConnector[]>(Status200OK, "application/json")] [ProducesResponseType<MangaConnector[]>(Status200OK, "application/json")]
public IActionResult GetDisabledConnectors() public IActionResult GetDisabledConnectors()
{ {
MangaConnector[] connectors = context.MangaConnectors.Where(c => c.Enabled == false).ToArray();
return Ok(connectors); return Ok(context.MangaConnectors.Where(c => c.Enabled == false).ToArray());
} }
/// <summary> /// <summary>
/// Enabled or disables a Connector /// Enabled or disables <see cref="MangaConnector"/> (Scanlation-Sites) with Name
/// </summary> /// </summary>
/// <param name="MangaConnectorName">ID of the connector</param> /// <param name="MangaConnectorName"><see cref="MangaConnector"/>.Name</param>
/// <param name="enabled">Set true to enable</param> /// <param name="Enabled">Set true to enable, false to disable</param>
/// <response code="200"></response> /// <response code="202"></response>
/// <response code="404">Connector with ID not found.</response> /// <response code="404"><see cref="MangaConnector"/> (Scanlation-Sites) with Name not found.</response>
/// <response code="500">Error during Database Operation</response> /// <response code="500">Error during Database Operation</response>
[HttpPatch("{MangaConnectorName}/SetEnabled/{enabled}")] [HttpPatch("{MangaConnectorName}/SetEnabled/{Enabled}")]
[ProducesResponseType(Status200OK)] [ProducesResponseType(Status202Accepted)]
[ProducesResponseType(Status404NotFound)] [ProducesResponseType(Status404NotFound)]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")] [ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public IActionResult SetEnabled(string MangaConnectorName, bool enabled) public IActionResult SetEnabled(string MangaConnectorName, bool Enabled)
{ {
try if(context.MangaConnectors.Find(MangaConnectorName) is not { } connector)
{ return NotFound();
MangaConnector? connector = context.MangaConnectors.Find(MangaConnectorName);
if (connector is null) connector.Enabled = Enabled;
return NotFound();
if(context.Sync() is { success: false } result)
connector.Enabled = enabled; return StatusCode(Status500InternalServerError, result.exceptionMessage);
context.SaveChanges(); return Accepted();
return Ok();
}
catch (Exception e)
{
Log.Error(e);
return StatusCode(500, e.Message);
}
} }
} }

View File

@ -2,9 +2,7 @@
using API.Schema.MangaContext.MangaConnectors; using API.Schema.MangaContext.MangaConnectors;
using API.Workers; using API.Workers;
using Asp.Versioning; using Asp.Versioning;
using log4net;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Net.Http.Headers; using Microsoft.Net.Http.Headers;
using SixLabors.ImageSharp; using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Formats.Jpeg; using SixLabors.ImageSharp.Formats.Jpeg;
@ -18,7 +16,7 @@ namespace API.Controllers;
[ApiVersion(2)] [ApiVersion(2)]
[ApiController] [ApiController]
[Route("v{v:apiVersion}/[controller]")] [Route("v{v:apiVersion}/[controller]")]
public class MangaController(IServiceScope scope) : Controller public class MangaController(MangaContext context) : Controller
{ {
/// <summary> /// <summary>
/// Returns all cached <see cref="Manga"/> /// Returns all cached <see cref="Manga"/>
@ -28,7 +26,6 @@ public class MangaController(IServiceScope scope) : Controller
[ProducesResponseType<Manga[]>(Status200OK, "application/json")] [ProducesResponseType<Manga[]>(Status200OK, "application/json")]
public IActionResult GetAllManga() public IActionResult GetAllManga()
{ {
MangaContext context = scope.ServiceProvider.GetRequiredService<MangaContext>();
Manga[] ret = context.Mangas.ToArray(); Manga[] ret = context.Mangas.ToArray();
return Ok(ret); return Ok(ret);
} }
@ -42,7 +39,6 @@ public class MangaController(IServiceScope scope) : Controller
[ProducesResponseType<Manga[]>(Status200OK, "application/json")] [ProducesResponseType<Manga[]>(Status200OK, "application/json")]
public IActionResult GetManga([FromBody]string[] MangaIds) public IActionResult GetManga([FromBody]string[] MangaIds)
{ {
MangaContext context = scope.ServiceProvider.GetRequiredService<MangaContext>();
Manga[] ret = context.Mangas.Where(m => MangaIds.Contains(m.Key)).ToArray(); Manga[] ret = context.Mangas.Where(m => MangaIds.Contains(m.Key)).ToArray();
return Ok(ret); return Ok(ret);
} }
@ -58,7 +54,6 @@ public class MangaController(IServiceScope scope) : Controller
[ProducesResponseType(Status404NotFound)] [ProducesResponseType(Status404NotFound)]
public IActionResult GetManga(string MangaId) public IActionResult GetManga(string MangaId)
{ {
MangaContext context = scope.ServiceProvider.GetRequiredService<MangaContext>();
if (context.Mangas.Find(MangaId) is not { } manga) if (context.Mangas.Find(MangaId) is not { } manga)
return NotFound(nameof(MangaId)); return NotFound(nameof(MangaId));
return Ok(manga); return Ok(manga);
@ -77,14 +72,13 @@ public class MangaController(IServiceScope scope) : Controller
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")] [ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public IActionResult DeleteManga(string MangaId) public IActionResult DeleteManga(string MangaId)
{ {
MangaContext context = scope.ServiceProvider.GetRequiredService<MangaContext>();
if (context.Mangas.Find(MangaId) is not { } manga) if (context.Mangas.Find(MangaId) is not { } manga)
return NotFound(nameof(MangaId)); return NotFound(nameof(MangaId));
context.Mangas.Remove(manga); context.Mangas.Remove(manga);
if(context.Sync().Result is { } errorMessage) if(context.Sync() is { success: false } result)
return StatusCode(Status500InternalServerError, errorMessage); return StatusCode(Status500InternalServerError, result.exceptionMessage);
return Ok(); return Ok();
} }
@ -101,7 +95,6 @@ public class MangaController(IServiceScope scope) : Controller
[ProducesResponseType(Status404NotFound)] [ProducesResponseType(Status404NotFound)]
public IActionResult MergeIntoManga(string MangaIdFrom, string MangaIdInto) public IActionResult MergeIntoManga(string MangaIdFrom, string MangaIdInto)
{ {
MangaContext context = scope.ServiceProvider.GetRequiredService<MangaContext>();
if (context.Mangas.Find(MangaIdFrom) is not { } from) if (context.Mangas.Find(MangaIdFrom) is not { } from)
return NotFound(nameof(MangaIdFrom)); return NotFound(nameof(MangaIdFrom));
if (context.Mangas.Find(MangaIdInto) is not { } into) if (context.Mangas.Find(MangaIdInto) is not { } into)
@ -132,16 +125,15 @@ public class MangaController(IServiceScope scope) : Controller
[ProducesResponseType<int>(Status503ServiceUnavailable, "text/plain")] [ProducesResponseType<int>(Status503ServiceUnavailable, "text/plain")]
public IActionResult GetCover(string MangaId, [FromQuery]int? width, [FromQuery]int? height) public IActionResult GetCover(string MangaId, [FromQuery]int? width, [FromQuery]int? height)
{ {
MangaContext context = scope.ServiceProvider.GetRequiredService<MangaContext>();
if (context.Mangas.Find(MangaId) is not { } manga) if (context.Mangas.Find(MangaId) is not { } manga)
return NotFound(nameof(MangaId)); return NotFound(nameof(MangaId));
if (!System.IO.File.Exists(manga.CoverFileNameInCache)) if (!System.IO.File.Exists(manga.CoverFileNameInCache))
{ {
if (Tranga.GetRunningWorkers().Any(worker => worker is DownloadCoverFromMangaconnectorWorker w && w.MangaConnectorId.ObjId == MangaId)) if (Tranga.GetRunningWorkers().Any(worker => worker is DownloadCoverFromMangaconnectorWorker w && context.MangaConnectorToManga.Find(w.MangaConnectorIdId)?.ObjId == MangaId))
{ {
Response.Headers.Append("Retry-After", $"{TrangaSettings.workCycleTimeout * 2 / 1000:D}"); Response.Headers.Append("Retry-After", $"{Tranga.Settings.WorkCycleTimeoutMs * 2 / 1000:D}");
return StatusCode(Status503ServiceUnavailable, TrangaSettings.workCycleTimeout * 2 / 1000); return StatusCode(Status503ServiceUnavailable, Tranga.Settings.WorkCycleTimeoutMs * 2 / 1000);
} }
else else
return NoContent(); return NoContent();
@ -178,7 +170,6 @@ public class MangaController(IServiceScope scope) : Controller
[ProducesResponseType(Status404NotFound)] [ProducesResponseType(Status404NotFound)]
public IActionResult GetChapters(string MangaId) public IActionResult GetChapters(string MangaId)
{ {
MangaContext context = scope.ServiceProvider.GetRequiredService<MangaContext>();
if (context.Mangas.Find(MangaId) is not { } manga) if (context.Mangas.Find(MangaId) is not { } manga)
return NotFound(nameof(MangaId)); return NotFound(nameof(MangaId));
@ -199,7 +190,6 @@ public class MangaController(IServiceScope scope) : Controller
[ProducesResponseType(Status404NotFound)] [ProducesResponseType(Status404NotFound)]
public IActionResult GetChaptersDownloaded(string MangaId) public IActionResult GetChaptersDownloaded(string MangaId)
{ {
MangaContext context = scope.ServiceProvider.GetRequiredService<MangaContext>();
if (context.Mangas.Find(MangaId) is not { } manga) if (context.Mangas.Find(MangaId) is not { } manga)
return NotFound(nameof(MangaId)); return NotFound(nameof(MangaId));
@ -223,7 +213,6 @@ public class MangaController(IServiceScope scope) : Controller
[ProducesResponseType(Status404NotFound)] [ProducesResponseType(Status404NotFound)]
public IActionResult GetChaptersNotDownloaded(string MangaId) public IActionResult GetChaptersNotDownloaded(string MangaId)
{ {
MangaContext context = scope.ServiceProvider.GetRequiredService<MangaContext>();
if (context.Mangas.Find(MangaId) is not { } manga) if (context.Mangas.Find(MangaId) is not { } manga)
return NotFound(nameof(MangaId)); return NotFound(nameof(MangaId));
@ -251,17 +240,16 @@ public class MangaController(IServiceScope scope) : Controller
[ProducesResponseType<int>(Status503ServiceUnavailable, "text/plain")] [ProducesResponseType<int>(Status503ServiceUnavailable, "text/plain")]
public IActionResult GetLatestChapter(string MangaId) public IActionResult GetLatestChapter(string MangaId)
{ {
MangaContext context = scope.ServiceProvider.GetRequiredService<MangaContext>();
if (context.Mangas.Find(MangaId) is not { } manga) if (context.Mangas.Find(MangaId) is not { } manga)
return NotFound(nameof(MangaId)); return NotFound(nameof(MangaId));
List<Chapter> chapters = manga.Chapters.ToList(); List<Chapter> chapters = manga.Chapters.ToList();
if (chapters.Count == 0) if (chapters.Count == 0)
{ {
if (Tranga.GetRunningWorkers().Any(worker => worker is RetrieveMangaChaptersFromMangaconnectorWorker w && w.MangaConnectorId.ObjId == MangaId && w.State < WorkerExecutionState.Completed)) if (Tranga.GetRunningWorkers().Any(worker => worker is RetrieveMangaChaptersFromMangaconnectorWorker w && context.MangaConnectorToManga.Find(w.MangaConnectorIdId)?.ObjId == MangaId && w.State < WorkerExecutionState.Completed))
{ {
Response.Headers.Append("Retry-After", $"{TrangaSettings.workCycleTimeout * 2 / 1000:D}"); Response.Headers.Append("Retry-After", $"{Tranga.Settings.WorkCycleTimeoutMs * 2 / 1000:D}");
return StatusCode(Status503ServiceUnavailable, TrangaSettings.workCycleTimeout * 2/ 1000); return StatusCode(Status503ServiceUnavailable, Tranga.Settings.WorkCycleTimeoutMs * 2/ 1000);
}else }else
return Ok(0); return Ok(0);
} }
@ -290,17 +278,16 @@ public class MangaController(IServiceScope scope) : Controller
[ProducesResponseType<int>(Status503ServiceUnavailable, "text/plain")] [ProducesResponseType<int>(Status503ServiceUnavailable, "text/plain")]
public IActionResult GetLatestChapterDownloaded(string MangaId) public IActionResult GetLatestChapterDownloaded(string MangaId)
{ {
MangaContext context = scope.ServiceProvider.GetRequiredService<MangaContext>();
if (context.Mangas.Find(MangaId) is not { } manga) if (context.Mangas.Find(MangaId) is not { } manga)
return NotFound(nameof(MangaId)); return NotFound(nameof(MangaId));
List<Chapter> chapters = manga.Chapters.ToList(); List<Chapter> chapters = manga.Chapters.ToList();
if (chapters.Count == 0) if (chapters.Count == 0)
{ {
if (Tranga.GetRunningWorkers().Any(worker => worker is RetrieveMangaChaptersFromMangaconnectorWorker w && w.MangaConnectorId.ObjId == MangaId && w.State < WorkerExecutionState.Completed)) if (Tranga.GetRunningWorkers().Any(worker => worker is RetrieveMangaChaptersFromMangaconnectorWorker w && context.MangaConnectorToManga.Find(w.MangaConnectorIdId)?.ObjId == MangaId && w.State < WorkerExecutionState.Completed))
{ {
Response.Headers.Append("Retry-After", $"{TrangaSettings.workCycleTimeout * 2 / 1000:D}"); Response.Headers.Append("Retry-After", $"{Tranga.Settings.WorkCycleTimeoutMs * 2 / 1000:D}");
return StatusCode(Status503ServiceUnavailable, TrangaSettings.workCycleTimeout * 2/ 1000); return StatusCode(Status503ServiceUnavailable, Tranga.Settings.WorkCycleTimeoutMs * 2/ 1000);
}else }else
return NoContent(); return NoContent();
} }
@ -326,13 +313,12 @@ public class MangaController(IServiceScope scope) : Controller
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")] [ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public IActionResult IgnoreChaptersBefore(string MangaId, [FromBody]float chapterThreshold) public IActionResult IgnoreChaptersBefore(string MangaId, [FromBody]float chapterThreshold)
{ {
MangaContext context = scope.ServiceProvider.GetRequiredService<MangaContext>();
if (context.Mangas.Find(MangaId) is not { } manga) if (context.Mangas.Find(MangaId) is not { } manga)
return NotFound(); return NotFound();
manga.IgnoreChaptersBefore = chapterThreshold; manga.IgnoreChaptersBefore = chapterThreshold;
if(context.Sync().Result is { } errorMessage) if(context.Sync() is { success: false } result)
return StatusCode(Status500InternalServerError, errorMessage); return StatusCode(Status500InternalServerError, result.exceptionMessage);
return Accepted(); return Accepted();
} }
@ -349,17 +335,56 @@ public class MangaController(IServiceScope scope) : Controller
[ProducesResponseType(Status404NotFound)] [ProducesResponseType(Status404NotFound)]
public IActionResult MoveFolder(string MangaId, string LibraryId) public IActionResult MoveFolder(string MangaId, string LibraryId)
{ {
MangaContext context = scope.ServiceProvider.GetRequiredService<MangaContext>();
if (context.Mangas.Find(MangaId) is not { } manga) if (context.Mangas.Find(MangaId) is not { } manga)
return NotFound(nameof(MangaId)); return NotFound(nameof(MangaId));
if(context.LocalLibraries.Find(LibraryId) is not { } library) if(context.FileLibraries.Find(LibraryId) is not { } library)
return NotFound(nameof(LibraryId)); return NotFound(nameof(LibraryId));
MoveMangaLibraryWorker moveLibrary = new(manga, library, scope); MoveMangaLibraryWorker moveLibrary = new(manga, library);
UpdateChaptersDownloadedWorker updateDownloadedFiles = new(manga, scope, [moveLibrary]);
Tranga.AddWorkers([moveLibrary, updateDownloadedFiles]); Tranga.AddWorkers([moveLibrary]);
return Accepted(); return Accepted();
} }
/// <summary>
/// (Un-)Marks <see cref="Manga"/> as requested for Download from <see cref="MangaConnector"/>
/// </summary>
/// <param name="MangaId"><see cref="Manga"/> with <paramref name="MangaId"/></param>
/// <param name="MangaConnectorName"><see cref="MangaConnector"/> with <paramref name="MangaConnectorName"/></param>
/// <param name="IsRequested">true to mark as requested, false to mark as not-requested</param>
/// <response code="200"></response>
/// <response code="404"><paramref name="MangaId"/> or <paramref name="MangaConnectorName"/> not found</response>
/// <response code="412"><see cref="Manga"/> was not linked to <see cref="MangaConnector"/>, so nothing changed</response>
/// <response code="428"><see cref="Manga"/> is not linked to <see cref="MangaConnector"/> yet. Search for <see cref="Manga"/> on <see cref="MangaConnector"/> first (to create a <see cref="MangaConnectorId{T}"/>).</response>
/// <response code="500">Error during Database Operation</response>
[HttpPost("{MangaId}/SetAsDownloadFrom/{MangaConnectorName}/{IsRequested}")]
[ProducesResponseType(Status200OK)]
[ProducesResponseType<string>(Status404NotFound, "text/plain")]
[ProducesResponseType<string>(Status412PreconditionFailed, "text/plain")]
[ProducesResponseType<string>(Status428PreconditionRequired, "text/plain")]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public IActionResult MarkAsRequested(string MangaId, string MangaConnectorName, bool IsRequested)
{
if (context.Mangas.Find(MangaId) is null)
return NotFound(nameof(MangaId));
if(context.MangaConnectors.Find(MangaConnectorName) is null)
return NotFound(nameof(MangaConnectorName));
if (context.MangaConnectorToManga.FirstOrDefault(id => id.MangaConnectorName == MangaConnectorName && id.ObjId == MangaId) is not { } mcId)
if(IsRequested)
return StatusCode(Status428PreconditionRequired, "Don't know how to download this Manga from MangaConnector");
else
return StatusCode(Status412PreconditionFailed, "Not linked anyways.");
mcId.UseForDownload = IsRequested;
if(context.Sync() is { success: false } result)
return StatusCode(Status500InternalServerError, result.exceptionMessage);
DownloadCoverFromMangaconnectorWorker downloadCover = new(mcId);
RetrieveMangaChaptersFromMangaconnectorWorker retrieveChapters = new(mcId, Tranga.Settings.DownloadLanguage);
Tranga.AddWorkers([downloadCover, retrieveChapters]);
return Ok();
}
} }

View File

@ -1,7 +1,6 @@
using API.Schema.MangaContext; using API.Schema.MangaContext;
using API.Schema.MangaContext.MetadataFetchers; using API.Schema.MangaContext.MetadataFetchers;
using Asp.Versioning; using Asp.Versioning;
using log4net;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.ModelBinding;
using static Microsoft.AspNetCore.Http.StatusCodes; using static Microsoft.AspNetCore.Http.StatusCodes;
@ -12,38 +11,39 @@ namespace API.Controllers;
[ApiVersion(2)] [ApiVersion(2)]
[ApiController] [ApiController]
[Route("v{v:apiVersion}/[controller]")] [Route("v{v:apiVersion}/[controller]")]
public class MetadataFetcherController(MangaContext context, ILog Log) : Controller public class MetadataFetcherController(MangaContext context) : Controller
{ {
/// <summary> /// <summary>
/// Get all available Connectors (Metadata-Sites) /// Get all <see cref="MetadataFetcher"/> (Metadata-Sites)
/// </summary> /// </summary>
/// <response code="200">Names of Metadata-Fetchers</response> /// <response code="200">Names of <see cref="MetadataFetcher"/> (Metadata-Sites)</response>
[HttpGet] [HttpGet]
[ProducesResponseType<string[]>(Status200OK, "application/json")] [ProducesResponseType<string[]>(Status200OK, "application/json")]
public IActionResult GetConnectors() public IActionResult GetConnectors()
{ {
string[] connectors = Tranga.MetadataFetchers.Select(f => f.MetadataFetcherName).ToArray(); return Ok(Tranga.MetadataFetchers.Select(m => m.Name).ToArray());
return Ok(connectors);
} }
/// <summary> /// <summary>
/// Returns all Mangas which have a linked Metadata-Provider /// Returns all <see cref="MetadataEntry"/>
/// </summary> /// </summary>
/// <response code="200"></response> /// <response code="200"></response>
[HttpGet("Links")] [HttpGet("Links")]
[ProducesResponseType<MetadataEntry>(Status200OK, "application/json")] [ProducesResponseType<MetadataEntry[]>(Status200OK, "application/json")]
public IActionResult GetLinkedEntries() public IActionResult GetLinkedEntries()
{ {
return Ok(context.MetadataEntries.ToArray()); return Ok(context.MetadataEntries.ToArray());
} }
/// <summary> /// <summary>
/// Searches Metadata-Provider for Manga-Metadata /// Searches <see cref="MetadataFetcher"/> (Metadata-Sites) for Manga-Metadata
/// </summary> /// </summary>
/// <param name="searchTerm">Instead of using the Manga for search, use a specific term</param> /// <param name="MangaId"><see cref="Manga"/>.Key</param>
/// <param name="MetadataFetcherName"><see cref="MetadataFetcher"/>.Name</param>
/// <param name="searchTerm">Instead of using the <paramref name="MangaId"/> for search on Website, use a specific term</param>
/// <response code="200"></response> /// <response code="200"></response>
/// <response code="400">Metadata-fetcher with Name does not exist</response> /// <response code="400"><see cref="MetadataFetcher"/> (Metadata-Sites) with <paramref name="MetadataFetcherName"/> does not exist</response>
/// <response code="404">Manga with ID not found</response> /// <response code="404"><see cref="Manga"/> with <paramref name="MangaId"/> not found</response>
[HttpPost("{MetadataFetcherName}/SearchManga/{MangaId}")] [HttpPost("{MetadataFetcherName}/SearchManga/{MangaId}")]
[ProducesResponseType<MetadataSearchResult[]>(Status200OK, "application/json")] [ProducesResponseType<MetadataSearchResult[]>(Status200OK, "application/json")]
[ProducesResponseType(Status400BadRequest)] [ProducesResponseType(Status400BadRequest)]
@ -52,7 +52,7 @@ public class MetadataFetcherController(MangaContext context, ILog Log) : Control
{ {
if(context.Mangas.Find(MangaId) is not { } manga) if(context.Mangas.Find(MangaId) is not { } manga)
return NotFound(); return NotFound();
if(Tranga.MetadataFetchers.FirstOrDefault(f => f.MetadataFetcherName == MetadataFetcherName) is not { } fetcher) if(Tranga.MetadataFetchers.FirstOrDefault(f => f.Name == MetadataFetcherName) is not { } fetcher)
return BadRequest(); return BadRequest();
MetadataSearchResult[] searchResults = searchTerm is null ? fetcher.SearchMetadataEntry(manga) : fetcher.SearchMetadataEntry(searchTerm); MetadataSearchResult[] searchResults = searchTerm is null ? fetcher.SearchMetadataEntry(manga) : fetcher.SearchMetadataEntry(searchTerm);
@ -60,14 +60,17 @@ public class MetadataFetcherController(MangaContext context, ILog Log) : Control
} }
/// <summary> /// <summary>
/// Links Metadata-Provider using Provider-Specific Identifier to Manga /// Links <see cref="MetadataFetcher"/> (Metadata-Sites) using Provider-Specific Identifier to <see cref="Manga"/>
/// </summary> /// </summary>
/// <param name="MangaId"><see cref="Manga"/>.Key</param>
/// <param name="MetadataFetcherName"><see cref="MetadataFetcher"/>.Name</param>
/// <param name="Identifier"><see cref="MetadataFetcherName"/>-Specific ID</param>
/// <response code="200"></response> /// <response code="200"></response>
/// <response code="400">Metadata-fetcher with Name does not exist</response> /// <response code="400"><see cref="MetadataFetcher"/> (Metadata-Sites) with <paramref name="MetadataFetcherName"/> does not exist</response>
/// <response code="404">Manga with ID not found</response> /// <response code="404"><see cref="Manga"/> with <paramref name="MangaId"/> not found</response>
/// <response code="500">Error during Database Operation</response> /// <response code="500">Error during Database Operation</response>
[HttpPost("{MetadataFetcherName}/Link/{MangaId}")] [HttpPost("{MetadataFetcherName}/Link/{MangaId}")]
[ProducesResponseType(Status200OK)] [ProducesResponseType<MetadataEntry>(Status200OK, "application/json")]
[ProducesResponseType(Status400BadRequest)] [ProducesResponseType(Status400BadRequest)]
[ProducesResponseType(Status404NotFound)] [ProducesResponseType(Status404NotFound)]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")] [ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
@ -75,30 +78,24 @@ public class MetadataFetcherController(MangaContext context, ILog Log) : Control
{ {
if(context.Mangas.Find(MangaId) is not { } manga) if(context.Mangas.Find(MangaId) is not { } manga)
return NotFound(); return NotFound();
if(Tranga.MetadataFetchers.FirstOrDefault(f => f.MetadataFetcherName == MetadataFetcherName) is not { } fetcher) if(Tranga.MetadataFetchers.FirstOrDefault(f => f.Name == MetadataFetcherName) is not { } fetcher)
return BadRequest(); return BadRequest();
MetadataEntry entry = fetcher.CreateMetadataEntry(manga, Identifier);
try MetadataEntry entry = fetcher.CreateMetadataEntry(manga, Identifier);
{ context.MetadataEntries.Add(entry);
context.MetadataEntries.Add(entry);
context.SaveChanges(); if(context.Sync() is { } errorMessage)
} return StatusCode(Status500InternalServerError, errorMessage);
catch (Exception e) return Ok(entry);
{
Log.Error(e);
return StatusCode(500, e.Message);
}
return Ok();
} }
/// <summary> /// <summary>
/// Un-Links Metadata-Provider using Provider-Specific Identifier to Manga /// Un-Links <see cref="MetadataFetcher"/> (Metadata-Sites) from <see cref="Manga"/>
/// </summary> /// </summary>
/// <response code="200"></response> /// <response code="200"></response>
/// <response code="400">Metadata-fetcher with Name does not exist</response> /// <response code="400"><see cref="MetadataFetcher"/> (Metadata-Sites) with <paramref name="MetadataFetcherName"/> does not exist</response>
/// <response code="404">Manga with ID not found</response> /// <response code="404"><see cref="Manga"/> with <paramref name="MangaId"/> not found</response>
/// <response code="412">No Entry linking Manga and Metadata-Provider found</response> /// <response code="412">No <see cref="MetadataEntry"/> linking <see cref="Manga"/> and <see cref="MetadataFetcher"/> found</response>
/// <response code="500">Error during Database Operation</response> /// <response code="500">Error during Database Operation</response>
[HttpPost("{MetadataFetcherName}/Unlink/{MangaId}")] [HttpPost("{MetadataFetcherName}/Unlink/{MangaId}")]
[ProducesResponseType(Status200OK)] [ProducesResponseType(Status200OK)]
@ -108,58 +105,17 @@ public class MetadataFetcherController(MangaContext context, ILog Log) : Control
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")] [ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public IActionResult UnlinkMangaMetadata(string MangaId, string MetadataFetcherName) public IActionResult UnlinkMangaMetadata(string MangaId, string MetadataFetcherName)
{ {
if(context.Mangas.Find(MangaId) is not { } manga) if(context.Mangas.Find(MangaId) is null)
return NotFound(); return NotFound();
if(Tranga.MetadataFetchers.FirstOrDefault(f => f.MetadataFetcherName == MetadataFetcherName) is not { } fetcher) if(Tranga.MetadataFetchers.FirstOrDefault(f => f.Name == MetadataFetcherName) is null)
return BadRequest(); return BadRequest();
MetadataEntry? entry = context.MetadataEntries.FirstOrDefault(e => e.MangaId == MangaId && e.MetadataFetcherName == MetadataFetcherName); if(context.MetadataEntries.FirstOrDefault(e => e.MangaId == MangaId && e.MetadataFetcherName == MetadataFetcherName) is not { } entry)
if (entry is null)
return StatusCode(Status412PreconditionFailed, "No entry found"); return StatusCode(Status412PreconditionFailed, "No entry found");
try
{
context.MetadataEntries.Remove(entry);
context.SaveChanges();
}
catch (Exception e)
{
Log.Error(e);
return StatusCode(500, e.Message);
}
return Ok();
}
/// <summary> context.Remove(entry);
/// Tries linking a Manga to a Metadata-Provider-Site
/// </summary> if(context.Sync() is { success: false } result)
/// <response code="200"></response> return StatusCode(Status500InternalServerError, result.exceptionMessage);
/// <response code="400">MetadataFetcher Name is invalid</response>
/// <response code="404">Manga has no linked entry with MetadataFetcher</response>
/// <response code="500">Error during Database Operation</response>
[HttpPost("{MetadataFetcherName}/{MangaId}/UpdateMetadata")]
[ProducesResponseType(Status200OK)]
[ProducesResponseType(Status400BadRequest)]
[ProducesResponseType(Status404NotFound)]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public IActionResult UpdateMetadata(string MangaId, string MetadataFetcherName)
{
if(Tranga.MetadataFetchers
.FirstOrDefault(f =>
f.MetadataFetcherName.Equals(MetadataFetcherName, StringComparison.InvariantCultureIgnoreCase)) is not { } fetcher)
return BadRequest();
MetadataEntry? entry = context.MetadataEntries
.FirstOrDefault(e =>
e.MangaId == MangaId && e.MetadataFetcherName.Equals(MetadataFetcherName, StringComparison.InvariantCultureIgnoreCase));
if (entry is null)
return NotFound();
try
{
fetcher.UpdateMetadata(entry, context);
}
catch (Exception e)
{
Log.Error(e);
return StatusCode(500, e.Message);
}
return Ok(); return Ok();
} }
} }

View File

@ -3,9 +3,9 @@ using API.APIEndpointRecords;
using API.Schema.NotificationsContext; using API.Schema.NotificationsContext;
using API.Schema.NotificationsContext.NotificationConnectors; using API.Schema.NotificationsContext.NotificationConnectors;
using Asp.Versioning; using Asp.Versioning;
using log4net;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using static Microsoft.AspNetCore.Http.StatusCodes; using static Microsoft.AspNetCore.Http.StatusCodes;
// ReSharper disable InconsistentNaming
namespace API.Controllers; namespace API.Controllers;
@ -13,121 +13,100 @@ namespace API.Controllers;
[ApiController] [ApiController]
[Produces("application/json")] [Produces("application/json")]
[Route("v{v:apiVersion}/[controller]")] [Route("v{v:apiVersion}/[controller]")]
public class NotificationConnectorController(NotificationsContext context, ILog Log) : Controller public class NotificationConnectorController(NotificationsContext context) : Controller
{ {
/// <summary> /// <summary>
/// Gets all configured Notification-Connectors /// Gets all configured <see cref="NotificationConnector"/>
/// </summary> /// </summary>
/// <response code="200"></response> /// <response code="200"></response>
[HttpGet] [HttpGet]
[ProducesResponseType<NotificationConnector[]>(Status200OK, "application/json")] [ProducesResponseType<NotificationConnector[]>(Status200OK, "application/json")]
public IActionResult GetAllConnectors() public IActionResult GetAllConnectors()
{ {
NotificationConnector[] ret = context.NotificationConnectors.ToArray();
return Ok(ret); return Ok(context.NotificationConnectors.ToArray());
} }
/// <summary> /// <summary>
/// Returns Notification-Connector with requested ID /// Returns <see cref="NotificationConnector"/> with requested Name
/// </summary> /// </summary>
/// <param name="NotificationConnectorId">Notification-Connector-ID</param> /// <param name="Name"><see cref="NotificationConnector"/>.Name</param>
/// <response code="200"></response> /// <response code="200"></response>
/// <response code="404">NotificationConnector with ID not found</response> /// <response code="404"><see cref="NotificationConnector"/> with <paramref name="Name"/> not found</response>
[HttpGet("{NotificationConnectorId}")] [HttpGet("{Name}")]
[ProducesResponseType<NotificationConnector>(Status200OK, "application/json")] [ProducesResponseType<NotificationConnector>(Status200OK, "application/json")]
[ProducesResponseType(Status404NotFound)] [ProducesResponseType(Status404NotFound)]
public IActionResult GetConnector(string NotificationConnectorId) public IActionResult GetConnector(string Name)
{ {
NotificationConnector? ret = context.NotificationConnectors.Find(NotificationConnectorId); if(context.NotificationConnectors.Find(Name) is not { } connector)
return (ret is not null) switch return NotFound();
{
true => Ok(ret), return Ok(connector);
false => NotFound()
};
} }
/// <summary> /// <summary>
/// Creates a new REST-Notification-Connector /// Creates a new <see cref="NotificationConnector"/>
/// </summary> /// </summary>
/// <remarks>Formatting placeholders: "%title" and "%text" can be placed in url, header-values and body and will be replaced when notifications are sent</remarks> /// <remarks>Formatting placeholders: "%title" and "%text" can be placed in url, header-values and body and will be replaced when notifications are sent</remarks>
/// <param name="notificationConnector">Notification-Connector</param> /// <response code="201"></response>
/// <response code="201">ID of new connector</response>
/// <response code="409">A NotificationConnector with name already exists</response>
/// <response code="500">Error during Database Operation</response> /// <response code="500">Error during Database Operation</response>
[HttpPut] [HttpPut]
[ProducesResponseType<string>(Status201Created, "application/json")] [ProducesResponseType(Status201Created)]
[ProducesResponseType(Status409Conflict)] [ProducesResponseType(Status409Conflict)]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")] [ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public IActionResult CreateConnector([FromBody]NotificationConnector notificationConnector) public IActionResult CreateConnector([FromBody]NotificationConnector notificationConnector)
{ {
if (context.NotificationConnectors.Find(notificationConnector.Name) is not null)
return Conflict(); context.NotificationConnectors.Add(notificationConnector);
try
{ if(context.Sync() is { success: false } result)
context.NotificationConnectors.Add(notificationConnector); return StatusCode(Status500InternalServerError, result.exceptionMessage);
context.SaveChanges(); return Created();
return Created(notificationConnector.Name, notificationConnector);
}
catch (Exception e)
{
Log.Error(e);
return StatusCode(500, e.Message);
}
} }
/// <summary> /// <summary>
/// Creates a new Gotify-Notification-Connector /// Creates a new Gotify-<see cref="NotificationConnector"/>
/// </summary> /// </summary>
/// <remarks>Priority needs to be between 0 and 10</remarks> /// <remarks>Priority needs to be between 0 and 10</remarks>
/// <response code="201">ID of new connector</response> /// <response code="201"></response>
/// <response code="400"></response>
/// <response code="409">A NotificationConnector with name already exists</response>
/// <response code="500">Error during Database Operation</response> /// <response code="500">Error during Database Operation</response>
[HttpPut("Gotify")] [HttpPut("Gotify")]
[ProducesResponseType<string>(Status201Created, "application/json")] [ProducesResponseType<string>(Status201Created, "application/json")]
[ProducesResponseType(Status400BadRequest)]
[ProducesResponseType(Status409Conflict)]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")] [ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public IActionResult CreateGotifyConnector([FromBody]GotifyRecord gotifyData) public IActionResult CreateGotifyConnector([FromBody]GotifyRecord gotifyData)
{ {
if(!gotifyData.Validate()) //TODO Validate Data
return BadRequest();
NotificationConnector gotifyConnector = new NotificationConnector(TokenGen.CreateToken("Gotify"), NotificationConnector gotifyConnector = new (gotifyData.Name,
gotifyData.endpoint, gotifyData.Endpoint,
new Dictionary<string, string>() { { "X-Gotify-IDOnConnector", gotifyData.appToken } }, new Dictionary<string, string>() { { "X-Gotify-IDOnConnector", gotifyData.AppToken } },
"POST", "POST",
$"{{\"message\": \"%text\", \"title\": \"%title\", \"priority\": {gotifyData.priority}}}"); $"{{\"message\": \"%text\", \"title\": \"%title\", \"Priority\": {gotifyData.Priority}}}");
return CreateConnector(gotifyConnector); return CreateConnector(gotifyConnector);
} }
/// <summary> /// <summary>
/// Creates a new Ntfy-Notification-Connector /// Creates a new Ntfy-<see cref="NotificationConnector"/>
/// </summary> /// </summary>
/// <remarks>Priority needs to be between 1 and 5</remarks> /// <remarks>Priority needs to be between 1 and 5</remarks>
/// <response code="201">ID of new connector</response> /// <response code="201"></response>
/// <response code="400"></response>
/// <response code="409">A NotificationConnector with name already exists</response>
/// <response code="500">Error during Database Operation</response> /// <response code="500">Error during Database Operation</response>
[HttpPut("Ntfy")] [HttpPut("Ntfy")]
[ProducesResponseType<string>(Status201Created, "application/json")] [ProducesResponseType<string>(Status201Created, "application/json")]
[ProducesResponseType(Status400BadRequest)]
[ProducesResponseType(Status409Conflict)]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")] [ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public IActionResult CreateNtfyConnector([FromBody]NtfyRecord ntfyRecord) public IActionResult CreateNtfyConnector([FromBody]NtfyRecord ntfyRecord)
{ {
if(!ntfyRecord.Validate()) //TODO Validate Data
return BadRequest();
string authHeader = "Basic " + Convert.ToBase64String(Encoding.UTF8.GetBytes($"{ntfyRecord.username}:{ntfyRecord.password}")); string authHeader = "Basic " + Convert.ToBase64String(Encoding.UTF8.GetBytes($"{ntfyRecord.Username}:{ntfyRecord.Password}"));
string auth = Convert.ToBase64String(Encoding.UTF8.GetBytes(authHeader)).Replace("=",""); string auth = Convert.ToBase64String(Encoding.UTF8.GetBytes(authHeader)).Replace("=","");
NotificationConnector ntfyConnector = new (TokenGen.CreateToken("Ntfy"), NotificationConnector ntfyConnector = new (ntfyRecord.Name,
$"{ntfyRecord.endpoint}?auth={auth}", $"{ntfyRecord.Endpoint}/{ntfyRecord.Topic}?auth={auth}",
new Dictionary<string, string>() new Dictionary<string, string>()
{ {
{"Title", "%title"}, {"Title", "%title"},
{"Priority", ntfyRecord.priority.ToString()}, {"Priority", ntfyRecord.Priority.ToString()},
}, },
"POST", "POST",
"%text"); "%text");
@ -135,58 +114,46 @@ public class NotificationConnectorController(NotificationsContext context, ILog
} }
/// <summary> /// <summary>
/// Creates a new Pushover-Notification-Connector /// Creates a new Pushover-<see cref="NotificationConnector"/>
/// </summary> /// </summary>
/// <remarks>https://pushover.net/api</remarks> /// <remarks>https://pushover.net/api</remarks>
/// <response code="201">ID of new connector</response> /// <response code="201">ID of new connector</response>
/// <response code="400"></response>
/// <response code="409">A NotificationConnector with name already exists</response>
/// <response code="500">Error during Database Operation</response> /// <response code="500">Error during Database Operation</response>
[HttpPut("Pushover")] [HttpPut("Pushover")]
[ProducesResponseType<string>(Status201Created, "application/json")] [ProducesResponseType<string>(Status201Created, "application/json")]
[ProducesResponseType(Status400BadRequest)]
[ProducesResponseType(Status409Conflict)]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")] [ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public IActionResult CreatePushoverConnector([FromBody]PushoverRecord pushoverRecord) public IActionResult CreatePushoverConnector([FromBody]PushoverRecord pushoverRecord)
{ {
if(!pushoverRecord.Validate()) //TODO Validate Data
return BadRequest();
NotificationConnector pushoverConnector = new (TokenGen.CreateToken("Pushover"), NotificationConnector pushoverConnector = new (pushoverRecord.Name,
$"https://api.pushover.net/1/messages.json", $"https://api.pushover.net/1/messages.json",
new Dictionary<string, string>(), new Dictionary<string, string>(),
"POST", "POST",
$"{{\"token\": \"{pushoverRecord.apptoken}\", \"user\": \"{pushoverRecord.user}\", \"message:\":\"%text\", \"%title\" }}"); $"{{\"token\": \"{pushoverRecord.AppToken}\", \"user\": \"{pushoverRecord.User}\", \"message:\":\"%text\", \"%title\" }}");
return CreateConnector(pushoverConnector); return CreateConnector(pushoverConnector);
} }
/// <summary> /// <summary>
/// Deletes the Notification-Connector with the requested ID /// Deletes the <see cref="NotificationConnector"/> with the requested Name
/// </summary> /// </summary>
/// <param name="NotificationConnectorId">Notification-Connector-ID</param> /// <param name="Name"><see cref="NotificationConnector"/>.Name</param>
/// <response code="200"></response> /// <response code="200"></response>
/// <response code="404">NotificationConnector with ID not found</response> /// <response code="404"><see cref="NotificationConnector"/> with Name not found</response>
/// <response code="500">Error during Database Operation</response> /// <response code="500">Error during Database Operation</response>
[HttpDelete("{NotificationConnectorId}")] [HttpDelete("{Name}")]
[ProducesResponseType(Status200OK)] [ProducesResponseType(Status200OK)]
[ProducesResponseType(Status404NotFound)] [ProducesResponseType(Status404NotFound)]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")] [ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public IActionResult DeleteConnector(string NotificationConnectorId) public IActionResult DeleteConnector(string Name)
{ {
try if(context.NotificationConnectors.Find(Name) is not { } connector)
{ return NotFound();
NotificationConnector? ret = context.NotificationConnectors.Find(NotificationConnectorId);
if(ret is null) context.NotificationConnectors.Remove(connector);
return NotFound();
if(context.Sync() is { success: false } result)
context.Remove(ret); return StatusCode(Status500InternalServerError, result.exceptionMessage);
context.SaveChanges(); return Created();
return Ok();
}
catch (Exception e)
{
Log.Error(e);
return StatusCode(500, e.Message);
}
} }
} }

View File

@ -1,6 +1,5 @@
using API.Schema.MangaContext; using API.Schema.MangaContext;
using Asp.Versioning; using Asp.Versioning;
using log4net;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using static Microsoft.AspNetCore.Http.StatusCodes; using static Microsoft.AspNetCore.Http.StatusCodes;
// ReSharper disable InconsistentNaming // ReSharper disable InconsistentNaming
@ -10,102 +9,70 @@ namespace API.Controllers;
[ApiVersion(2)] [ApiVersion(2)]
[ApiController] [ApiController]
[Route("v{v:apiVersion}/[controller]")] [Route("v{v:apiVersion}/[controller]")]
public class QueryController(MangaContext context, ILog Log) : Controller public class QueryController(MangaContext context) : Controller
{ {
/// <summary> /// <summary>
/// Returns the Author-Information for Author-ID /// Returns the <see cref="Author"/> with <paramref name="AuthorId"/>
/// </summary> /// </summary>
/// <param name="AuthorId">Author-Id</param> /// <param name="AuthorId"><see cref="Author"/>.Key</param>
/// <response code="200"></response> /// <response code="200"></response>
/// <response code="404">Author with ID not found</response> /// <response code="404"><see cref="Author"/> with <paramref name="AuthorId"/> not found</response>
[HttpGet("Author/{AuthorId}")] [HttpGet("Author/{AuthorId}")]
[ProducesResponseType<Author>(Status200OK, "application/json")] [ProducesResponseType<Author>(Status200OK, "application/json")]
[ProducesResponseType(Status404NotFound)] [ProducesResponseType(Status404NotFound)]
public IActionResult GetAuthor(string AuthorId) public IActionResult GetAuthor(string AuthorId)
{ {
Author? ret = context.Authors.Find(AuthorId); if (context.Authors.Find(AuthorId) is not { } author)
if (ret is null)
return NotFound(); return NotFound();
return Ok(ret);
return Ok(author);
} }
/// <summary> /// <summary>
/// Returns all Mangas which where Authored by Author with AuthorId /// Returns all <see cref="Manga"/> which where Authored by <see cref="Author"/> with <paramref name="AuthorId"/>
/// </summary> /// </summary>
/// <param name="AuthorId">Author-ID</param> /// <param name="AuthorId"><see cref="Author"/>.Key</param>
/// <response code="200"></response> /// <response code="200"></response>
/// <response code="404">Author not found</response> /// <response code="404"><see cref="Author"/> with <paramref name="AuthorId"/></response>
[HttpGet("Mangas/WithAuthorId/{AuthorId}")] [HttpGet("Mangas/WithAuthorId/{AuthorId}")]
[ProducesResponseType<Manga[]>(Status200OK, "application/json")] [ProducesResponseType<Manga[]>(Status200OK, "application/json")]
public IActionResult GetMangaWithAuthorIds(string AuthorId) public IActionResult GetMangaWithAuthorIds(string AuthorId)
{ {
if(context.Authors.Find(AuthorId) is not { } a) if (context.Authors.Find(AuthorId) is not { } author)
return NotFound(); return NotFound();
return Ok(context.Mangas.Where(m => m.Authors.Contains(a)));
} return Ok(context.Mangas.Where(m => m.Authors.Contains(author)));
/*
/// <summary>
/// Returns Link-Information for Link-Id
/// </summary>
/// <param name="LinkId"></param>
/// <response code="200"></response>
/// <response code="404">Link with ID not found</response>
[HttpGet("Link/{LinkId}")]
[ProducesResponseType<Link>(Status200OK, "application/json")]
[ProducesResponseType(Status404NotFound)]
public IActionResult GetLink(string LinkId)
{
Link? ret = context.Links.Find(LinkId);
if (ret is null)
return NotFound();
return Ok(ret);
} }
/// <summary> /// <summary>
/// Returns AltTitle-Information for AltTitle-Id /// Returns all <see cref="Manga"/> with <see cref="Tag"/>
/// </summary> /// </summary>
/// <param name="AltTitleId"></param> /// <param name="Tag"><see cref="Tag"/>.Tag</param>
/// <response code="200"></response> /// <response code="200"></response>
/// <response code="404">AltTitle with ID not found</response> /// <response code="404"><see cref="Tag"/> not found</response>
[HttpGet("AltTitle/{AltTitleId}")]
[ProducesResponseType<AltTitle>(Status200OK, "application/json")]
[ProducesResponseType(Status404NotFound)]
public IActionResult GetAltTitle(string AltTitleId)
{
AltTitle? ret = context.AltTitles.Find(AltTitleId);
if (ret is null)
return NotFound();
return Ok(ret);
}*/
/// <summary>
/// Returns all Obj with Tag
/// </summary>
/// <param name="Tag"></param>
/// <response code="200"></response>
/// <response code="404">Tag not found</response>
[HttpGet("Mangas/WithTag/{Tag}")] [HttpGet("Mangas/WithTag/{Tag}")]
[ProducesResponseType<Manga[]>(Status200OK, "application/json")] [ProducesResponseType<Manga[]>(Status200OK, "application/json")]
public IActionResult GetMangasWithTag(string Tag) public IActionResult GetMangasWithTag(string Tag)
{ {
if(context.Tags.Find(Tag) is not { } t) if (context.Tags.Find(Tag) is not { } tag)
return NotFound(); return NotFound();
return Ok(context.Mangas.Where(m => m.MangaTags.Contains(t)));
return Ok(context.Mangas.Where(m => m.MangaTags.Contains(tag)));
} }
/// <summary> /// <summary>
/// Returns Chapter-Information for Chapter-Id /// Returns <see cref="Chapter"/> with <paramref name="ChapterId"/>
/// </summary> /// </summary>
/// <param name="ChapterId"></param> /// <param name="ChapterId"><see cref="Chapter"/>.Key</param>
/// <response code="200"></response> /// <response code="200"></response>
/// <response code="404">Chapter with ID not found</response> /// <response code="404"><see cref="Chapter"/> with <paramref name="ChapterId"/> not found</response>
[HttpGet("Chapter/{ChapterId}")] [HttpGet("Chapter/{ChapterId}")]
[ProducesResponseType<Chapter>(Status200OK, "application/json")] [ProducesResponseType<Chapter>(Status200OK, "application/json")]
public IActionResult GetChapter(string ChapterId) public IActionResult GetChapter(string ChapterId)
{ {
Chapter? ret = context.Chapters.Find(ChapterId); if (context.Chapters.Find(ChapterId) is not { } chapter)
if (ret is null)
return NotFound(); return NotFound();
return Ok(ret);
return Ok(chapter);
} }
} }

View File

@ -1,9 +1,7 @@
using API.Schema.MangaContext; using API.Schema.MangaContext;
using API.Schema.MangaContext.MangaConnectors;
using Asp.Versioning; using Asp.Versioning;
using log4net;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Soenneker.Utils.String.NeedlemanWunsch;
using static Microsoft.AspNetCore.Http.StatusCodes; using static Microsoft.AspNetCore.Http.StatusCodes;
// ReSharper disable InconsistentNaming // ReSharper disable InconsistentNaming
@ -12,75 +10,50 @@ namespace API.Controllers;
[ApiVersion(2)] [ApiVersion(2)]
[ApiController] [ApiController]
[Route("v{v:apiVersion}/[controller]")] [Route("v{v:apiVersion}/[controller]")]
public class SearchController(MangaContext context, ILog Log) : Controller public class SearchController(MangaContext context) : Controller
{ {
/// <summary> /// <summary>
/// Initiate a search for a Obj on a specific Connector /// Initiate a search for a <see cref="Manga"/> on <see cref="MangaConnector"/> with searchTerm
/// </summary> /// </summary>
/// <param name="MangaConnectorName"></param> /// <param name="MangaConnectorName"><see cref="MangaConnector"/>.Name</param>
/// <param name="Query"></param> /// <param name="Query">searchTerm</param>
/// <response code="200"></response> /// <response code="200"></response>
/// <response code="404">MangaConnector with ID not found</response> /// <response code="404"><see cref="MangaConnector"/> with Name not found</response>
/// <response code="406">MangaConnector with ID is disabled</response> /// <response code="412"><see cref="MangaConnector"/> with Name is disabled</response>
/// <response code="500">Error during Database Operation</response>
[HttpGet("{MangaConnectorName}/{Query}")] [HttpGet("{MangaConnectorName}/{Query}")]
[ProducesResponseType<Manga[]>(Status200OK, "application/json")] [ProducesResponseType<Manga[]>(Status200OK, "application/json")]
[ProducesResponseType(Status404NotFound)] [ProducesResponseType(Status404NotFound)]
[ProducesResponseType(Status406NotAcceptable)] [ProducesResponseType(Status406NotAcceptable)]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public IActionResult SearchManga(string MangaConnectorName, string Query) public IActionResult SearchManga(string MangaConnectorName, string Query)
{ {
if(context.MangaConnectors.Find(MangaConnectorName) is not { } connector) if(context.MangaConnectors.Find(MangaConnectorName) is not { } connector)
return NotFound(); return NotFound();
else if (connector.Enabled is false) if (connector.Enabled is false)
return StatusCode(Status406NotAcceptable); return StatusCode(Status412PreconditionFailed);
(Manga, MangaConnectorId<Manga>)[] mangas = connector.SearchManga(Query); (Manga, MangaConnectorId<Manga>)[] mangas = connector.SearchManga(Query);
List<Manga> retMangas = new(); List<Manga> retMangas = new();
foreach ((Manga manga, MangaConnectorId<Manga> mcId) manga in mangas) foreach ((Manga manga, MangaConnectorId<Manga> mcId) manga in mangas)
{ {
try if(Tranga.AddMangaToContext(manga, context, out Manga? add))
{ retMangas.Add(add);
if(AddMangaToContext(manga) is { } add)
retMangas.Add(add);
}
catch (DbUpdateException e)
{
Log.Error(e);
return StatusCode(Status500InternalServerError, e.Message);
}
} }
return Ok(retMangas.ToArray()); return Ok(retMangas.ToArray());
} }
/// <summary>
/// Search for a known Obj
/// </summary>
/// <param name="Query"></param>
/// <response code="200"></response>
[HttpGet("Local/{Query}")]
[ProducesResponseType<Manga[]>(Status200OK, "application/json")]
public IActionResult SearchMangaLocally(string Query)
{
Dictionary<Manga, double> distance = context.Mangas
.ToArray()
.ToDictionary(m => m, m => NeedlemanWunschStringUtil.CalculateSimilarityPercentage(Query, m.Name));
return Ok(distance.Where(kv => kv.Value > 50).OrderByDescending(kv => kv.Value).Select(kv => kv.Key).ToArray());
}
/// <summary> /// <summary>
/// Returns Obj from MangaConnector associated with URL /// Returns <see cref="Manga"/> from the <see cref="MangaConnector"/> associated with <paramref name="url"/>
/// </summary> /// </summary>
/// <param name="url">Obj-Page URL</param> /// <param name="url"></param>
/// <response code="200"></response> /// <response code="200"></response>
/// <response code="300">Multiple connectors found for URL</response> /// <response code="300">Multiple <see cref="MangaConnector"/> found for URL</response>
/// <response code="404">Obj not found</response> /// <response code="404"><see cref="Manga"/> not found</response>
/// <response code="500">Error during Database Operation</response> /// <response code="500">Error during Database Operation</response>
[HttpPost("Url")] [HttpPost("Url")]
[ProducesResponseType<Manga>(Status200OK, "application/json")] [ProducesResponseType<Manga>(Status200OK, "application/json")]
[ProducesResponseType(Status404NotFound)] [ProducesResponseType(Status404NotFound)]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")] [ProducesResponseType(Status500InternalServerError)]
public IActionResult GetMangaFromUrl([FromBody]string url) public IActionResult GetMangaFromUrl([FromBody]string url)
{ {
if (context.MangaConnectors.Find("Global") is not { } connector) if (context.MangaConnectors.Find("Global") is not { } connector)
@ -88,51 +61,10 @@ public class SearchController(MangaContext context, ILog Log) : Controller
if(connector.GetMangaFromUrl(url) is not { } manga) if(connector.GetMangaFromUrl(url) is not { } manga)
return NotFound(); return NotFound();
try
{ if(Tranga.AddMangaToContext(manga, context, out Manga? add) == false)
if(AddMangaToContext(manga) is { } add)
return Ok(add);
return StatusCode(Status500InternalServerError); return StatusCode(Status500InternalServerError);
}
catch (DbUpdateException e)
{
Log.Error(e);
return StatusCode(Status500InternalServerError, e.Message);
}
}
private Manga? AddMangaToContext((Manga, MangaConnectorId<Manga>) manga) => AddMangaToContext(manga.Item1, manga.Item2, context);
internal static Manga? AddMangaToContext(Manga addManga, MangaConnectorId<Manga> addMcId, MangaContext context)
{
Manga manga = context.Mangas.Find(addManga.Key) ?? addManga;
MangaConnectorId<Manga> mcId = context.MangaConnectorToManga.Find(addMcId.Key) ?? addMcId;
mcId.Obj = manga;
IEnumerable<MangaTag> mergedTags = manga.MangaTags.Select(mt => return Ok(add);
{
MangaTag? inDb = context.Tags.Find(mt.Tag);
return inDb ?? mt;
});
manga.MangaTags = mergedTags.ToList();
IEnumerable<Author> mergedAuthors = manga.Authors.Select(ma =>
{
Author? inDb = context.Authors.Find(ma.Key);
return inDb ?? ma;
});
manga.Authors = mergedAuthors.ToList();
try
{
if(context.MangaConnectorToManga.Find(addMcId.Key) is null)
context.MangaConnectorToManga.Add(mcId);
context.SaveChanges();
}
catch (DbUpdateException e)
{
return null;
}
return manga;
} }
} }

View File

@ -1,28 +1,25 @@
using API.MangaDownloadClients; using API.MangaDownloadClients;
using API.Schema.JobsContext.Jobs;
using API.Schema.MangaContext;
using Asp.Versioning; using Asp.Versioning;
using log4net;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json.Linq;
using static Microsoft.AspNetCore.Http.StatusCodes; using static Microsoft.AspNetCore.Http.StatusCodes;
// ReSharper disable InconsistentNaming
namespace API.Controllers; namespace API.Controllers;
[ApiVersion(2)] [ApiVersion(2)]
[ApiController] [ApiController]
[Route("v{v:apiVersion}/[controller]")] [Route("v{v:apiVersion}/[controller]")]
public class SettingsController(MangaContext context, ILog Log) : Controller public class SettingsController() : Controller
{ {
/// <summary> /// <summary>
/// Get all Settings /// Get all <see cref="Tranga.Settings"/>
/// </summary> /// </summary>
/// <response code="200"></response> /// <response code="200"></response>
[HttpGet] [HttpGet]
[ProducesResponseType<JObject>(Status200OK, "application/json")] [ProducesResponseType<TrangaSettings>(Status200OK, "application/json")]
public IActionResult GetSettings() public IActionResult GetSettings()
{ {
return Ok(JObject.Parse(TrangaSettings.Serialize())); return Ok(Tranga.Settings);
} }
/// <summary> /// <summary>
@ -33,7 +30,7 @@ public class SettingsController(MangaContext context, ILog Log) : Controller
[ProducesResponseType<string>(Status200OK, "text/plain")] [ProducesResponseType<string>(Status200OK, "text/plain")]
public IActionResult GetUserAgent() public IActionResult GetUserAgent()
{ {
return Ok(TrangaSettings.userAgent); return Ok(Tranga.Settings.UserAgent);
} }
/// <summary> /// <summary>
@ -44,7 +41,8 @@ public class SettingsController(MangaContext context, ILog Log) : Controller
[ProducesResponseType(Status200OK)] [ProducesResponseType(Status200OK)]
public IActionResult SetUserAgent([FromBody]string userAgent) public IActionResult SetUserAgent([FromBody]string userAgent)
{ {
TrangaSettings.UpdateUserAgent(userAgent); //TODO Validate
Tranga.Settings.SetUserAgent(userAgent);
return Ok(); return Ok();
} }
@ -56,7 +54,7 @@ public class SettingsController(MangaContext context, ILog Log) : Controller
[ProducesResponseType(Status200OK)] [ProducesResponseType(Status200OK)]
public IActionResult ResetUserAgent() public IActionResult ResetUserAgent()
{ {
TrangaSettings.UpdateUserAgent(TrangaSettings.DefaultUserAgent); Tranga.Settings.SetUserAgent(TrangaSettings.DefaultUserAgent);
return Ok(); return Ok();
} }
@ -68,7 +66,7 @@ public class SettingsController(MangaContext context, ILog Log) : Controller
[ProducesResponseType<Dictionary<RequestType,int>>(Status200OK, "application/json")] [ProducesResponseType<Dictionary<RequestType,int>>(Status200OK, "application/json")]
public IActionResult GetRequestLimits() public IActionResult GetRequestLimits()
{ {
return Ok(TrangaSettings.requestLimits); return Ok(Tranga.Settings.RequestLimits);
} }
/// <summary> /// <summary>
@ -96,7 +94,7 @@ public class SettingsController(MangaContext context, ILog Log) : Controller
{ {
if (requestLimit <= 0) if (requestLimit <= 0)
return BadRequest(); return BadRequest();
TrangaSettings.UpdateRequestLimit(RequestType, requestLimit); Tranga.Settings.SetRequestLimit(RequestType, requestLimit);
return Ok(); return Ok();
} }
@ -108,7 +106,7 @@ public class SettingsController(MangaContext context, ILog Log) : Controller
[ProducesResponseType<string>(Status200OK)] [ProducesResponseType<string>(Status200OK)]
public IActionResult ResetRequestLimits(RequestType RequestType) public IActionResult ResetRequestLimits(RequestType RequestType)
{ {
TrangaSettings.UpdateRequestLimit(RequestType, TrangaSettings.DefaultRequestLimits[RequestType]); Tranga.Settings.SetRequestLimit(RequestType, TrangaSettings.DefaultRequestLimits[RequestType]);
return Ok(); return Ok();
} }
@ -120,35 +118,35 @@ public class SettingsController(MangaContext context, ILog Log) : Controller
[ProducesResponseType<string>(Status200OK)] [ProducesResponseType<string>(Status200OK)]
public IActionResult ResetRequestLimits() public IActionResult ResetRequestLimits()
{ {
TrangaSettings.ResetRequestLimits(); Tranga.Settings.ResetRequestLimits();
return Ok(); return Ok();
} }
/// <summary> /// <summary>
/// Returns Level of Image-Compression for Images /// Returns Level of Image-Compression for Images
/// </summary> /// </summary>
/// <response code="200">JPEG compression-level as Integer</response> /// <response code="200">JPEG ImageCompression-level as Integer</response>
[HttpGet("ImageCompression")] [HttpGet("ImageCompressionLevel")]
[ProducesResponseType<int>(Status200OK, "text/plain")] [ProducesResponseType<int>(Status200OK, "text/plain")]
public IActionResult GetImageCompression() public IActionResult GetImageCompression()
{ {
return Ok(TrangaSettings.compression); return Ok(Tranga.Settings.ImageCompression);
} }
/// <summary> /// <summary>
/// Set the Image-Compression-Level for Images /// Set the Image-Compression-Level for Images
/// </summary> /// </summary>
/// <param name="level">100 to disable, 0-99 for JPEG compression-Level</param> /// <param name="level">100 to disable, 0-99 for JPEG ImageCompression-Level</param>
/// <response code="200"></response> /// <response code="200"></response>
/// <response code="400">Level outside permitted range</response> /// <response code="400">Level outside permitted range</response>
[HttpPatch("ImageCompression")] [HttpPatch("ImageCompressionLevel/{level}")]
[ProducesResponseType(Status200OK)] [ProducesResponseType(Status200OK)]
[ProducesResponseType(Status400BadRequest)] [ProducesResponseType(Status400BadRequest)]
public IActionResult SetImageCompression([FromBody]int level) public IActionResult SetImageCompression(int level)
{ {
if (level < 1 || level > 100) if (level < 1 || level > 100)
return BadRequest(); return BadRequest();
TrangaSettings.UpdateCompressImages(level); Tranga.Settings.UpdateImageCompression(level);
return Ok(); return Ok();
} }
@ -160,7 +158,7 @@ public class SettingsController(MangaContext context, ILog Log) : Controller
[ProducesResponseType<bool>(Status200OK, "text/plain")] [ProducesResponseType<bool>(Status200OK, "text/plain")]
public IActionResult GetBwImagesToggle() public IActionResult GetBwImagesToggle()
{ {
return Ok(TrangaSettings.bwImages); return Ok(Tranga.Settings.BlackWhiteImages);
} }
/// <summary> /// <summary>
@ -168,37 +166,11 @@ public class SettingsController(MangaContext context, ILog Log) : Controller
/// </summary> /// </summary>
/// <param name="enabled">true to enable</param> /// <param name="enabled">true to enable</param>
/// <response code="200"></response> /// <response code="200"></response>
[HttpPatch("BWImages")] [HttpPatch("BWImages/{enabled}")]
[ProducesResponseType(Status200OK)] [ProducesResponseType(Status200OK)]
public IActionResult SetBwImagesToggle([FromBody]bool enabled) public IActionResult SetBwImagesToggle(bool enabled)
{ {
TrangaSettings.UpdateBwImages(enabled); Tranga.Settings.SetBlackWhiteImageEnabled(enabled);
return Ok();
}
/// <summary>
/// Get state of April Fools Mode
/// </summary>
/// <remarks>April Fools Mode disables all downloads on April 1st</remarks>
/// <response code="200">True if enabled</response>
[HttpGet("AprilFoolsMode")]
[ProducesResponseType<bool>(Status200OK, "text/plain")]
public IActionResult GetAprilFoolsMode()
{
return Ok(TrangaSettings.aprilFoolsMode);
}
/// <summary>
/// Enable/Disable April Fools Mode
/// </summary>
/// <remarks>April Fools Mode disables all downloads on April 1st</remarks>
/// <param name="enabled">true to enable</param>
/// <response code="200"></response>
[HttpPatch("AprilFoolsMode")]
[ProducesResponseType(Status200OK)]
public IActionResult SetAprilFoolsMode([FromBody]bool enabled)
{
TrangaSettings.UpdateAprilFoolsMode(enabled);
return Ok(); return Ok();
} }
@ -224,7 +196,7 @@ public class SettingsController(MangaContext context, ILog Log) : Controller
[ProducesResponseType<string>(Status200OK, "text/plain")] [ProducesResponseType<string>(Status200OK, "text/plain")]
public IActionResult GetCustomNamingScheme() public IActionResult GetCustomNamingScheme()
{ {
return Ok(TrangaSettings.chapterNamingScheme); return Ok(Tranga.Settings.ChapterNamingScheme);
} }
/// <summary> /// <summary>
@ -237,58 +209,20 @@ public class SettingsController(MangaContext context, ILog Log) : Controller
/// %C Chapter /// %C Chapter
/// %T Title /// %T Title
/// %A Author (first in list) /// %A Author (first in list)
/// %I Chapter Internal ID
/// %i Obj Internal ID
/// %Y Year (Obj) /// %Y Year (Obj)
/// ///
/// ?_(...) replace _ with a value from above: /// ?_(...) replace _ with a value from above:
/// Everything inside the braces will only be added if the value of %_ is not null /// Everything inside the braces will only be added if the value of %_ is not null
/// </remarks> /// </remarks>
/// <response code="200"></response> /// <response code="200"></response>
/// <response code="500">Error during Database Operation</response>
[HttpPatch("ChapterNamingScheme")] [HttpPatch("ChapterNamingScheme")]
[ProducesResponseType(Status200OK)] [ProducesResponseType(Status200OK)]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public IActionResult SetCustomNamingScheme([FromBody]string namingScheme) public IActionResult SetCustomNamingScheme([FromBody]string namingScheme)
{ {
try //TODO Move old Chapters
{ Tranga.Settings.SetChapterNamingScheme(namingScheme);
Dictionary<Chapter, string> oldPaths = context.Chapters.ToDictionary(c => c, c => c.FullArchiveFilePath);
TrangaSettings.UpdateChapterNamingScheme(namingScheme); return Ok();
MoveFileOrFolderJob[] newJobs = oldPaths
.Select(kv => new MoveFileOrFolderJob(kv.Value, kv.Key.FullArchiveFilePath)).ToArray();
context.Jobs.AddRange(newJobs);
return Ok();
}
catch (Exception e)
{
Log.Error(e);
return StatusCode(500, e);
}
}
/// <summary>
/// Creates a UpdateCoverJob for all Obj
/// </summary>
/// <response code="200">Array of JobIds</response>
/// <response code="500">Error during Database Operation</response>
[HttpPost("CleanupCovers")]
[ProducesResponseType<string[]>(Status200OK)]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public IActionResult CleanupCovers()
{
try
{
Tranga.RemoveStaleFiles(context);
List<UpdateCoverJob> newJobs = context.Mangas.ToList().Select(m => new UpdateCoverJob(m, 0)).ToList();
context.Jobs.AddRange(newJobs);
return Ok(newJobs.Select(j => j.Key));
}
catch (Exception e)
{
Log.Error(e);
return StatusCode(500, e);
}
} }
/// <summary> /// <summary>
@ -300,7 +234,7 @@ public class SettingsController(MangaContext context, ILog Log) : Controller
[ProducesResponseType(Status200OK)] [ProducesResponseType(Status200OK)]
public IActionResult SetFlareSolverrUrl([FromBody]string flareSolverrUrl) public IActionResult SetFlareSolverrUrl([FromBody]string flareSolverrUrl)
{ {
TrangaSettings.UpdateFlareSolverrUrl(flareSolverrUrl); Tranga.Settings.SetFlareSolverrUrl(flareSolverrUrl);
return Ok(); return Ok();
} }
@ -312,7 +246,7 @@ public class SettingsController(MangaContext context, ILog Log) : Controller
[ProducesResponseType(Status200OK)] [ProducesResponseType(Status200OK)]
public IActionResult ClearFlareSolverrUrl() public IActionResult ClearFlareSolverrUrl()
{ {
TrangaSettings.UpdateFlareSolverrUrl(string.Empty); Tranga.Settings.SetFlareSolverrUrl(string.Empty);
return Ok(); return Ok();
} }
@ -331,4 +265,28 @@ public class SettingsController(MangaContext context, ILog Log) : Controller
RequestResult result = client.MakeRequestInternal(knownProtectedUrl); RequestResult result = client.MakeRequestInternal(knownProtectedUrl);
return (int)result.statusCode >= 200 && (int)result.statusCode < 300 ? Ok() : StatusCode(500, result.statusCode); return (int)result.statusCode >= 200 && (int)result.statusCode < 300 ? Ok() : StatusCode(500, result.statusCode);
} }
/// <summary>
/// Returns the language in which Manga are downloaded
/// </summary>
/// <response code="200"></response>
[HttpGet("DownloadLanguage")]
[ProducesResponseType<string>(Status200OK, "text/plain")]
public IActionResult GetDownloadLanguage()
{
return Ok(Tranga.Settings.DownloadLanguage);
}
/// <summary>
/// Sets the language in which Manga are downloaded
/// </summary>
/// <response code="200"></response>
[HttpPatch("DownloadLanguage/{Language}")]
[ProducesResponseType(Status200OK)]
public IActionResult SetDownloadLanguage(string Language)
{
//TODO Validation
Tranga.Settings.SetDownloadLanguage(Language);
return Ok();
}
} }

View File

@ -1,10 +1,8 @@
using API.APIEndpointRecords; using API.APIEndpointRecords;
using API.Schema.MangaContext;
using API.Workers; using API.Workers;
using Asp.Versioning; using Asp.Versioning;
using log4net; using log4net;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using static Microsoft.AspNetCore.Http.StatusCodes; using static Microsoft.AspNetCore.Http.StatusCodes;
// ReSharper disable InconsistentNaming // ReSharper disable InconsistentNaming
@ -13,7 +11,7 @@ namespace API.Controllers;
[ApiVersion(2)] [ApiVersion(2)]
[ApiController] [ApiController]
[Route("v{version:apiVersion}/[controller]")] [Route("v{version:apiVersion}/[controller]")]
public class WorkerController(ILog Log) : Controller public class WorkerController() : Controller
{ {
/// <summary> /// <summary>
/// Returns all <see cref="BaseWorker"/> /// Returns all <see cref="BaseWorker"/>
@ -23,7 +21,7 @@ public class WorkerController(ILog Log) : Controller
[ProducesResponseType<BaseWorker[]>(Status200OK, "application/json")] [ProducesResponseType<BaseWorker[]>(Status200OK, "application/json")]
public IActionResult GetAllWorkers() public IActionResult GetAllWorkers()
{ {
return Ok(Tranga.Workers.ToArray()); return Ok(Tranga.AllWorkers.ToArray());
} }
/// <summary> /// <summary>
@ -35,7 +33,7 @@ public class WorkerController(ILog Log) : Controller
[ProducesResponseType<BaseWorker[]>(Status200OK, "application/json")] [ProducesResponseType<BaseWorker[]>(Status200OK, "application/json")]
public IActionResult GetJobs([FromBody]string[] WorkerIds) public IActionResult GetJobs([FromBody]string[] WorkerIds)
{ {
return Ok(Tranga.Workers.Where(worker => WorkerIds.Contains(worker.Key)).ToArray()); return Ok(Tranga.AllWorkers.Where(worker => WorkerIds.Contains(worker.Key)).ToArray());
} }
/// <summary> /// <summary>
@ -47,7 +45,7 @@ public class WorkerController(ILog Log) : Controller
[ProducesResponseType<BaseWorker[]>(Status200OK, "application/json")] [ProducesResponseType<BaseWorker[]>(Status200OK, "application/json")]
public IActionResult GetJobsInState(WorkerExecutionState State) public IActionResult GetJobsInState(WorkerExecutionState State)
{ {
return Ok(Tranga.Workers.Where(worker => worker.State == State).ToArray()); return Ok(Tranga.AllWorkers.Where(worker => worker.State == State).ToArray());
} }
/// <summary> /// <summary>
@ -61,7 +59,7 @@ public class WorkerController(ILog Log) : Controller
[ProducesResponseType(Status404NotFound)] [ProducesResponseType(Status404NotFound)]
public IActionResult GetJob(string WorkerId) public IActionResult GetJob(string WorkerId)
{ {
if(Tranga.Workers.FirstOrDefault(w => w.Key == WorkerId) is not { } worker) if(Tranga.AllWorkers.FirstOrDefault(w => w.Key == WorkerId) is not { } worker)
return NotFound(nameof(WorkerId)); return NotFound(nameof(WorkerId));
return Ok(worker); return Ok(worker);
} }
@ -77,117 +75,80 @@ public class WorkerController(ILog Log) : Controller
[ProducesResponseType(Status404NotFound)] [ProducesResponseType(Status404NotFound)]
public IActionResult DeleteJob(string WorkerId) public IActionResult DeleteJob(string WorkerId)
{ {
if(Tranga.Workers.FirstOrDefault(w => w.Key == WorkerId) is not { } worker) if(Tranga.AllWorkers.FirstOrDefault(w => w.Key == WorkerId) is not { } worker)
return NotFound(nameof(WorkerId)); return NotFound(nameof(WorkerId));
Tranga.RemoveWorker(worker); Tranga.RemoveWorker(worker);
return Ok(); return Ok();
} }
/// <summary> /// <summary>
/// Modify Job with ID /// Modify <see cref="BaseWorker"/> with <paramref name="WorkerId"/>
/// </summary> /// </summary>
/// <param name="JobId">Job-ID</param> /// <param name="WorkerId"><see cref="BaseWorker"/>.Key</param>
/// <param name="modifyJobRecord">Fields to modify, set to null to keep previous value</param> /// <param name="modifyWorkerRecord">Fields to modify, set to null to keep previous value</param>
/// <response code="202">Job modified</response> /// <response code="202"></response>
/// <response code="400">Malformed request</response> /// <response code="400"></response>
/// <response code="404">Job with ID not found</response> /// <response code="404"><see cref="BaseWorker"/> with <paramref name="WorkerId"/> could not be found</response>
/// <response code="500">Error during Database Operation</response> /// <response code="409"><see cref="BaseWorker"/> is not <see cref="IPeriodic"/>, can not modify <paramref name="modifyWorkerRecord.IntervalMs"/></response>
[HttpPatch("{JobId}")] [HttpPatch("{WorkerId}")]
[ProducesResponseType<Job>(Status202Accepted, "application/json")] [ProducesResponseType<BaseWorker>(Status202Accepted, "application/json")]
[ProducesResponseType(Status400BadRequest)] [ProducesResponseType(Status400BadRequest)]
[ProducesResponseType(Status404NotFound)] [ProducesResponseType(Status404NotFound)]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")] [ProducesResponseType<string>(Status409Conflict, "text/plain")]
public IActionResult ModifyJob(string JobId, [FromBody]ModifyJobRecord modifyJobRecord) public IActionResult ModifyJob(string WorkerId, [FromBody]ModifyWorkerRecord modifyWorkerRecord)
{ {
try if(Tranga.AllWorkers.FirstOrDefault(w => w.Key == WorkerId) is not { } worker)
{ return NotFound(nameof(WorkerId));
Job? ret = context.Jobs.Find(JobId);
if(ret is null) if(modifyWorkerRecord.IntervalMs is not null && worker is not IPeriodic)
return NotFound(); return Conflict("Can not modify Interval of non-Periodic worker");
else if(modifyWorkerRecord.IntervalMs is not null && worker is IPeriodic periodic)
ret.RecurrenceMs = modifyJobRecord.RecurrenceMs ?? ret.RecurrenceMs; periodic.Interval = TimeSpan.FromMilliseconds((long)modifyWorkerRecord.IntervalMs);
ret.Enabled = modifyJobRecord.Enabled ?? ret.Enabled;
return Accepted(worker);
context.SaveChanges();
return new AcceptedResult(ret.Key, ret);
}
catch (Exception e)
{
Log.Error(e);
return StatusCode(500, e.Message);
}
} }
/// <summary> /// <summary>
/// Starts the Job with the requested ID /// Starts <see cref="BaseWorker"/> with <paramref name="WorkerId"/>
/// </summary> /// </summary>
/// <param name="JobId">Job-ID</param> /// <param name="WorkerId"><see cref="BaseWorker"/>.Key</param>
/// <param name="startDependencies">Start Jobs necessary for execution</param> /// <response code="200"></response>
/// <response code="202">Job started</response> /// <response code="404"><see cref="BaseWorker"/> with <paramref name="WorkerId"/> could not be found</response>
/// <response code="404">Job with ID not found</response> /// <response code="412"><see cref="BaseWorker"/> was already running</response>
/// <response code="409">Job was already running</response> [HttpPost("{WorkerId}/Start")]
/// <response code="500">Error during Database Operation</response>
[HttpPost("{JobId}/Start")]
[ProducesResponseType(Status202Accepted)] [ProducesResponseType(Status202Accepted)]
[ProducesResponseType(Status404NotFound)] [ProducesResponseType(Status404NotFound)]
[ProducesResponseType(Status409Conflict)] [ProducesResponseType<string>(Status412PreconditionFailed, "text/plain")]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")] public IActionResult StartJob(string WorkerId)
public IActionResult StartJob(string JobId, [FromBody(EmptyBodyBehavior = EmptyBodyBehavior.Allow)]bool startDependencies = false)
{ {
Job? ret = context.Jobs.Find(JobId); if(Tranga.AllWorkers.FirstOrDefault(w => w.Key == WorkerId) is not { } worker)
if (ret is null) return NotFound(nameof(WorkerId));
return NotFound();
List<Job> dependencies = startDependencies ? ret.GetDependenciesAndSelf() : [ret];
try if (worker.State >= WorkerExecutionState.Waiting)
{ return StatusCode(Status412PreconditionFailed, "Already running");
if(dependencies.Any(d => d.state >= JobState.Running && d.state < JobState.Completed))
return new ConflictResult(); Tranga.MarkWorkerForStart(worker);
dependencies.ForEach(d => return Ok();
{
d.LastExecution = DateTime.UnixEpoch;
d.state = JobState.CompletedWaiting;
});
context.SaveChanges();
return Accepted();
}
catch (Exception e)
{
Log.Error(e);
return StatusCode(500, e.Message);
}
} }
/// <summary> /// <summary>
/// Stops the Job with the requested ID /// Stops <see cref="BaseWorker"/> with <paramref name="WorkerId"/>
/// </summary> /// </summary>
/// <param name="JobId">Job-ID</param> /// <param name="WorkerId"><see cref="BaseWorker"/>.Key</param>
/// <remarks><h1>NOT IMPLEMENTED</h1></remarks> /// <response code="200"></response>
[HttpPost("{JobId}/Stop")] /// <response code="404"><see cref="BaseWorker"/> with <paramref name="WorkerId"/> could not be found</response>
/// <response code="208"><see cref="BaseWorker"/> was not running</response>
[HttpPost("{WorkerId}/Stop")]
[ProducesResponseType(Status501NotImplemented)] [ProducesResponseType(Status501NotImplemented)]
public IActionResult StopJob(string JobId) public IActionResult StopJob(string WorkerId)
{ {
return StatusCode(Status501NotImplemented); if(Tranga.AllWorkers.FirstOrDefault(w => w.Key == WorkerId) is not { } worker)
} return NotFound(nameof(WorkerId));
/// <summary> if(worker.State is < WorkerExecutionState.Running or >= WorkerExecutionState.Completed)
/// Removes failed and completed Jobs (that are not recurring) return StatusCode(Status208AlreadyReported, "Not running");
/// </summary>
/// <response code="202">Job started</response> Tranga.StopWorker(worker);
/// <response code="500">Error during Database Operation</response> return Ok();
[HttpPost("Cleanup")]
public IActionResult CleanupJobs()
{
try
{
context.Jobs.RemoveRange(context.Jobs.Where(j => j.state == JobState.Failed || j.state == JobState.Completed));
context.SaveChanges();
return Ok();
}
catch (Exception e)
{
Log.Error(e);
return StatusCode(500, e.Message);
}
} }
} }

View File

@ -16,14 +16,14 @@ public abstract class DownloadClient
public RequestResult MakeRequest(string url, RequestType requestType, string? referrer = null, string? clickButton = null) public RequestResult MakeRequest(string url, RequestType requestType, string? referrer = null, string? clickButton = null)
{ {
Log.Debug($"Requesting {requestType} {url}"); Log.Debug($"Requesting {requestType} {url}");
if (!TrangaSettings.requestLimits.ContainsKey(requestType)) if (!Tranga.Settings.RequestLimits.ContainsKey(requestType))
{ {
return new RequestResult(HttpStatusCode.NotAcceptable, null, Stream.Null); return new RequestResult(HttpStatusCode.NotAcceptable, null, Stream.Null);
} }
int rateLimit = TrangaSettings.userAgent == TrangaSettings.DefaultUserAgent int rateLimit = Tranga.Settings.UserAgent == TrangaSettings.DefaultUserAgent
? TrangaSettings.DefaultRequestLimits[requestType] ? TrangaSettings.DefaultRequestLimits[requestType]
: TrangaSettings.requestLimits[requestType]; : Tranga.Settings.RequestLimits[requestType];
TimeSpan timeBetweenRequests = TimeSpan.FromMinutes(1).Divide(rateLimit); TimeSpan timeBetweenRequests = TimeSpan.FromMinutes(1).Divide(rateLimit);
DateTime now = DateTime.Now; DateTime now = DateTime.Now;

View File

@ -18,13 +18,13 @@ public class FlareSolverrDownloadClient : DownloadClient
Log.Warn("Client can not click button"); Log.Warn("Client can not click button");
if(referrer is not null) if(referrer is not null)
Log.Warn("Client can not set referrer"); Log.Warn("Client can not set referrer");
if (TrangaSettings.flareSolverrUrl == string.Empty) if (Tranga.Settings.FlareSolverrUrl == string.Empty)
{ {
Log.Error("FlareSolverr URL is empty"); Log.Error("FlareSolverr URL is empty");
return new(HttpStatusCode.InternalServerError, null, Stream.Null); return new(HttpStatusCode.InternalServerError, null, Stream.Null);
} }
Uri flareSolverrUri = new (TrangaSettings.flareSolverrUrl); Uri flareSolverrUri = new (Tranga.Settings.FlareSolverrUrl);
if (flareSolverrUri.Segments.Last() != "v1") if (flareSolverrUri.Segments.Last() != "v1")
flareSolverrUri = new UriBuilder(flareSolverrUri) flareSolverrUri = new UriBuilder(flareSolverrUri)
{ {
@ -35,7 +35,7 @@ public class FlareSolverrDownloadClient : DownloadClient
{ {
Timeout = TimeSpan.FromSeconds(10), Timeout = TimeSpan.FromSeconds(10),
DefaultVersionPolicy = HttpVersionPolicy.RequestVersionOrHigher, DefaultVersionPolicy = HttpVersionPolicy.RequestVersionOrHigher,
DefaultRequestHeaders = { { "User-Agent", TrangaSettings.userAgent } } DefaultRequestHeaders = { { "User-Agent", Tranga.Settings.UserAgent } }
}; };
JObject requestObj = new() JObject requestObj = new()

View File

@ -12,7 +12,7 @@ internal class HttpDownloadClient : DownloadClient
HttpClient client = new(); HttpClient client = new();
client.Timeout = TimeSpan.FromSeconds(10); client.Timeout = TimeSpan.FromSeconds(10);
client.DefaultVersionPolicy = HttpVersionPolicy.RequestVersionOrHigher; client.DefaultVersionPolicy = HttpVersionPolicy.RequestVersionOrHigher;
client.DefaultRequestHeaders.Add("User-Agent", TrangaSettings.userAgent); client.DefaultRequestHeaders.Add("User-Agent", Tranga.Settings.UserAgent);
HttpResponseMessage? response; HttpResponseMessage? response;
Uri uri = new(url); Uri uri = new(url);
HttpRequestMessage requestMessage = new(HttpMethod.Get, uri); HttpRequestMessage requestMessage = new(HttpMethod.Get, uri);

View File

@ -0,0 +1,70 @@
// <auto-generated />
using API.Schema.LibraryContext;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace API.Migrations.Library
{
[DbContext(typeof(LibraryContext))]
[Migration("20250703191925_Initial")]
partial class Initial
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "9.0.5")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("API.Schema.LibraryContext.LibraryConnectors.LibraryConnector", b =>
{
b.Property<string>("Key")
.HasColumnType("text");
b.Property<string>("Auth")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("BaseUrl")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<byte>("LibraryType")
.HasColumnType("smallint");
b.HasKey("Key");
b.ToTable("LibraryConnectors");
b.HasDiscriminator<byte>("LibraryType");
b.UseTphMappingStrategy();
});
modelBuilder.Entity("API.Schema.LibraryContext.LibraryConnectors.Kavita", b =>
{
b.HasBaseType("API.Schema.LibraryContext.LibraryConnectors.LibraryConnector");
b.HasDiscriminator().HasValue((byte)1);
});
modelBuilder.Entity("API.Schema.LibraryContext.LibraryConnectors.Komga", b =>
{
b.HasBaseType("API.Schema.LibraryContext.LibraryConnectors.LibraryConnector");
b.HasDiscriminator().HasValue((byte)0);
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,35 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace API.Migrations.Library
{
/// <inheritdoc />
public partial class Initial : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "LibraryConnectors",
columns: table => new
{
Key = table.Column<string>(type: "text", nullable: false),
LibraryType = table.Column<byte>(type: "smallint", nullable: false),
BaseUrl = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
Auth = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_LibraryConnectors", x => x.Key);
});
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "LibraryConnectors");
}
}
}

View File

@ -0,0 +1,67 @@
// <auto-generated />
using API.Schema.LibraryContext;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace API.Migrations.Library
{
[DbContext(typeof(LibraryContext))]
partial class LibraryContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "9.0.5")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("API.Schema.LibraryContext.LibraryConnectors.LibraryConnector", b =>
{
b.Property<string>("Key")
.HasColumnType("text");
b.Property<string>("Auth")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("BaseUrl")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<byte>("LibraryType")
.HasColumnType("smallint");
b.HasKey("Key");
b.ToTable("LibraryConnectors");
b.HasDiscriminator<byte>("LibraryType");
b.UseTphMappingStrategy();
});
modelBuilder.Entity("API.Schema.LibraryContext.LibraryConnectors.Kavita", b =>
{
b.HasBaseType("API.Schema.LibraryContext.LibraryConnectors.LibraryConnector");
b.HasDiscriminator().HasValue((byte)1);
});
modelBuilder.Entity("API.Schema.LibraryContext.LibraryConnectors.Komga", b =>
{
b.HasBaseType("API.Schema.LibraryContext.LibraryConnectors.LibraryConnector");
b.HasDiscriminator().HasValue((byte)0);
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,547 @@
// <auto-generated />
using API.Schema.MangaContext;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace API.Migrations.Manga
{
[DbContext(typeof(MangaContext))]
[Migration("20250703192023_Initial")]
partial class Initial
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "9.0.5")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("API.Schema.MangaContext.Author", b =>
{
b.Property<string>("Key")
.HasColumnType("text");
b.Property<string>("AuthorName")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("character varying(128)");
b.HasKey("Key");
b.ToTable("Authors");
});
modelBuilder.Entity("API.Schema.MangaContext.Chapter", b =>
{
b.Property<string>("Key")
.HasColumnType("text");
b.Property<string>("ChapterNumber")
.IsRequired()
.HasMaxLength(10)
.HasColumnType("character varying(10)");
b.Property<bool>("Downloaded")
.HasColumnType("boolean");
b.Property<string>("FileName")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("ParentMangaId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("Title")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<int?>("VolumeNumber")
.HasColumnType("integer");
b.HasKey("Key");
b.HasIndex("ParentMangaId");
b.ToTable("Chapters");
});
modelBuilder.Entity("API.Schema.MangaContext.FileLibrary", b =>
{
b.Property<string>("Key")
.HasColumnType("text");
b.Property<string>("BasePath")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("LibraryName")
.IsRequired()
.HasMaxLength(512)
.HasColumnType("character varying(512)");
b.HasKey("Key");
b.ToTable("FileLibraries");
});
modelBuilder.Entity("API.Schema.MangaContext.Manga", b =>
{
b.Property<string>("Key")
.HasColumnType("text");
b.Property<string>("CoverFileNameInCache")
.HasMaxLength(512)
.HasColumnType("character varying(512)");
b.Property<string>("CoverUrl")
.IsRequired()
.HasMaxLength(512)
.HasColumnType("character varying(512)");
b.Property<string>("Description")
.IsRequired()
.HasColumnType("text");
b.Property<string>("DirectoryName")
.IsRequired()
.HasMaxLength(1024)
.HasColumnType("character varying(1024)");
b.Property<float>("IgnoreChaptersBefore")
.HasColumnType("real");
b.Property<string>("LibraryId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(512)
.HasColumnType("character varying(512)");
b.Property<string>("OriginalLanguage")
.HasMaxLength(8)
.HasColumnType("character varying(8)");
b.Property<byte>("ReleaseStatus")
.HasColumnType("smallint");
b.Property<long?>("Year")
.HasColumnType("bigint");
b.HasKey("Key");
b.HasIndex("LibraryId");
b.ToTable("Mangas");
});
modelBuilder.Entity("API.Schema.MangaContext.MangaConnectorId<API.Schema.MangaContext.Chapter>", b =>
{
b.Property<string>("Key")
.HasColumnType("text");
b.Property<string>("IdOnConnectorSite")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("MangaConnectorName")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("character varying(32)");
b.Property<string>("ObjId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<bool>("UseForDownload")
.HasColumnType("boolean");
b.Property<string>("WebsiteUrl")
.HasMaxLength(512)
.HasColumnType("character varying(512)");
b.HasKey("Key");
b.HasIndex("MangaConnectorName");
b.HasIndex("ObjId");
b.ToTable("MangaConnectorToChapter");
});
modelBuilder.Entity("API.Schema.MangaContext.MangaConnectorId<API.Schema.MangaContext.Manga>", b =>
{
b.Property<string>("Key")
.HasColumnType("text");
b.Property<string>("IdOnConnectorSite")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("MangaConnectorName")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("character varying(32)");
b.Property<string>("ObjId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<bool>("UseForDownload")
.HasColumnType("boolean");
b.Property<string>("WebsiteUrl")
.HasMaxLength(512)
.HasColumnType("character varying(512)");
b.HasKey("Key");
b.HasIndex("MangaConnectorName");
b.HasIndex("ObjId");
b.ToTable("MangaConnectorToManga");
});
modelBuilder.Entity("API.Schema.MangaContext.MangaConnectors.MangaConnector", b =>
{
b.Property<string>("Name")
.HasMaxLength(32)
.HasColumnType("character varying(32)");
b.PrimitiveCollection<string[]>("BaseUris")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("text[]");
b.Property<bool>("Enabled")
.HasColumnType("boolean");
b.Property<string>("IconUrl")
.IsRequired()
.HasMaxLength(2048)
.HasColumnType("character varying(2048)");
b.PrimitiveCollection<string[]>("SupportedLanguages")
.IsRequired()
.HasMaxLength(8)
.HasColumnType("text[]");
b.HasKey("Name");
b.ToTable("MangaConnectors");
b.HasDiscriminator<string>("Name").HasValue("MangaConnector");
b.UseTphMappingStrategy();
});
modelBuilder.Entity("API.Schema.MangaContext.MangaTag", b =>
{
b.Property<string>("Tag")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.HasKey("Tag");
b.ToTable("Tags");
});
modelBuilder.Entity("API.Schema.MangaContext.MetadataFetchers.MetadataEntry", b =>
{
b.Property<string>("MetadataFetcherName")
.HasColumnType("text");
b.Property<string>("Identifier")
.HasColumnType("text");
b.Property<string>("MangaId")
.IsRequired()
.HasColumnType("text");
b.HasKey("MetadataFetcherName", "Identifier");
b.HasIndex("MangaId");
b.ToTable("MetadataEntries");
});
modelBuilder.Entity("API.Schema.MangaContext.MetadataFetchers.MetadataFetcher", b =>
{
b.Property<string>("Name")
.HasColumnType("text");
b.Property<string>("MetadataEntry")
.IsRequired()
.HasMaxLength(21)
.HasColumnType("character varying(21)");
b.HasKey("Name");
b.ToTable("MetadataFetcher");
b.HasDiscriminator<string>("MetadataEntry").HasValue("MetadataFetcher");
b.UseTphMappingStrategy();
});
modelBuilder.Entity("AuthorToManga", b =>
{
b.Property<string>("AuthorIds")
.HasColumnType("text");
b.Property<string>("MangaIds")
.HasColumnType("text");
b.HasKey("AuthorIds", "MangaIds");
b.HasIndex("MangaIds");
b.ToTable("AuthorToManga");
});
modelBuilder.Entity("MangaTagToManga", b =>
{
b.Property<string>("MangaTagIds")
.HasColumnType("character varying(64)");
b.Property<string>("MangaIds")
.HasColumnType("text");
b.HasKey("MangaTagIds", "MangaIds");
b.HasIndex("MangaIds");
b.ToTable("MangaTagToManga");
});
modelBuilder.Entity("API.Schema.MangaContext.MangaConnectors.ComickIo", b =>
{
b.HasBaseType("API.Schema.MangaContext.MangaConnectors.MangaConnector");
b.HasDiscriminator().HasValue("ComickIo");
});
modelBuilder.Entity("API.Schema.MangaContext.MangaConnectors.Global", b =>
{
b.HasBaseType("API.Schema.MangaContext.MangaConnectors.MangaConnector");
b.HasDiscriminator().HasValue("Global");
});
modelBuilder.Entity("API.Schema.MangaContext.MangaConnectors.MangaDex", b =>
{
b.HasBaseType("API.Schema.MangaContext.MangaConnectors.MangaConnector");
b.HasDiscriminator().HasValue("MangaDex");
});
modelBuilder.Entity("API.Schema.MangaContext.MetadataFetchers.MyAnimeList", b =>
{
b.HasBaseType("API.Schema.MangaContext.MetadataFetchers.MetadataFetcher");
b.HasDiscriminator().HasValue("MyAnimeList");
});
modelBuilder.Entity("API.Schema.MangaContext.Chapter", b =>
{
b.HasOne("API.Schema.MangaContext.Manga", "ParentManga")
.WithMany("Chapters")
.HasForeignKey("ParentMangaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("ParentManga");
});
modelBuilder.Entity("API.Schema.MangaContext.Manga", b =>
{
b.HasOne("API.Schema.MangaContext.FileLibrary", "Library")
.WithMany()
.HasForeignKey("LibraryId")
.OnDelete(DeleteBehavior.SetNull);
b.OwnsMany("API.Schema.MangaContext.AltTitle", "AltTitles", b1 =>
{
b1.Property<string>("Key")
.HasColumnType("text");
b1.Property<string>("Language")
.IsRequired()
.HasMaxLength(8)
.HasColumnType("character varying(8)");
b1.Property<string>("MangaKey")
.IsRequired()
.HasColumnType("text");
b1.Property<string>("Title")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b1.HasKey("Key");
b1.HasIndex("MangaKey");
b1.ToTable("AltTitle");
b1.WithOwner()
.HasForeignKey("MangaKey");
});
b.OwnsMany("API.Schema.MangaContext.Link", "Links", b1 =>
{
b1.Property<string>("Key")
.HasColumnType("text");
b1.Property<string>("LinkProvider")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b1.Property<string>("LinkUrl")
.IsRequired()
.HasMaxLength(2048)
.HasColumnType("character varying(2048)");
b1.Property<string>("MangaKey")
.IsRequired()
.HasColumnType("text");
b1.HasKey("Key");
b1.HasIndex("MangaKey");
b1.ToTable("Link");
b1.WithOwner()
.HasForeignKey("MangaKey");
});
b.Navigation("AltTitles");
b.Navigation("Library");
b.Navigation("Links");
});
modelBuilder.Entity("API.Schema.MangaContext.MangaConnectorId<API.Schema.MangaContext.Chapter>", b =>
{
b.HasOne("API.Schema.MangaContext.MangaConnectors.MangaConnector", "MangaConnector")
.WithMany()
.HasForeignKey("MangaConnectorName")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("API.Schema.MangaContext.Chapter", "Obj")
.WithMany("MangaConnectorIds")
.HasForeignKey("ObjId")
.OnDelete(DeleteBehavior.NoAction)
.IsRequired();
b.Navigation("MangaConnector");
b.Navigation("Obj");
});
modelBuilder.Entity("API.Schema.MangaContext.MangaConnectorId<API.Schema.MangaContext.Manga>", b =>
{
b.HasOne("API.Schema.MangaContext.MangaConnectors.MangaConnector", "MangaConnector")
.WithMany()
.HasForeignKey("MangaConnectorName")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("API.Schema.MangaContext.Manga", "Obj")
.WithMany("MangaConnectorIds")
.HasForeignKey("ObjId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("MangaConnector");
b.Navigation("Obj");
});
modelBuilder.Entity("API.Schema.MangaContext.MetadataFetchers.MetadataEntry", b =>
{
b.HasOne("API.Schema.MangaContext.Manga", "Manga")
.WithMany()
.HasForeignKey("MangaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("API.Schema.MangaContext.MetadataFetchers.MetadataFetcher", "MetadataFetcher")
.WithMany()
.HasForeignKey("MetadataFetcherName")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Manga");
b.Navigation("MetadataFetcher");
});
modelBuilder.Entity("AuthorToManga", b =>
{
b.HasOne("API.Schema.MangaContext.Author", null)
.WithMany()
.HasForeignKey("AuthorIds")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("API.Schema.MangaContext.Manga", null)
.WithMany()
.HasForeignKey("MangaIds")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("MangaTagToManga", b =>
{
b.HasOne("API.Schema.MangaContext.Manga", null)
.WithMany()
.HasForeignKey("MangaIds")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("API.Schema.MangaContext.MangaTag", null)
.WithMany()
.HasForeignKey("MangaTagIds")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("API.Schema.MangaContext.Chapter", b =>
{
b.Navigation("MangaConnectorIds");
});
modelBuilder.Entity("API.Schema.MangaContext.Manga", b =>
{
b.Navigation("Chapters");
b.Navigation("MangaConnectorIds");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,396 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace API.Migrations.Manga
{
/// <inheritdoc />
public partial class Initial : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Authors",
columns: table => new
{
Key = table.Column<string>(type: "text", nullable: false),
AuthorName = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Authors", x => x.Key);
});
migrationBuilder.CreateTable(
name: "FileLibraries",
columns: table => new
{
Key = table.Column<string>(type: "text", nullable: false),
BasePath = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
LibraryName = table.Column<string>(type: "character varying(512)", maxLength: 512, nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_FileLibraries", x => x.Key);
});
migrationBuilder.CreateTable(
name: "MangaConnectors",
columns: table => new
{
Name = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: false),
SupportedLanguages = table.Column<string[]>(type: "text[]", maxLength: 8, nullable: false),
IconUrl = table.Column<string>(type: "character varying(2048)", maxLength: 2048, nullable: false),
BaseUris = table.Column<string[]>(type: "text[]", maxLength: 256, nullable: false),
Enabled = table.Column<bool>(type: "boolean", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_MangaConnectors", x => x.Name);
});
migrationBuilder.CreateTable(
name: "MetadataFetcher",
columns: table => new
{
Name = table.Column<string>(type: "text", nullable: false),
MetadataEntry = table.Column<string>(type: "character varying(21)", maxLength: 21, nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_MetadataFetcher", x => x.Name);
});
migrationBuilder.CreateTable(
name: "Tags",
columns: table => new
{
Tag = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Tags", x => x.Tag);
});
migrationBuilder.CreateTable(
name: "Mangas",
columns: table => new
{
Key = table.Column<string>(type: "text", nullable: false),
Name = table.Column<string>(type: "character varying(512)", maxLength: 512, nullable: false),
Description = table.Column<string>(type: "text", nullable: false),
CoverUrl = table.Column<string>(type: "character varying(512)", maxLength: 512, nullable: false),
ReleaseStatus = table.Column<byte>(type: "smallint", nullable: false),
LibraryId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: true),
IgnoreChaptersBefore = table.Column<float>(type: "real", nullable: false),
DirectoryName = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: false),
CoverFileNameInCache = table.Column<string>(type: "character varying(512)", maxLength: 512, nullable: true),
Year = table.Column<long>(type: "bigint", nullable: true),
OriginalLanguage = table.Column<string>(type: "character varying(8)", maxLength: 8, nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Mangas", x => x.Key);
table.ForeignKey(
name: "FK_Mangas_FileLibraries_LibraryId",
column: x => x.LibraryId,
principalTable: "FileLibraries",
principalColumn: "Key",
onDelete: ReferentialAction.SetNull);
});
migrationBuilder.CreateTable(
name: "AltTitle",
columns: table => new
{
Key = table.Column<string>(type: "text", nullable: false),
Language = table.Column<string>(type: "character varying(8)", maxLength: 8, nullable: false),
Title = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
MangaKey = table.Column<string>(type: "text", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_AltTitle", x => x.Key);
table.ForeignKey(
name: "FK_AltTitle_Mangas_MangaKey",
column: x => x.MangaKey,
principalTable: "Mangas",
principalColumn: "Key",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "AuthorToManga",
columns: table => new
{
AuthorIds = table.Column<string>(type: "text", nullable: false),
MangaIds = table.Column<string>(type: "text", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_AuthorToManga", x => new { x.AuthorIds, x.MangaIds });
table.ForeignKey(
name: "FK_AuthorToManga_Authors_AuthorIds",
column: x => x.AuthorIds,
principalTable: "Authors",
principalColumn: "Key",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_AuthorToManga_Mangas_MangaIds",
column: x => x.MangaIds,
principalTable: "Mangas",
principalColumn: "Key",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "Chapters",
columns: table => new
{
Key = table.Column<string>(type: "text", nullable: false),
ParentMangaId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
VolumeNumber = table.Column<int>(type: "integer", nullable: true),
ChapterNumber = table.Column<string>(type: "character varying(10)", maxLength: 10, nullable: false),
Title = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
FileName = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
Downloaded = table.Column<bool>(type: "boolean", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Chapters", x => x.Key);
table.ForeignKey(
name: "FK_Chapters_Mangas_ParentMangaId",
column: x => x.ParentMangaId,
principalTable: "Mangas",
principalColumn: "Key",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "Link",
columns: table => new
{
Key = table.Column<string>(type: "text", nullable: false),
LinkProvider = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
LinkUrl = table.Column<string>(type: "character varying(2048)", maxLength: 2048, nullable: false),
MangaKey = table.Column<string>(type: "text", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Link", x => x.Key);
table.ForeignKey(
name: "FK_Link_Mangas_MangaKey",
column: x => x.MangaKey,
principalTable: "Mangas",
principalColumn: "Key",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "MangaConnectorToManga",
columns: table => new
{
Key = table.Column<string>(type: "text", nullable: false),
ObjId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
MangaConnectorName = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: false),
IdOnConnectorSite = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
WebsiteUrl = table.Column<string>(type: "character varying(512)", maxLength: 512, nullable: true),
UseForDownload = table.Column<bool>(type: "boolean", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_MangaConnectorToManga", x => x.Key);
table.ForeignKey(
name: "FK_MangaConnectorToManga_MangaConnectors_MangaConnectorName",
column: x => x.MangaConnectorName,
principalTable: "MangaConnectors",
principalColumn: "Name",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_MangaConnectorToManga_Mangas_ObjId",
column: x => x.ObjId,
principalTable: "Mangas",
principalColumn: "Key",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "MangaTagToManga",
columns: table => new
{
MangaTagIds = table.Column<string>(type: "character varying(64)", nullable: false),
MangaIds = table.Column<string>(type: "text", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_MangaTagToManga", x => new { x.MangaTagIds, x.MangaIds });
table.ForeignKey(
name: "FK_MangaTagToManga_Mangas_MangaIds",
column: x => x.MangaIds,
principalTable: "Mangas",
principalColumn: "Key",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_MangaTagToManga_Tags_MangaTagIds",
column: x => x.MangaTagIds,
principalTable: "Tags",
principalColumn: "Tag",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "MetadataEntries",
columns: table => new
{
MetadataFetcherName = table.Column<string>(type: "text", nullable: false),
Identifier = table.Column<string>(type: "text", nullable: false),
MangaId = table.Column<string>(type: "text", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_MetadataEntries", x => new { x.MetadataFetcherName, x.Identifier });
table.ForeignKey(
name: "FK_MetadataEntries_Mangas_MangaId",
column: x => x.MangaId,
principalTable: "Mangas",
principalColumn: "Key",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_MetadataEntries_MetadataFetcher_MetadataFetcherName",
column: x => x.MetadataFetcherName,
principalTable: "MetadataFetcher",
principalColumn: "Name",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "MangaConnectorToChapter",
columns: table => new
{
Key = table.Column<string>(type: "text", nullable: false),
ObjId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
MangaConnectorName = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: false),
IdOnConnectorSite = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
WebsiteUrl = table.Column<string>(type: "character varying(512)", maxLength: 512, nullable: true),
UseForDownload = table.Column<bool>(type: "boolean", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_MangaConnectorToChapter", x => x.Key);
table.ForeignKey(
name: "FK_MangaConnectorToChapter_Chapters_ObjId",
column: x => x.ObjId,
principalTable: "Chapters",
principalColumn: "Key");
table.ForeignKey(
name: "FK_MangaConnectorToChapter_MangaConnectors_MangaConnectorName",
column: x => x.MangaConnectorName,
principalTable: "MangaConnectors",
principalColumn: "Name",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_AltTitle_MangaKey",
table: "AltTitle",
column: "MangaKey");
migrationBuilder.CreateIndex(
name: "IX_AuthorToManga_MangaIds",
table: "AuthorToManga",
column: "MangaIds");
migrationBuilder.CreateIndex(
name: "IX_Chapters_ParentMangaId",
table: "Chapters",
column: "ParentMangaId");
migrationBuilder.CreateIndex(
name: "IX_Link_MangaKey",
table: "Link",
column: "MangaKey");
migrationBuilder.CreateIndex(
name: "IX_MangaConnectorToChapter_MangaConnectorName",
table: "MangaConnectorToChapter",
column: "MangaConnectorName");
migrationBuilder.CreateIndex(
name: "IX_MangaConnectorToChapter_ObjId",
table: "MangaConnectorToChapter",
column: "ObjId");
migrationBuilder.CreateIndex(
name: "IX_MangaConnectorToManga_MangaConnectorName",
table: "MangaConnectorToManga",
column: "MangaConnectorName");
migrationBuilder.CreateIndex(
name: "IX_MangaConnectorToManga_ObjId",
table: "MangaConnectorToManga",
column: "ObjId");
migrationBuilder.CreateIndex(
name: "IX_Mangas_LibraryId",
table: "Mangas",
column: "LibraryId");
migrationBuilder.CreateIndex(
name: "IX_MangaTagToManga_MangaIds",
table: "MangaTagToManga",
column: "MangaIds");
migrationBuilder.CreateIndex(
name: "IX_MetadataEntries_MangaId",
table: "MetadataEntries",
column: "MangaId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "AltTitle");
migrationBuilder.DropTable(
name: "AuthorToManga");
migrationBuilder.DropTable(
name: "Link");
migrationBuilder.DropTable(
name: "MangaConnectorToChapter");
migrationBuilder.DropTable(
name: "MangaConnectorToManga");
migrationBuilder.DropTable(
name: "MangaTagToManga");
migrationBuilder.DropTable(
name: "MetadataEntries");
migrationBuilder.DropTable(
name: "Authors");
migrationBuilder.DropTable(
name: "Chapters");
migrationBuilder.DropTable(
name: "MangaConnectors");
migrationBuilder.DropTable(
name: "Tags");
migrationBuilder.DropTable(
name: "MetadataFetcher");
migrationBuilder.DropTable(
name: "Mangas");
migrationBuilder.DropTable(
name: "FileLibraries");
}
}
}

View File

@ -0,0 +1,544 @@
// <auto-generated />
using API.Schema.MangaContext;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace API.Migrations.Manga
{
[DbContext(typeof(MangaContext))]
partial class MangaContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "9.0.5")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("API.Schema.MangaContext.Author", b =>
{
b.Property<string>("Key")
.HasColumnType("text");
b.Property<string>("AuthorName")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("character varying(128)");
b.HasKey("Key");
b.ToTable("Authors");
});
modelBuilder.Entity("API.Schema.MangaContext.Chapter", b =>
{
b.Property<string>("Key")
.HasColumnType("text");
b.Property<string>("ChapterNumber")
.IsRequired()
.HasMaxLength(10)
.HasColumnType("character varying(10)");
b.Property<bool>("Downloaded")
.HasColumnType("boolean");
b.Property<string>("FileName")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("ParentMangaId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("Title")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<int?>("VolumeNumber")
.HasColumnType("integer");
b.HasKey("Key");
b.HasIndex("ParentMangaId");
b.ToTable("Chapters");
});
modelBuilder.Entity("API.Schema.MangaContext.FileLibrary", b =>
{
b.Property<string>("Key")
.HasColumnType("text");
b.Property<string>("BasePath")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("LibraryName")
.IsRequired()
.HasMaxLength(512)
.HasColumnType("character varying(512)");
b.HasKey("Key");
b.ToTable("FileLibraries");
});
modelBuilder.Entity("API.Schema.MangaContext.Manga", b =>
{
b.Property<string>("Key")
.HasColumnType("text");
b.Property<string>("CoverFileNameInCache")
.HasMaxLength(512)
.HasColumnType("character varying(512)");
b.Property<string>("CoverUrl")
.IsRequired()
.HasMaxLength(512)
.HasColumnType("character varying(512)");
b.Property<string>("Description")
.IsRequired()
.HasColumnType("text");
b.Property<string>("DirectoryName")
.IsRequired()
.HasMaxLength(1024)
.HasColumnType("character varying(1024)");
b.Property<float>("IgnoreChaptersBefore")
.HasColumnType("real");
b.Property<string>("LibraryId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(512)
.HasColumnType("character varying(512)");
b.Property<string>("OriginalLanguage")
.HasMaxLength(8)
.HasColumnType("character varying(8)");
b.Property<byte>("ReleaseStatus")
.HasColumnType("smallint");
b.Property<long?>("Year")
.HasColumnType("bigint");
b.HasKey("Key");
b.HasIndex("LibraryId");
b.ToTable("Mangas");
});
modelBuilder.Entity("API.Schema.MangaContext.MangaConnectorId<API.Schema.MangaContext.Chapter>", b =>
{
b.Property<string>("Key")
.HasColumnType("text");
b.Property<string>("IdOnConnectorSite")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("MangaConnectorName")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("character varying(32)");
b.Property<string>("ObjId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<bool>("UseForDownload")
.HasColumnType("boolean");
b.Property<string>("WebsiteUrl")
.HasMaxLength(512)
.HasColumnType("character varying(512)");
b.HasKey("Key");
b.HasIndex("MangaConnectorName");
b.HasIndex("ObjId");
b.ToTable("MangaConnectorToChapter");
});
modelBuilder.Entity("API.Schema.MangaContext.MangaConnectorId<API.Schema.MangaContext.Manga>", b =>
{
b.Property<string>("Key")
.HasColumnType("text");
b.Property<string>("IdOnConnectorSite")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("MangaConnectorName")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("character varying(32)");
b.Property<string>("ObjId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<bool>("UseForDownload")
.HasColumnType("boolean");
b.Property<string>("WebsiteUrl")
.HasMaxLength(512)
.HasColumnType("character varying(512)");
b.HasKey("Key");
b.HasIndex("MangaConnectorName");
b.HasIndex("ObjId");
b.ToTable("MangaConnectorToManga");
});
modelBuilder.Entity("API.Schema.MangaContext.MangaConnectors.MangaConnector", b =>
{
b.Property<string>("Name")
.HasMaxLength(32)
.HasColumnType("character varying(32)");
b.PrimitiveCollection<string[]>("BaseUris")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("text[]");
b.Property<bool>("Enabled")
.HasColumnType("boolean");
b.Property<string>("IconUrl")
.IsRequired()
.HasMaxLength(2048)
.HasColumnType("character varying(2048)");
b.PrimitiveCollection<string[]>("SupportedLanguages")
.IsRequired()
.HasMaxLength(8)
.HasColumnType("text[]");
b.HasKey("Name");
b.ToTable("MangaConnectors");
b.HasDiscriminator<string>("Name").HasValue("MangaConnector");
b.UseTphMappingStrategy();
});
modelBuilder.Entity("API.Schema.MangaContext.MangaTag", b =>
{
b.Property<string>("Tag")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.HasKey("Tag");
b.ToTable("Tags");
});
modelBuilder.Entity("API.Schema.MangaContext.MetadataFetchers.MetadataEntry", b =>
{
b.Property<string>("MetadataFetcherName")
.HasColumnType("text");
b.Property<string>("Identifier")
.HasColumnType("text");
b.Property<string>("MangaId")
.IsRequired()
.HasColumnType("text");
b.HasKey("MetadataFetcherName", "Identifier");
b.HasIndex("MangaId");
b.ToTable("MetadataEntries");
});
modelBuilder.Entity("API.Schema.MangaContext.MetadataFetchers.MetadataFetcher", b =>
{
b.Property<string>("Name")
.HasColumnType("text");
b.Property<string>("MetadataEntry")
.IsRequired()
.HasMaxLength(21)
.HasColumnType("character varying(21)");
b.HasKey("Name");
b.ToTable("MetadataFetcher");
b.HasDiscriminator<string>("MetadataEntry").HasValue("MetadataFetcher");
b.UseTphMappingStrategy();
});
modelBuilder.Entity("AuthorToManga", b =>
{
b.Property<string>("AuthorIds")
.HasColumnType("text");
b.Property<string>("MangaIds")
.HasColumnType("text");
b.HasKey("AuthorIds", "MangaIds");
b.HasIndex("MangaIds");
b.ToTable("AuthorToManga");
});
modelBuilder.Entity("MangaTagToManga", b =>
{
b.Property<string>("MangaTagIds")
.HasColumnType("character varying(64)");
b.Property<string>("MangaIds")
.HasColumnType("text");
b.HasKey("MangaTagIds", "MangaIds");
b.HasIndex("MangaIds");
b.ToTable("MangaTagToManga");
});
modelBuilder.Entity("API.Schema.MangaContext.MangaConnectors.ComickIo", b =>
{
b.HasBaseType("API.Schema.MangaContext.MangaConnectors.MangaConnector");
b.HasDiscriminator().HasValue("ComickIo");
});
modelBuilder.Entity("API.Schema.MangaContext.MangaConnectors.Global", b =>
{
b.HasBaseType("API.Schema.MangaContext.MangaConnectors.MangaConnector");
b.HasDiscriminator().HasValue("Global");
});
modelBuilder.Entity("API.Schema.MangaContext.MangaConnectors.MangaDex", b =>
{
b.HasBaseType("API.Schema.MangaContext.MangaConnectors.MangaConnector");
b.HasDiscriminator().HasValue("MangaDex");
});
modelBuilder.Entity("API.Schema.MangaContext.MetadataFetchers.MyAnimeList", b =>
{
b.HasBaseType("API.Schema.MangaContext.MetadataFetchers.MetadataFetcher");
b.HasDiscriminator().HasValue("MyAnimeList");
});
modelBuilder.Entity("API.Schema.MangaContext.Chapter", b =>
{
b.HasOne("API.Schema.MangaContext.Manga", "ParentManga")
.WithMany("Chapters")
.HasForeignKey("ParentMangaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("ParentManga");
});
modelBuilder.Entity("API.Schema.MangaContext.Manga", b =>
{
b.HasOne("API.Schema.MangaContext.FileLibrary", "Library")
.WithMany()
.HasForeignKey("LibraryId")
.OnDelete(DeleteBehavior.SetNull);
b.OwnsMany("API.Schema.MangaContext.AltTitle", "AltTitles", b1 =>
{
b1.Property<string>("Key")
.HasColumnType("text");
b1.Property<string>("Language")
.IsRequired()
.HasMaxLength(8)
.HasColumnType("character varying(8)");
b1.Property<string>("MangaKey")
.IsRequired()
.HasColumnType("text");
b1.Property<string>("Title")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b1.HasKey("Key");
b1.HasIndex("MangaKey");
b1.ToTable("AltTitle");
b1.WithOwner()
.HasForeignKey("MangaKey");
});
b.OwnsMany("API.Schema.MangaContext.Link", "Links", b1 =>
{
b1.Property<string>("Key")
.HasColumnType("text");
b1.Property<string>("LinkProvider")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b1.Property<string>("LinkUrl")
.IsRequired()
.HasMaxLength(2048)
.HasColumnType("character varying(2048)");
b1.Property<string>("MangaKey")
.IsRequired()
.HasColumnType("text");
b1.HasKey("Key");
b1.HasIndex("MangaKey");
b1.ToTable("Link");
b1.WithOwner()
.HasForeignKey("MangaKey");
});
b.Navigation("AltTitles");
b.Navigation("Library");
b.Navigation("Links");
});
modelBuilder.Entity("API.Schema.MangaContext.MangaConnectorId<API.Schema.MangaContext.Chapter>", b =>
{
b.HasOne("API.Schema.MangaContext.MangaConnectors.MangaConnector", "MangaConnector")
.WithMany()
.HasForeignKey("MangaConnectorName")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("API.Schema.MangaContext.Chapter", "Obj")
.WithMany("MangaConnectorIds")
.HasForeignKey("ObjId")
.OnDelete(DeleteBehavior.NoAction)
.IsRequired();
b.Navigation("MangaConnector");
b.Navigation("Obj");
});
modelBuilder.Entity("API.Schema.MangaContext.MangaConnectorId<API.Schema.MangaContext.Manga>", b =>
{
b.HasOne("API.Schema.MangaContext.MangaConnectors.MangaConnector", "MangaConnector")
.WithMany()
.HasForeignKey("MangaConnectorName")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("API.Schema.MangaContext.Manga", "Obj")
.WithMany("MangaConnectorIds")
.HasForeignKey("ObjId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("MangaConnector");
b.Navigation("Obj");
});
modelBuilder.Entity("API.Schema.MangaContext.MetadataFetchers.MetadataEntry", b =>
{
b.HasOne("API.Schema.MangaContext.Manga", "Manga")
.WithMany()
.HasForeignKey("MangaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("API.Schema.MangaContext.MetadataFetchers.MetadataFetcher", "MetadataFetcher")
.WithMany()
.HasForeignKey("MetadataFetcherName")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Manga");
b.Navigation("MetadataFetcher");
});
modelBuilder.Entity("AuthorToManga", b =>
{
b.HasOne("API.Schema.MangaContext.Author", null)
.WithMany()
.HasForeignKey("AuthorIds")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("API.Schema.MangaContext.Manga", null)
.WithMany()
.HasForeignKey("MangaIds")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("MangaTagToManga", b =>
{
b.HasOne("API.Schema.MangaContext.Manga", null)
.WithMany()
.HasForeignKey("MangaIds")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("API.Schema.MangaContext.MangaTag", null)
.WithMany()
.HasForeignKey("MangaTagIds")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("API.Schema.MangaContext.Chapter", b =>
{
b.Navigation("MangaConnectorIds");
});
modelBuilder.Entity("API.Schema.MangaContext.Manga", b =>
{
b.Navigation("Chapters");
b.Navigation("MangaConnectorIds");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,91 @@
// <auto-generated />
using System;
using System.Collections.Generic;
using API.Schema.NotificationsContext;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace API.Migrations.Notifications
{
[DbContext(typeof(NotificationsContext))]
[Migration("20250703191820_Initial")]
partial class Initial
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "9.0.5")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "hstore");
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("API.Schema.NotificationsContext.Notification", b =>
{
b.Property<string>("Key")
.HasColumnType("text");
b.Property<DateTime>("Date")
.HasColumnType("timestamp with time zone");
b.Property<bool>("IsSent")
.HasColumnType("boolean");
b.Property<string>("Message")
.IsRequired()
.HasMaxLength(512)
.HasColumnType("character varying(512)");
b.Property<string>("Title")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("character varying(128)");
b.Property<byte>("Urgency")
.HasColumnType("smallint");
b.HasKey("Key");
b.ToTable("Notifications");
});
modelBuilder.Entity("API.Schema.NotificationsContext.NotificationConnectors.NotificationConnector", b =>
{
b.Property<string>("Name")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("Body")
.IsRequired()
.HasMaxLength(4096)
.HasColumnType("character varying(4096)");
b.Property<Dictionary<string, string>>("Headers")
.IsRequired()
.HasColumnType("hstore");
b.Property<string>("HttpMethod")
.IsRequired()
.HasMaxLength(8)
.HasColumnType("character varying(8)");
b.Property<string>("Url")
.IsRequired()
.HasMaxLength(2048)
.HasColumnType("character varying(2048)");
b.HasKey("Name");
b.ToTable("NotificationConnectors");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,60 @@
using System;
using System.Collections.Generic;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace API.Migrations.Notifications
{
/// <inheritdoc />
public partial class Initial : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterDatabase()
.Annotation("Npgsql:PostgresExtension:hstore", ",,");
migrationBuilder.CreateTable(
name: "NotificationConnectors",
columns: table => new
{
Name = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
Url = table.Column<string>(type: "character varying(2048)", maxLength: 2048, nullable: false),
Headers = table.Column<Dictionary<string, string>>(type: "hstore", nullable: false),
HttpMethod = table.Column<string>(type: "character varying(8)", maxLength: 8, nullable: false),
Body = table.Column<string>(type: "character varying(4096)", maxLength: 4096, nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_NotificationConnectors", x => x.Name);
});
migrationBuilder.CreateTable(
name: "Notifications",
columns: table => new
{
Key = table.Column<string>(type: "text", nullable: false),
Urgency = table.Column<byte>(type: "smallint", nullable: false),
Title = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: false),
Message = table.Column<string>(type: "character varying(512)", maxLength: 512, nullable: false),
Date = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
IsSent = table.Column<bool>(type: "boolean", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Notifications", x => x.Key);
});
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "NotificationConnectors");
migrationBuilder.DropTable(
name: "Notifications");
}
}
}

View File

@ -0,0 +1,88 @@
// <auto-generated />
using System;
using System.Collections.Generic;
using API.Schema.NotificationsContext;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace API.Migrations.Notifications
{
[DbContext(typeof(NotificationsContext))]
partial class NotificationsContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "9.0.5")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "hstore");
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("API.Schema.NotificationsContext.Notification", b =>
{
b.Property<string>("Key")
.HasColumnType("text");
b.Property<DateTime>("Date")
.HasColumnType("timestamp with time zone");
b.Property<bool>("IsSent")
.HasColumnType("boolean");
b.Property<string>("Message")
.IsRequired()
.HasMaxLength(512)
.HasColumnType("character varying(512)");
b.Property<string>("Title")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("character varying(128)");
b.Property<byte>("Urgency")
.HasColumnType("smallint");
b.HasKey("Key");
b.ToTable("Notifications");
});
modelBuilder.Entity("API.Schema.NotificationsContext.NotificationConnectors.NotificationConnector", b =>
{
b.Property<string>("Name")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("Body")
.IsRequired()
.HasMaxLength(4096)
.HasColumnType("character varying(4096)");
b.Property<Dictionary<string, string>>("Headers")
.IsRequired()
.HasColumnType("hstore");
b.Property<string>("HttpMethod")
.IsRequired()
.HasMaxLength(8)
.HasColumnType("character varying(8)");
b.Property<string>("Url")
.IsRequired()
.HasMaxLength(2048)
.HasColumnType("character varying(2048)");
b.HasKey("Name");
b.ToTable("NotificationConnectors");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -97,8 +97,7 @@ app.MapControllers()
app.UseSwagger(); app.UseSwagger();
app.UseSwaggerUI(options => app.UseSwaggerUI(options =>
{ {
options.SwaggerEndpoint( options.SwaggerEndpoint($"/swagger/v2/swagger.json", "v2");
$"/swagger/v2/swagger.json", "v2");
}); });
app.UseHttpsRedirection(); app.UseHttpsRedirection();
@ -118,8 +117,8 @@ using (IServiceScope scope = app.Services.CreateScope())
]; ];
MangaConnector[] newConnectors = connectors.Where(c => !context.MangaConnectors.Contains(c)).ToArray(); MangaConnector[] newConnectors = connectors.Where(c => !context.MangaConnectors.Contains(c)).ToArray();
context.MangaConnectors.AddRange(newConnectors); context.MangaConnectors.AddRange(newConnectors);
if (!context.LocalLibraries.Any()) if (!context.FileLibraries.Any())
context.LocalLibraries.Add(new FileLibrary(TrangaSettings.downloadLocation, "Default FileLibrary")); context.FileLibraries.Add(new FileLibrary(Tranga.Settings.DownloadLocation, "Default FileLibrary"));
context.Sync(); context.Sync();
} }
@ -128,7 +127,8 @@ using (IServiceScope scope = app.Services.CreateScope())
{ {
NotificationsContext context = scope.ServiceProvider.GetRequiredService<NotificationsContext>(); NotificationsContext context = scope.ServiceProvider.GetRequiredService<NotificationsContext>();
context.Database.Migrate(); context.Database.Migrate();
context.Notifications.RemoveRange(context.Notifications);
string[] emojis = { "(•‿•)", "(づ \u25d5‿\u25d5 )づ", "( \u02d8\u25bd\u02d8)っ\u2668", "=\uff3e\u25cf \u22cf \u25cf\uff3e=", "(ΦωΦ)", "(\u272a\u3268\u272a)", "( ノ・o・ )ノ", "(〜^\u2207^ )〜", "~(\u2267ω\u2266)~","૮ \u00b4• ﻌ \u00b4• ა", "(\u02c3ᆺ\u02c2)", "(=\ud83d\udf66 \u0f1d \ud83d\udf66=)"}; string[] emojis = { "(•‿•)", "(づ \u25d5‿\u25d5 )づ", "( \u02d8\u25bd\u02d8)っ\u2668", "=\uff3e\u25cf \u22cf \u25cf\uff3e=", "(ΦωΦ)", "(\u272a\u3268\u272a)", "( ノ・o・ )ノ", "(〜^\u2207^ )〜", "~(\u2267ω\u2266)~","૮ \u00b4• ﻌ \u00b4• ა", "(\u02c3ᆺ\u02c2)", "(=\ud83d\udf66 \u0f1d \ud83d\udf66=)"};
context.Notifications.Add(new Notification("Tranga Started", emojis[Random.Shared.Next(0, emojis.Length - 1)], NotificationUrgency.High)); context.Notifications.Add(new Notification("Tranga Started", emojis[Random.Shared.Next(0, emojis.Length - 1)], NotificationUrgency.High));
@ -143,7 +143,6 @@ using (IServiceScope scope = app.Services.CreateScope())
context.Sync(); context.Sync();
} }
TrangaSettings.Load();
Tranga.StartLogger(); Tranga.StartLogger();
Tranga.PeriodicWorkerStarterThread.Start(app.Services); Tranga.PeriodicWorkerStarterThread.Start(app.Services);

View File

@ -6,7 +6,7 @@ namespace API.Schema.LibraryContext.LibraryConnectors;
public class Kavita : LibraryConnector public class Kavita : LibraryConnector
{ {
public Kavita(string baseUrl, string auth) : base(TokenGen.CreateToken(typeof(Kavita), baseUrl), LibraryType.Kavita, baseUrl, auth) public Kavita(string baseUrl, string auth) : base(LibraryType.Kavita, baseUrl, auth)
{ {
} }

View File

@ -5,8 +5,7 @@ namespace API.Schema.LibraryContext.LibraryConnectors;
public class Komga : LibraryConnector public class Komga : LibraryConnector
{ {
public Komga(string baseUrl, string auth) : base(TokenGen.CreateToken(typeof(Komga), baseUrl), LibraryType.Komga, public Komga(string baseUrl, string auth) : base(LibraryType.Komga, baseUrl, auth)
baseUrl, auth)
{ {
} }

View File

@ -6,27 +6,52 @@ using Newtonsoft.Json;
namespace API.Schema.LibraryContext.LibraryConnectors; namespace API.Schema.LibraryContext.LibraryConnectors;
[PrimaryKey("LibraryConnectorId")] [PrimaryKey("Key")]
public abstract class LibraryConnector(string libraryConnectorId, LibraryType libraryType, string baseUrl, string auth) public abstract class LibraryConnector : Identifiable
{ {
[StringLength(64)]
[Required] [Required]
public string LibraryConnectorId { get; } = libraryConnectorId; public LibraryType LibraryType { get; init; }
[Required]
public LibraryType LibraryType { get; init; } = libraryType;
[StringLength(256)] [StringLength(256)]
[Required] [Required]
[Url] [Url]
public string BaseUrl { get; init; } = baseUrl; public string BaseUrl { get; init; }
[StringLength(256)] [StringLength(256)]
[Required] [Required]
public string Auth { get; init; } = auth; public string Auth { get; init; }
[JsonIgnore] [JsonIgnore]
[NotMapped] [NotMapped]
protected ILog Log { get; init; } = LogManager.GetLogger($"{libraryType.ToString()} {baseUrl}"); protected ILog Log { get; init; }
protected LibraryConnector(LibraryType libraryType, string baseUrl, string auth)
: base()
{
this.LibraryType = libraryType;
this.BaseUrl = baseUrl;
this.Auth = auth;
this.Log = LogManager.GetLogger(GetType());
}
/// <summary>
/// EF CORE ONLY!!!!
/// </summary>
internal LibraryConnector(string key, LibraryType libraryType, string baseUrl, string auth)
: base(key)
{
this.LibraryType = libraryType;
this.BaseUrl = baseUrl;
this.Auth = auth;
this.Log = LogManager.GetLogger(GetType());
}
public override string ToString() => $"{base.ToString()} {this.LibraryType} {this.BaseUrl}";
protected abstract void UpdateLibraryInternal(); protected abstract void UpdateLibraryInternal();
internal abstract bool Test(); internal abstract bool Test();
}
public enum LibraryType : byte
{
Komga = 0,
Kavita = 1
} }

View File

@ -1,7 +0,0 @@
namespace API.Schema.LibraryContext.LibraryConnectors;
public enum LibraryType : byte
{
Komga = 0,
Kavita = 1
}

View File

@ -108,7 +108,7 @@ public class Chapter : Identifiable, IComparable<Chapter>
private static readonly Regex ReplaceRexx = new(@"%([a-zA-Z])|(.+?)"); private static readonly Regex ReplaceRexx = new(@"%([a-zA-Z])|(.+?)");
private string GetArchiveFilePath() private string GetArchiveFilePath()
{ {
string archiveNamingScheme = TrangaSettings.chapterNamingScheme; string archiveNamingScheme = Tranga.Settings.ChapterNamingScheme;
StringBuilder stringBuilder = new(); StringBuilder stringBuilder = new();
foreach (Match nullable in NullableRex.Matches(archiveNamingScheme)) foreach (Match nullable in NullableRex.Matches(archiveNamingScheme))
{ {

View File

@ -38,22 +38,24 @@ public class MangaConnectorId<T> : Identifiable where T : Identifiable
[StringLength(256)] [Required] public string IdOnConnectorSite { get; init; } [StringLength(256)] [Required] public string IdOnConnectorSite { get; init; }
[Url] [StringLength(512)] public string? WebsiteUrl { get; internal init; } [Url] [StringLength(512)] public string? WebsiteUrl { get; internal init; }
public bool UseForDownload { get; internal set; }
private readonly ILazyLoader _lazyLoader = null!; private readonly ILazyLoader _lazyLoader = null!;
public MangaConnectorId(T obj, MangaConnector mangaConnector, string idOnConnectorSite, string? websiteUrl) public MangaConnectorId(T obj, MangaConnector mangaConnector, string idOnConnectorSite, string? websiteUrl, bool useForDownload = false)
: base(TokenGen.CreateToken(typeof(MangaConnectorId<T>), mangaConnector.Name, idOnConnectorSite)) : base(TokenGen.CreateToken(typeof(MangaConnectorId<T>), mangaConnector.Name, idOnConnectorSite))
{ {
this.Obj = obj; this.Obj = obj;
this.MangaConnector = mangaConnector; this.MangaConnector = mangaConnector;
this.IdOnConnectorSite = idOnConnectorSite; this.IdOnConnectorSite = idOnConnectorSite;
this.WebsiteUrl = websiteUrl; this.WebsiteUrl = websiteUrl;
this.UseForDownload = useForDownload;
} }
/// <summary> /// <summary>
/// EF CORE ONLY!!! /// EF CORE ONLY!!!
/// </summary> /// </summary>
public MangaConnectorId(ILazyLoader lazyLoader, string key, string objId, string mangaConnectorName, string idOnConnectorSite, string? websiteUrl) public MangaConnectorId(ILazyLoader lazyLoader, string key, string objId, string mangaConnectorName, string idOnConnectorSite, bool useForDownload, string? websiteUrl)
: base(key) : base(key)
{ {
this._lazyLoader = lazyLoader; this._lazyLoader = lazyLoader;
@ -61,6 +63,7 @@ public class MangaConnectorId<T> : Identifiable where T : Identifiable
this.MangaConnectorName = mangaConnectorName; this.MangaConnectorName = mangaConnectorName;
this.IdOnConnectorSite = idOnConnectorSite; this.IdOnConnectorSite = idOnConnectorSite;
this.WebsiteUrl = websiteUrl; this.WebsiteUrl = websiteUrl;
this.UseForDownload = useForDownload;
} }
public override string ToString() => $"{base.ToString()} {_obj}"; public override string ToString() => $"{base.ToString()} {_obj}";

View File

@ -8,7 +8,7 @@ public class MangaContext(DbContextOptions<MangaContext> options) : TrangaBaseCo
{ {
public DbSet<MangaConnector> MangaConnectors { get; set; } public DbSet<MangaConnector> MangaConnectors { get; set; }
public DbSet<Manga> Mangas { get; set; } public DbSet<Manga> Mangas { get; set; }
public DbSet<FileLibrary> LocalLibraries { get; set; } public DbSet<FileLibrary> FileLibraries { get; set; }
public DbSet<Chapter> Chapters { get; set; } public DbSet<Chapter> Chapters { get; set; }
public DbSet<Author> Authors { get; set; } public DbSet<Author> Authors { get; set; }
public DbSet<MangaTag> Tags { get; set; } public DbSet<MangaTag> Tags { get; set; }

View File

@ -10,8 +10,5 @@ public class MangaTag(string tag)
[Required] [Required]
public string Tag { get; init; } = tag; public string Tag { get; init; } = tag;
public override string ToString() public override string ToString() => Tag;
{
return $"{Tag}";
}
} }

View File

@ -19,7 +19,7 @@ public class MetadataEntry
this.Manga = manga; this.Manga = manga;
this.MangaId = manga.Key; this.MangaId = manga.Key;
this.MetadataFetcher = fetcher; this.MetadataFetcher = fetcher;
this.MetadataFetcherName = fetcher.MetadataFetcherName; this.MetadataFetcherName = fetcher.Name;
this.Identifier = identifier; this.Identifier = identifier;
} }

View File

@ -2,23 +2,23 @@ using Microsoft.EntityFrameworkCore;
namespace API.Schema.MangaContext.MetadataFetchers; namespace API.Schema.MangaContext.MetadataFetchers;
[PrimaryKey("MetadataFetcherName")] [PrimaryKey("Name")]
public abstract class MetadataFetcher public abstract class MetadataFetcher
{ {
// ReSharper disable once EntityFramework.ModelValidation.UnlimitedStringLength // ReSharper disable once EntityFramework.ModelValidation.UnlimitedStringLength
public string MetadataFetcherName { get; init; } public string Name { get; init; }
protected MetadataFetcher() protected MetadataFetcher()
{ {
this.MetadataFetcherName = this.GetType().Name; this.Name = this.GetType().Name;
} }
/// <summary> /// <summary>
/// EFCORE ONLY!!! /// EFCORE ONLY!!!
/// </summary> /// </summary>
internal MetadataFetcher(string metadataFetcherName) internal MetadataFetcher(string name)
{ {
this.MetadataFetcherName = metadataFetcherName; this.Name = name;
} }
internal MetadataEntry CreateMetadataEntry(Manga manga, string identifier) => internal MetadataEntry CreateMetadataEntry(Manga manga, string identifier) =>

View File

@ -67,7 +67,7 @@ public class MyAnimeList : MetadataFetcher
dbManga.Authors.Clear(); dbManga.Authors.Clear();
dbManga.Authors = resultData.Authors.Select(a => new Author(a.Name)).ToList(); dbManga.Authors = resultData.Authors.Select(a => new Author(a.Name)).ToList();
dbContext.SaveChanges(); dbContext.Sync();
} }
catch (DbUpdateException e) catch (DbUpdateException e)
{ {

View File

@ -19,6 +19,8 @@ public class Notification : Identifiable
[Required] [Required]
public DateTime Date { get; init; } public DateTime Date { get; init; }
public bool IsSent { get; internal set; }
public Notification(string title, string message = "", NotificationUrgency urgency = NotificationUrgency.Normal, DateTime? date = null) public Notification(string title, string message = "", NotificationUrgency urgency = NotificationUrgency.Normal, DateTime? date = null)
: base(TokenGen.CreateToken("Notification")) : base(TokenGen.CreateToken("Notification"))
@ -27,21 +29,23 @@ public class Notification : Identifiable
this.Message = message; this.Message = message;
this.Urgency = urgency; this.Urgency = urgency;
this.Date = date ?? DateTime.UtcNow; this.Date = date ?? DateTime.UtcNow;
this.IsSent = false;
} }
/// <summary> /// <summary>
/// EF ONLY!!! /// EF ONLY!!!
/// </summary> /// </summary>
public Notification(string key, string title, string message, NotificationUrgency urgency, DateTime date) public Notification(string key, string title, string message, NotificationUrgency urgency, DateTime date, bool isSent)
: base(key) : base(key)
{ {
this.Title = title; this.Title = title;
this.Message = message; this.Message = message;
this.Urgency = urgency; this.Urgency = urgency;
this.Date = date; this.Date = date;
this.IsSent = isSent;
} }
public override string ToString() => $"{base.ToString()} {Urgency} {Title}"; public override string ToString() => $"{base.ToString()} {Urgency} {Title} {Message}";
} }
public enum NotificationUrgency : byte public enum NotificationUrgency : byte

View File

@ -34,7 +34,7 @@ public class NotificationConnector(string name, string url, Dictionary<string, s
[NotMapped] [NotMapped]
private readonly HttpClient Client = new() private readonly HttpClient Client = new()
{ {
DefaultRequestHeaders = { { "User-Agent", TrangaSettings.userAgent } } DefaultRequestHeaders = { { "User-Agent", Tranga.Settings.UserAgent } }
}; };
[JsonIgnore] [JsonIgnore]
@ -79,4 +79,6 @@ public class NotificationConnector(string name, string url, Dictionary<string, s
return sb.ToString(); return sb.ToString();
} }
} }
public override string ToString() => $"{GetType().Name} {Name}";
} }

View File

@ -22,17 +22,19 @@ public abstract class TrangaBaseContext<T> : DbContext where T : DbContext
}, Array.Empty<string>(), LogLevel.Warning, DbContextLoggerOptions.Level | DbContextLoggerOptions.Category | DbContextLoggerOptions.UtcTime); }, Array.Empty<string>(), LogLevel.Warning, DbContextLoggerOptions.Level | DbContextLoggerOptions.Category | DbContextLoggerOptions.UtcTime);
} }
internal async Task<string?> Sync() internal (bool success, string? exceptionMessage) Sync()
{ {
try try
{ {
await this.SaveChangesAsync(); this.SaveChanges();
return null; return (true, null);
} }
catch (Exception e) catch (Exception e)
{ {
Log.Error(null, e); Log.Error(null, e);
return e.Message; return (false, e.Message);
} }
} }
public override string ToString() => $"{GetType().Name} {typeof(T).Name}";
} }

View File

@ -1,5 +1,10 @@
using API.Schema.MangaContext.MetadataFetchers; using System.Diagnostics.CodeAnalysis;
using API.Schema.LibraryContext;
using API.Schema.MangaContext;
using API.Schema.MangaContext.MetadataFetchers;
using API.Schema.NotificationsContext;
using API.Workers; using API.Workers;
using API.Workers.MaintenanceWorkers;
using log4net; using log4net;
using log4net.Config; using log4net.Config;
@ -20,16 +25,33 @@ public static class Tranga
public static Thread PeriodicWorkerStarterThread { get; } = new (WorkerStarter); public static Thread PeriodicWorkerStarterThread { get; } = new (WorkerStarter);
private static readonly ILog Log = LogManager.GetLogger(typeof(Tranga)); private static readonly ILog Log = LogManager.GetLogger(typeof(Tranga));
internal static readonly MetadataFetcher[] MetadataFetchers = [new MyAnimeList()]; internal static readonly MetadataFetcher[] MetadataFetchers = [new MyAnimeList()];
internal static TrangaSettings Settings = TrangaSettings.Load();
internal static readonly UpdateMetadataWorker UpdateMetadataWorker = new ();
internal static readonly SendNotificationsWorker SendNotificationsWorker = new();
internal static readonly UpdateChaptersDownloadedWorker UpdateChaptersDownloadedWorker = new();
internal static readonly CheckForNewChaptersWorker CheckForNewChaptersWorker = new();
internal static readonly CleanupMangaCoversWorker CleanupMangaCoversWorker = new();
internal static readonly StartNewChapterDownloadsWorker StartNewChapterDownloadsWorker = new();
internal static readonly RemoveOldNotificationsWorker RemoveOldNotificationsWorker = new();
internal static void StartLogger() internal static void StartLogger()
{ {
BasicConfigurator.Configure(); BasicConfigurator.Configure();
Log.Info("Logger Configured."); Log.Info("Logger Configured.");
Log.Info(TRANGA); Log.Info(TRANGA);
AddWorker(UpdateMetadataWorker);
AddWorker(SendNotificationsWorker);
AddWorker(UpdateChaptersDownloadedWorker);
AddWorker(CheckForNewChaptersWorker);
AddWorker(CleanupMangaCoversWorker);
AddWorker(StartNewChapterDownloadsWorker);
AddWorker(RemoveOldNotificationsWorker);
} }
internal static HashSet<BaseWorker> Workers { get; private set; } = new (); internal static HashSet<BaseWorker> AllWorkers { get; private set; } = new ();
public static void AddWorker(BaseWorker worker) => Workers.Add(worker); public static void AddWorker(BaseWorker worker) => AllWorkers.Add(worker);
public static void AddWorkers(IEnumerable<BaseWorker> workers) public static void AddWorkers(IEnumerable<BaseWorker> workers)
{ {
foreach (BaseWorker baseWorker in workers) foreach (BaseWorker baseWorker in workers)
@ -37,23 +59,21 @@ public static class Tranga
AddWorker(baseWorker); AddWorker(baseWorker);
} }
} }
public static void RemoveWorker(BaseWorker worker)
public static void RemoveWorker(BaseWorker removeWorker)
{ {
IEnumerable<BaseWorker> baseWorkers = Workers.Where(w => w.DependenciesAndSelf.Any(w => w == worker)); IEnumerable<BaseWorker> baseWorkers = AllWorkers.Where(w => w.DependenciesAndSelf.Any(worker => worker == removeWorker));
foreach (BaseWorker baseWorker in baseWorkers)
foreach (BaseWorker worker in baseWorkers)
{ {
baseWorker.Cancel(); StopWorker(worker);
Workers.Remove(baseWorker); AllWorkers.Remove(worker);
if (RunningWorkers.ContainsKey(baseWorker))
{
RunningWorkers[baseWorker].Abort();
RunningWorkers.Remove(baseWorker);
}
} }
} }
private static readonly Dictionary<BaseWorker, Thread> RunningWorkers = new(); private static readonly Dictionary<BaseWorker, Task<BaseWorker[]>> RunningWorkers = new();
public static BaseWorker[] GetRunningWorkers() => RunningWorkers.Keys.ToArray(); public static BaseWorker[] GetRunningWorkers() => RunningWorkers.Keys.ToArray();
private static readonly HashSet<BaseWorker> StartWorkers = new();
private static void WorkerStarter(object? serviceProviderObj) private static void WorkerStarter(object? serviceProviderObj)
{ {
Log.Info("WorkerStarter Thread running."); Log.Info("WorkerStarter Thread running.");
@ -66,9 +86,117 @@ public static class Tranga
while (true) while (true)
{ {
using IServiceScope scope = serviceProvider.CreateScope(); CheckRunningWorkers();
Thread.Sleep(TrangaSettings.workCycleTimeout); foreach (BaseWorker baseWorker in AllWorkers.DueWorkers())
StartWorkers.Add(baseWorker);
foreach (BaseWorker worker in StartWorkers.ToArray())
{
if(RunningWorkers.ContainsKey(worker))
continue;
if (worker is BaseWorkerWithContext<MangaContext> mangaContextWorker)
{
mangaContextWorker.SetScope(serviceProvider.CreateScope());
RunningWorkers.Add(mangaContextWorker, mangaContextWorker.DoWork());
}else if (worker is BaseWorkerWithContext<NotificationsContext> notificationContextWorker)
{
notificationContextWorker.SetScope(serviceProvider.CreateScope());
RunningWorkers.Add(notificationContextWorker, notificationContextWorker.DoWork());
}else if (worker is BaseWorkerWithContext<LibraryContext> libraryContextWorker)
{
libraryContextWorker.SetScope(serviceProvider.CreateScope());
RunningWorkers.Add(libraryContextWorker, libraryContextWorker.DoWork());
}else
RunningWorkers.Add(worker, worker.DoWork());
StartWorkers.Remove(worker);
}
Thread.Sleep(Settings.WorkCycleTimeoutMs);
} }
} }
private static void CheckRunningWorkers()
{
KeyValuePair<BaseWorker, Task<BaseWorker[]>>[] done = RunningWorkers.Where(kv => kv.Value.IsCompleted).ToArray();
if (done.Length < 1)
return;
Log.Info($"Done: {done.Length}");
Log.Debug(string.Join("\n", done.Select(d => d.Key.ToString())));
foreach ((BaseWorker worker, Task<BaseWorker[]> task) in done)
{
RunningWorkers.Remove(worker);
foreach (BaseWorker newWorker in task.Result)
AllWorkers.Add(newWorker);
task.Dispose();
}
}
private static IEnumerable<BaseWorker> DueWorkers(this IEnumerable<BaseWorker> workers)
{
return workers.Where(w =>
{
if (w.State is >= WorkerExecutionState.Running and < WorkerExecutionState.Completed)
return false;
if (w is IPeriodic periodicWorker)
return periodicWorker.IsDue;
return true;
});
}
internal static void MarkWorkerForStart(BaseWorker worker) => StartWorkers.Add(worker);
internal static void StopWorker(BaseWorker worker)
{
StartWorkers.Remove(worker);
worker.Cancel();
RunningWorkers.Remove(worker);
}
internal static bool AddMangaToContext((Manga, MangaConnectorId<Manga>) addManga, MangaContext context, [NotNullWhen(true)]out Manga? manga) => AddMangaToContext(addManga.Item1, addManga.Item2, context, out manga);
internal static bool AddMangaToContext(Manga addManga, MangaConnectorId<Manga> addMcId, MangaContext context, [NotNullWhen(true)]out Manga? manga)
{
manga = context.Mangas.Find(addManga.Key) ?? addManga;
MangaConnectorId<Manga> mcId = context.MangaConnectorToManga.Find(addMcId.Key) ?? addMcId;
mcId.Obj = manga;
IEnumerable<MangaTag> mergedTags = manga.MangaTags.Select(mt =>
{
MangaTag? inDb = context.Tags.Find(mt.Tag);
return inDb ?? mt;
});
manga.MangaTags = mergedTags.ToList();
IEnumerable<Author> mergedAuthors = manga.Authors.Select(ma =>
{
Author? inDb = context.Authors.Find(ma.Key);
return inDb ?? ma;
});
manga.Authors = mergedAuthors.ToList();
if(context.MangaConnectorToManga.Find(addMcId.Key) is null)
context.MangaConnectorToManga.Add(mcId);
if (context.Sync() is { success: false })
return false;
return true;
}
internal static bool AddChapterToContext((Chapter, MangaConnectorId<Chapter>) addChapter, MangaContext context,
[NotNullWhen(true)] out Chapter? chapter) => AddChapterToContext(addChapter.Item1, addChapter.Item2, context, out chapter);
internal static bool AddChapterToContext(Chapter addChapter, MangaConnectorId<Chapter> addChId, MangaContext context, [NotNullWhen(true)] out Chapter? chapter)
{
chapter = context.Chapters.Find(addChapter.Key) ?? addChapter;
MangaConnectorId<Chapter> chId = context.MangaConnectorToChapter.Find(addChId.Key) ?? addChId;
chId.Obj = chapter;
if(context.MangaConnectorToChapter.Find(chId.Key) is null)
context.MangaConnectorToChapter.Add(chId);
if (context.Sync() is { success: false })
return false;
return true;
}
} }

View File

@ -1,21 +1,25 @@
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using API.MangaDownloadClients; using API.MangaDownloadClients;
using API.Schema.NotificationsContext;
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace API; namespace API;
public static class TrangaSettings public struct TrangaSettings()
{ {
public static string downloadLocation { get; private set; } = (RuntimeInformation.IsOSPlatform(OSPlatform.Linux) ? "/Obj" : Path.Join(Directory.GetCurrentDirectory(), "Downloads"));
public static string workingDirectory { get; private set; } = Path.Join(RuntimeInformation.IsOSPlatform(OSPlatform.Linux) ? "/usr/share" : Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "tranga-api"); [JsonIgnore]
public static string workingDirectory => Path.Join(RuntimeInformation.IsOSPlatform(OSPlatform.Linux) ? "/usr/share" : Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "tranga-api");
[JsonIgnore]
public static string settingsFilePath => Path.Join(workingDirectory, "settings.json");
[JsonIgnore]
public static string coverImageCache => Path.Join(workingDirectory, "imageCache");
public string DownloadLocation => RuntimeInformation.IsOSPlatform(OSPlatform.Linux) ? "/Manga" : Path.Join(Directory.GetCurrentDirectory(), "Manga");
[JsonIgnore] [JsonIgnore]
internal static readonly string DefaultUserAgent = $"Tranga/2.0 ({Enum.GetName(Environment.OSVersion.Platform)}; {(Environment.Is64BitOperatingSystem ? "x64" : "")})"; internal static readonly string DefaultUserAgent = $"Tranga/2.0 ({Enum.GetName(Environment.OSVersion.Platform)}; {(Environment.Is64BitOperatingSystem ? "x64" : "")})";
public static string userAgent { get; private set; } = DefaultUserAgent; public string UserAgent { get; set; } = DefaultUserAgent;
public static int compression{ get; private set; } = 40; public int ImageCompression{ get; set; } = 40;
public static bool bwImages { get; private set; } = false; public bool BlackWhiteImages { get; set; } = false;
public static string flareSolverrUrl { get; private set; } = string.Empty; public string FlareSolverrUrl { get; set; } = string.Empty;
/// <summary> /// <summary>
/// Placeholders: /// Placeholders:
/// %M Obj Name /// %M Obj Name
@ -30,13 +34,8 @@ public static class TrangaSettings
/// ?_(...) replace _ with a value from above: /// ?_(...) replace _ with a value from above:
/// Everything inside the braces will only be added if the value of %_ is not null /// Everything inside the braces will only be added if the value of %_ is not null
/// </summary> /// </summary>
public static string chapterNamingScheme { get; private set; } = "%M - ?V(Vol.%V )Ch.%C?T( - %T)"; public string ChapterNamingScheme { get; set; } = "%M - ?V(Vol.%V )Ch.%C?T( - %T)";
[JsonIgnore] public int WorkCycleTimeoutMs { get; set; } = 20000;
public static string settingsFilePath => Path.Join(workingDirectory, "settings.json");
[JsonIgnore]
public static string coverImageCache => Path.Join(workingDirectory, "imageCache");
public static bool aprilFoolsMode { get; private set; } = true;
public static int workCycleTimeout { get; private set; } = 20000;
[JsonIgnore] [JsonIgnore]
internal static readonly Dictionary<RequestType, int> DefaultRequestLimits = new () internal static readonly Dictionary<RequestType, int> DefaultRequestLimits = new ()
{ {
@ -47,142 +46,67 @@ public static class TrangaSettings
{RequestType.MangaCover, 60}, {RequestType.MangaCover, 60},
{RequestType.Default, 60} {RequestType.Default, 60}
}; };
public static Dictionary<RequestType, int> requestLimits { get; private set; } = DefaultRequestLimits; public Dictionary<RequestType, int> RequestLimits { get; set; } = DefaultRequestLimits;
public static TimeSpan NotificationUrgencyDelay(NotificationUrgency urgency) => urgency switch public string DownloadLanguage { get; set; } = "en";
public static TrangaSettings Load()
{ {
NotificationUrgency.High => TimeSpan.Zero, if (!File.Exists(settingsFilePath))
NotificationUrgency.Normal => TimeSpan.FromMinutes(5), new TrangaSettings().Save();
NotificationUrgency.Low => TimeSpan.FromMinutes(10), return JsonConvert.DeserializeObject<TrangaSettings>(File.ReadAllText(settingsFilePath));
_ => TimeSpan.FromHours(1)
}; //TODO make this a setting?
public static void Load()
{
if(File.Exists(settingsFilePath))
Deserialize(File.ReadAllText(settingsFilePath));
else return;
Directory.CreateDirectory(downloadLocation);
ExportSettings();
} }
public static void UpdateAprilFoolsMode(bool enabled) public void Save()
{ {
aprilFoolsMode = enabled; File.WriteAllText(settingsFilePath, JsonConvert.SerializeObject(this));
ExportSettings();
} }
public static void UpdateCompressImages(int value) public void SetUserAgent(string value)
{ {
compression = int.Clamp(value, 1, 100); this.UserAgent = value;
ExportSettings(); Save();
} }
public static void UpdateBwImages(bool enabled) public void SetRequestLimit(RequestType type, int value)
{ {
bwImages = enabled; this.RequestLimits[type] = value;
ExportSettings(); Save();
} }
public static void UpdateUserAgent(string? customUserAgent) public void ResetRequestLimits()
{ {
userAgent = customUserAgent ?? DefaultUserAgent; this.RequestLimits = DefaultRequestLimits;
ExportSettings(); Save();
} }
public static void UpdateRequestLimit(RequestType requestType, int newLimit) public void UpdateImageCompression(int value)
{ {
requestLimits[requestType] = newLimit; this.ImageCompression = value;
ExportSettings(); Save();
} }
public static void UpdateChapterNamingScheme(string namingScheme) public void SetBlackWhiteImageEnabled(bool enabled)
{ {
chapterNamingScheme = namingScheme; this.BlackWhiteImages = enabled;
ExportSettings(); Save();
} }
public static void UpdateFlareSolverrUrl(string url) public void SetChapterNamingScheme(string scheme)
{ {
flareSolverrUrl = url; this.ChapterNamingScheme = scheme;
ExportSettings(); Save();
} }
public static void ResetRequestLimits() public void SetFlareSolverrUrl(string url)
{ {
requestLimits = DefaultRequestLimits; this.FlareSolverrUrl = url;
ExportSettings(); Save();
} }
public static void ExportSettings() public void SetDownloadLanguage(string language)
{ {
if (File.Exists(settingsFilePath)) this.DownloadLanguage = language;
{ Save();
while(IsFileInUse(settingsFilePath))
Thread.Sleep(100);
}
else
Directory.CreateDirectory(new FileInfo(settingsFilePath).DirectoryName!);
File.WriteAllText(settingsFilePath, Serialize());
}
internal static bool IsFileInUse(string filePath)
{
if (!File.Exists(filePath))
return false;
try
{
using FileStream stream = new (filePath, FileMode.Open, FileAccess.Read, FileShare.None);
stream.Close();
return false;
}
catch (IOException)
{
return true;
}
}
public static JObject AsJObject()
{
JObject jobj = new ();
jobj.Add("downloadLocation", JToken.FromObject(downloadLocation));
jobj.Add("workingDirectory", JToken.FromObject(workingDirectory));
jobj.Add("userAgent", JToken.FromObject(userAgent));
jobj.Add("aprilFoolsMode", JToken.FromObject(aprilFoolsMode));
jobj.Add("requestLimits", JToken.FromObject(requestLimits));
jobj.Add("compression", JToken.FromObject(compression));
jobj.Add("bwImages", JToken.FromObject(bwImages));
jobj.Add("workCycleTimeout", JToken.FromObject(workCycleTimeout));
jobj.Add("chapterNamingScheme", JToken.FromObject(chapterNamingScheme));
jobj.Add("flareSolverrUrl", JToken.FromObject(flareSolverrUrl));
return jobj;
}
public static string Serialize() => AsJObject().ToString();
public static void Deserialize(string serialized)
{
JObject jobj = JObject.Parse(serialized);
if (jobj.TryGetValue("downloadLocation", out JToken? dl))
downloadLocation = dl.Value<string>()!;
if (jobj.TryGetValue("workingDirectory", out JToken? wd))
workingDirectory = wd.Value<string>()!;
if (jobj.TryGetValue("userAgent", out JToken? ua))
userAgent = ua.Value<string>()!;
if (jobj.TryGetValue("aprilFoolsMode", out JToken? afm))
aprilFoolsMode = afm.Value<bool>()!;
if (jobj.TryGetValue("requestLimits", out JToken? rl))
requestLimits = rl.ToObject<Dictionary<RequestType, int>>()!;
if (jobj.TryGetValue("compression", out JToken? ci))
compression = ci.Value<int>()!;
if (jobj.TryGetValue("bwImages", out JToken? bwi))
bwImages = bwi.Value<bool>()!;
if (jobj.TryGetValue("workCycleTimeout", out JToken? snjt))
workCycleTimeout = snjt.Value<int>()!;
if (jobj.TryGetValue("chapterNamingScheme", out JToken? cns))
chapterNamingScheme = cns.Value<string>()!;
if (jobj.TryGetValue("flareSolverrUrl", out JToken? fsu))
flareSolverrUrl = fsu.Value<string>()!;
} }
} }

View File

@ -5,16 +5,46 @@ namespace API.Workers;
public abstract class BaseWorker : Identifiable public abstract class BaseWorker : Identifiable
{ {
/// <summary>
/// Workers this Worker depends on being completed before running.
/// </summary>
public BaseWorker[] DependsOn { get; init; } public BaseWorker[] DependsOn { get; init; }
/// <summary>
/// Dependencies and dependencies of dependencies. See also <see cref="DependsOn"/>.
/// </summary>
public IEnumerable<BaseWorker> AllDependencies => DependsOn.Select(d => d.AllDependencies).SelectMany(x => x); public IEnumerable<BaseWorker> AllDependencies => DependsOn.Select(d => d.AllDependencies).SelectMany(x => x);
/// <summary>
/// <see cref="AllDependencies"/> and Self.
/// </summary>
public IEnumerable<BaseWorker> DependenciesAndSelf => AllDependencies.Append(this); public IEnumerable<BaseWorker> DependenciesAndSelf => AllDependencies.Append(this);
/// <summary>
/// <see cref="DependsOn"/> where <see cref="WorkerExecutionState"/> is less than Completed.
/// </summary>
public IEnumerable<BaseWorker> MissingDependencies => DependsOn.Where(d => d.State < WorkerExecutionState.Completed); public IEnumerable<BaseWorker> MissingDependencies => DependsOn.Where(d => d.State < WorkerExecutionState.Completed);
public bool DependenciesFulfilled => DependsOn.All(d => d.State >= WorkerExecutionState.Completed); public bool AllDependenciesFulfilled => DependsOn.All(d => d.State >= WorkerExecutionState.Completed);
internal WorkerExecutionState State { get; set; } internal WorkerExecutionState State { get; private set; }
private static readonly CancellationTokenSource CancellationTokenSource = new(TimeSpan.FromMinutes(10)); private static readonly CancellationTokenSource CancellationTokenSource = new(TimeSpan.FromMinutes(10));
protected ILog Log { get; init; } protected ILog Log { get; init; }
public void Cancel() => CancellationTokenSource.Cancel();
protected void Fail() => this.State = WorkerExecutionState.Failed; /// <summary>
/// Stops worker, and marks as <see cref="WorkerExecutionState"/>.Cancelled
/// </summary>
public void Cancel()
{
Log.Debug($"Cancelled {this}");
this.State = WorkerExecutionState.Cancelled;
CancellationTokenSource.Cancel();
}
/// <summary>
/// Stops worker, and marks as <see cref="WorkerExecutionState"/>.Failed
/// </summary>
protected void Fail()
{
Log.Debug($"Failed {this}");
this.State = WorkerExecutionState.Failed;
CancellationTokenSource.Cancel();
}
public BaseWorker(IEnumerable<BaseWorker>? dependsOn = null) public BaseWorker(IEnumerable<BaseWorker>? dependsOn = null)
{ {
@ -22,8 +52,25 @@ public abstract class BaseWorker : Identifiable
this.Log = LogManager.GetLogger(GetType()); this.Log = LogManager.GetLogger(GetType());
} }
/// <summary>
/// Sets States during worker-run.
/// States:
/// <list type="bullet">
/// <item><see cref="WorkerExecutionState"/>.Waiting when waiting for <see cref="MissingDependencies"/></item>
/// <item><see cref="WorkerExecutionState"/>.Running when running</item>
/// <item><see cref="WorkerExecutionState"/>.Completed after finished</item>
/// </list>
/// </summary>
/// <returns>
/// <list type="bullet">
/// <item>If <see cref="BaseWorker"/> has <see cref="MissingDependencies"/>, missing dependencies.</item>
/// <item>If <see cref="MissingDependencies"/> are <see cref="WorkerExecutionState"/>.Running, itself after waiting for dependencies.</item>
/// <item>If <see cref="BaseWorker"/> has run, additional <see cref="BaseWorker"/>.</item>
/// </list>
/// </returns>
public Task<BaseWorker[]> DoWork() public Task<BaseWorker[]> DoWork()
{ {
Log.Debug($"Checking {this}");
this.State = WorkerExecutionState.Waiting; this.State = WorkerExecutionState.Waiting;
BaseWorker[] missingDependenciesThatNeedStarting = MissingDependencies.Where(d => d.State < WorkerExecutionState.Waiting).ToArray(); BaseWorker[] missingDependenciesThatNeedStarting = MissingDependencies.Where(d => d.State < WorkerExecutionState.Waiting).ToArray();
@ -33,8 +80,17 @@ public abstract class BaseWorker : Identifiable
if (MissingDependencies.Any()) if (MissingDependencies.Any())
return new Task<BaseWorker[]>(WaitForDependencies); return new Task<BaseWorker[]>(WaitForDependencies);
Log.Info($"Running {this}");
DateTime startTime = DateTime.UtcNow;
Task<BaseWorker[]> task = new (DoWorkInternal, CancellationTokenSource.Token); Task<BaseWorker[]> task = new (DoWorkInternal, CancellationTokenSource.Token);
task.GetAwaiter().OnCompleted(() => this.State = WorkerExecutionState.Completed); task.GetAwaiter().OnCompleted(() =>
{
DateTime endTime = DateTime.UtcNow;
Log.Info($"Completed {this}\n\t{endTime.Subtract(startTime).TotalMilliseconds} ms");
this.State = WorkerExecutionState.Completed;
if(this is IPeriodic periodic)
periodic.LastExecution = DateTime.UtcNow;
});
task.Start(); task.Start();
this.State = WorkerExecutionState.Running; this.State = WorkerExecutionState.Running;
return task; return task;
@ -44,9 +100,10 @@ public abstract class BaseWorker : Identifiable
private BaseWorker[] WaitForDependencies() private BaseWorker[] WaitForDependencies()
{ {
Log.Info($"Waiting for {MissingDependencies.Count()} Dependencies {this}:\n\t{string.Join("\n\t", MissingDependencies.Select(d => d.ToString()))}");
while (CancellationTokenSource.IsCancellationRequested == false && MissingDependencies.Any()) while (CancellationTokenSource.IsCancellationRequested == false && MissingDependencies.Any())
{ {
Thread.Sleep(TrangaSettings.workCycleTimeout); Thread.Sleep(Tranga.Settings.WorkCycleTimeoutMs);
} }
return [this]; return [this];
} }
@ -55,6 +112,7 @@ public abstract class BaseWorker : Identifiable
public enum WorkerExecutionState public enum WorkerExecutionState
{ {
Failed = 0, Failed = 0,
Cancelled = 32,
Created = 64, Created = 64,
Waiting = 96, Waiting = 96,
Running = 128, Running = 128,

View File

@ -1,8 +1,24 @@
using System.Configuration;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
namespace API.Workers; namespace API.Workers;
public abstract class BaseWorkerWithContext<T>(IServiceScope scope, IEnumerable<BaseWorker>? dependsOn = null) : BaseWorker(dependsOn) where T : DbContext public abstract class BaseWorkerWithContext<T>(IEnumerable<BaseWorker>? dependsOn = null) : BaseWorker(dependsOn) where T : DbContext
{ {
protected T DbContext { get; init; } = scope.ServiceProvider.GetRequiredService<T>(); protected T DbContext = null!;
private IServiceScope? _scope;
public void SetScope(IServiceScope scope)
{
this._scope = scope;
this.DbContext = scope.ServiceProvider.GetRequiredService<T>();
}
/// <exception cref="ConfigurationErrorsException">Scope has not been set. <see cref="SetScope"/></exception>
public new Task<BaseWorker[]> DoWork()
{
if (DbContext is null)
throw new ConfigurationErrorsException("Scope has not been set.");
return base.DoWork();
}
} }

View File

@ -1,9 +1,9 @@
namespace API.Workers; namespace API.Workers;
public interface IPeriodic<T> where T : BaseWorker public interface IPeriodic
{ {
protected DateTime LastExecution { get; set; } internal DateTime LastExecution { get; set; }
protected TimeSpan Interval { get; set; } public TimeSpan Interval { get; set; }
public DateTime NextExecution => LastExecution.Add(Interval); public DateTime NextExecution => LastExecution.Add(Interval);
public bool IsDue => NextExecution <= DateTime.UtcNow;
} }

View File

@ -1,28 +0,0 @@
using API.Schema.MangaContext;
using Microsoft.EntityFrameworkCore;
namespace API.Workers;
public class UpdateChaptersDownloadedWorker(Manga manga, IServiceScope scope, IEnumerable<BaseWorker>? dependsOn = null)
: BaseWorkerWithContext<MangaContext>(scope, dependsOn), IPeriodic<UpdateChaptersDownloadedWorker>
{
public DateTime LastExecution { get; set; } = DateTime.UtcNow;
public TimeSpan Interval { get; set; } = TimeSpan.FromMinutes(60);
protected override BaseWorker[] DoWorkInternal()
{
foreach (Chapter mangaChapter in manga.Chapters)
{
mangaChapter.Downloaded = mangaChapter.CheckDownloaded();
}
try
{
DbContext.SaveChanges();
}
catch (DbUpdateException e)
{
Log.Error(e);
}
return [];
}
}

View File

@ -2,6 +2,7 @@ using System.IO.Compression;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using API.MangaDownloadClients; using API.MangaDownloadClients;
using API.Schema.MangaContext; using API.Schema.MangaContext;
using API.Schema.MangaContext.MangaConnectors;
using SixLabors.ImageSharp; using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Formats.Jpeg; using SixLabors.ImageSharp.Formats.Jpeg;
using SixLabors.ImageSharp.Processing; using SixLabors.ImageSharp.Processing;
@ -10,21 +11,23 @@ using static System.IO.UnixFileMode;
namespace API.Workers; namespace API.Workers;
public class DownloadChapterFromMangaconnectorWorker(Chapter chapter, IServiceScope scope, IEnumerable<BaseWorker>? dependsOn = null) public class DownloadChapterFromMangaconnectorWorker(MangaConnectorId<Chapter> chId, IEnumerable<BaseWorker>? dependsOn = null)
: BaseWorkerWithContext<MangaContext>(scope, dependsOn) : BaseWorkerWithContext<MangaContext>(dependsOn)
{ {
internal readonly string MangaConnectorIdId = chId.Key;
protected override BaseWorker[] DoWorkInternal() protected override BaseWorker[] DoWorkInternal()
{ {
if (DbContext.MangaConnectorToChapter.Find(MangaConnectorIdId) is not { } MangaConnectorId)
return []; //TODO Exception?
MangaConnector mangaConnector = MangaConnectorId.MangaConnector;
Chapter chapter = MangaConnectorId.Obj;
if (chapter.Downloaded) if (chapter.Downloaded)
{ {
Log.Info("Chapter was already downloaded."); Log.Info("Chapter was already downloaded.");
return []; return [];
} }
//TODO MangaConnector Selection string[] imageUrls = mangaConnector.GetChapterImageUrls(MangaConnectorId);
MangaConnectorId<Chapter> mcId = chapter.MangaConnectorIds.First();
string[] imageUrls = mcId.MangaConnector.GetChapterImageUrls(mcId);
if (imageUrls.Length < 1) if (imageUrls.Length < 1)
{ {
Log.Info($"No imageUrls for chapter {chapter}"); Log.Info($"No imageUrls for chapter {chapter}");
@ -89,14 +92,14 @@ public class DownloadChapterFromMangaconnectorWorker(Chapter chapter, IServiceSc
Directory.Delete(tempFolder, true); //Cleanup Directory.Delete(tempFolder, true); //Cleanup
chapter.Downloaded = true; chapter.Downloaded = true;
DbContext.SaveChanges(); DbContext.Sync();
return []; return [];
} }
private void ProcessImage(string imagePath) private void ProcessImage(string imagePath)
{ {
if (!TrangaSettings.bwImages && TrangaSettings.compression == 100) if (!Tranga.Settings.BlackWhiteImages && Tranga.Settings.ImageCompression == 100)
{ {
Log.Debug("No processing requested for image"); Log.Debug("No processing requested for image");
return; return;
@ -107,12 +110,12 @@ public class DownloadChapterFromMangaconnectorWorker(Chapter chapter, IServiceSc
try try
{ {
using Image image = Image.Load(imagePath); using Image image = Image.Load(imagePath);
if (TrangaSettings.bwImages) if (Tranga.Settings.BlackWhiteImages)
image.Mutate(i => i.ApplyProcessor(new AdaptiveThresholdProcessor())); image.Mutate(i => i.ApplyProcessor(new AdaptiveThresholdProcessor()));
File.Delete(imagePath); File.Delete(imagePath);
image.SaveAsJpeg(imagePath, new JpegEncoder() image.SaveAsJpeg(imagePath, new JpegEncoder()
{ {
Quality = TrangaSettings.compression Quality = Tranga.Settings.ImageCompression
}); });
} }
catch (Exception e) catch (Exception e)
@ -160,21 +163,23 @@ public class DownloadChapterFromMangaconnectorWorker(Chapter chapter, IServiceSc
File.SetUnixFileMode(newFilePath, GroupRead | GroupWrite | UserRead | UserWrite | OtherRead | OtherWrite); File.SetUnixFileMode(newFilePath, GroupRead | GroupWrite | UserRead | UserWrite | OtherRead | OtherWrite);
Log.Debug($"Copied cover from {fileInCache} to {newFilePath}"); Log.Debug($"Copied cover from {fileInCache} to {newFilePath}");
} }
private bool DownloadImage(string imageUrl, string savePath) private bool DownloadImage(string imageUrl, string savePath)
{ {
HttpDownloadClient downloadClient = new(); HttpDownloadClient downloadClient = new();
RequestResult requestResult = downloadClient.MakeRequest(imageUrl, RequestType.MangaImage); RequestResult requestResult = downloadClient.MakeRequest(imageUrl, RequestType.MangaImage);
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300) if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
return false; return false;
if (requestResult.result == Stream.Null) if (requestResult.result == Stream.Null)
return false; return false;
FileStream fs = new (savePath, FileMode.Create, FileAccess.Write, FileShare.None); FileStream fs = new(savePath, FileMode.Create, FileAccess.Write, FileShare.None);
requestResult.result.CopyTo(fs); requestResult.result.CopyTo(fs);
fs.Close(); fs.Close();
ProcessImage(savePath); ProcessImage(savePath);
return true; return true;
} }
public override string ToString() => $"{base.ToString()} {MangaConnectorIdId}";
} }

View File

@ -1,26 +1,24 @@
using API.Schema.MangaContext; using API.Schema.MangaContext;
using API.Schema.MangaContext.MangaConnectors; using API.Schema.MangaContext.MangaConnectors;
using Microsoft.EntityFrameworkCore;
namespace API.Workers; namespace API.Workers;
public class DownloadCoverFromMangaconnectorWorker(MangaConnectorId<Manga> mcId, IServiceScope scope, IEnumerable<BaseWorker>? dependsOn = null) public class DownloadCoverFromMangaconnectorWorker(MangaConnectorId<Manga> mcId, IEnumerable<BaseWorker>? dependsOn = null)
: BaseWorkerWithContext<MangaContext>(scope, dependsOn) : BaseWorkerWithContext<MangaContext>(dependsOn)
{ {
public MangaConnectorId<Manga> MangaConnectorId { get; init; } = mcId; internal readonly string MangaConnectorIdId = mcId.Key;
protected override BaseWorker[] DoWorkInternal() protected override BaseWorker[] DoWorkInternal()
{ {
if (DbContext.MangaConnectorToManga.Find(MangaConnectorIdId) is not { } MangaConnectorId)
return []; //TODO Exception?
MangaConnector mangaConnector = MangaConnectorId.MangaConnector; MangaConnector mangaConnector = MangaConnectorId.MangaConnector;
Manga manga = MangaConnectorId.Obj; Manga manga = MangaConnectorId.Obj;
try
{ manga.CoverFileNameInCache = mangaConnector.SaveCoverImageToCache(MangaConnectorId);
manga.CoverFileNameInCache = mangaConnector.SaveCoverImageToCache(MangaConnectorId);
DbContext.SaveChanges(); DbContext.Sync();
}
catch (DbUpdateException e)
{
Log.Error(e);
}
return []; return [];
} }
public override string ToString() => $"{base.ToString()} {MangaConnectorIdId}";
} }

View File

@ -1,39 +1,35 @@
using API.Schema.MangaContext; using API.Schema.MangaContext;
using API.Schema.MangaContext.MangaConnectors; using API.Schema.MangaContext.MangaConnectors;
using Microsoft.EntityFrameworkCore;
namespace API.Workers; namespace API.Workers;
public class RetrieveMangaChaptersFromMangaconnectorWorker(MangaConnectorId<Manga> mcId, string language, IServiceScope scope, IEnumerable<BaseWorker>? dependsOn = null) public class RetrieveMangaChaptersFromMangaconnectorWorker(MangaConnectorId<Manga> mcId, string language, IEnumerable<BaseWorker>? dependsOn = null)
: BaseWorkerWithContext<MangaContext>(scope, dependsOn) : BaseWorkerWithContext<MangaContext>(dependsOn)
{ {
public MangaConnectorId<Manga> MangaConnectorId { get; init; } = mcId; internal readonly string MangaConnectorIdId = mcId.Key;
protected override BaseWorker[] DoWorkInternal() protected override BaseWorker[] DoWorkInternal()
{ {
if (DbContext.MangaConnectorToManga.Find(MangaConnectorIdId) is not { } MangaConnectorId)
return []; //TODO Exception?
MangaConnector mangaConnector = MangaConnectorId.MangaConnector; MangaConnector mangaConnector = MangaConnectorId.MangaConnector;
Manga manga = MangaConnectorId.Obj; Manga manga = MangaConnectorId.Obj;
// This gets all chapters that are not downloaded // This gets all chapters that are not downloaded
(Chapter, MangaConnectorId<Chapter>)[] allChapters = (Chapter, MangaConnectorId<Chapter>)[] allChapters =
mangaConnector.GetChapters(MangaConnectorId, language).DistinctBy(c => c.Item1.Key).ToArray(); mangaConnector.GetChapters(MangaConnectorId, language).DistinctBy(c => c.Item1.Key).ToArray();
(Chapter, MangaConnectorId<Chapter>)[] newChapters = allChapters.Where(chapter =>
manga.Chapters.Any(ch => chapter.Item1.Key == ch.Key && ch.Downloaded) == false).ToArray();
Log.Info($"{manga.Chapters.Count} existing + {newChapters.Length} new chapters.");
try int addedChapters = 0;
foreach ((Chapter chapter, MangaConnectorId<Chapter> mcId) newChapter in allChapters)
{ {
foreach ((Chapter chapter, MangaConnectorId<Chapter> mcId) newChapter in newChapters) if (Tranga.AddChapterToContext(newChapter, DbContext, out Chapter? addedChapter) == false)
{ continue;
manga.Chapters.Add(newChapter.chapter); manga.Chapters.Add(addedChapter);
DbContext.MangaConnectorToChapter.Add(newChapter.mcId); }
} Log.Info($"{manga.Chapters.Count} existing + {addedChapters} new chapters.");
DbContext.SaveChanges(); DbContext.Sync();
}
catch (DbUpdateException e)
{
Log.Error(e);
}
return []; return [];
} }
public override string ToString() => $"{base.ToString()} {MangaConnectorIdId}";
} }

View File

@ -44,4 +44,6 @@ public class MoveFileOrFolderWorker(string toLocation, string fromLocation, IEnu
{ {
File.Move(from.FullName, toLocation); File.Move(from.FullName, toLocation);
} }
public override string ToString() => $"{base.ToString()} {FromLocation} {ToLocation}";
} }

View File

@ -1,25 +1,26 @@
using API.Schema.MangaContext; using API.Schema.MangaContext;
using Microsoft.EntityFrameworkCore;
namespace API.Workers; namespace API.Workers;
public class MoveMangaLibraryWorker(Manga manga, FileLibrary toLibrary, IServiceScope scope, IEnumerable<BaseWorker>? dependsOn = null) public class MoveMangaLibraryWorker(Manga manga, FileLibrary toLibrary, IEnumerable<BaseWorker>? dependsOn = null)
: BaseWorkerWithContext<MangaContext>(scope, dependsOn) : BaseWorkerWithContext<MangaContext>(dependsOn)
{ {
internal readonly string MangaId = manga.Key;
internal readonly string LibraryId = toLibrary.Key;
protected override BaseWorker[] DoWorkInternal() protected override BaseWorker[] DoWorkInternal()
{ {
if (DbContext.Mangas.Find(MangaId) is not { } manga)
return []; //TODO Exception?
if (DbContext.FileLibraries.Find(LibraryId) is not { } toLibrary)
return []; //TODO Exception?
Dictionary<Chapter, string> oldPath = manga.Chapters.ToDictionary(c => c, c => c.FullArchiveFilePath); Dictionary<Chapter, string> oldPath = manga.Chapters.ToDictionary(c => c, c => c.FullArchiveFilePath);
manga.Library = toLibrary; manga.Library = toLibrary;
try
{ if (DbContext.Sync() is { success: false })
DbContext.SaveChanges();
}
catch (DbUpdateException e)
{
Log.Error(e);
return []; return [];
}
return manga.Chapters.Select(c => new MoveFileOrFolderWorker(c.FullArchiveFilePath, oldPath[c])).ToArray<BaseWorker>(); return manga.Chapters.Select(c => new MoveFileOrFolderWorker(c.FullArchiveFilePath, oldPath[c])).ToArray<BaseWorker>();
} }
public override string ToString() => $"{base.ToString()} {MangaId} {LibraryId}";
} }

View File

@ -0,0 +1,22 @@
using API.Schema.MangaContext;
namespace API.Workers;
public class CheckForNewChaptersWorker(TimeSpan? interval = null, IEnumerable<BaseWorker>? dependsOn = null)
: BaseWorkerWithContext<MangaContext>(dependsOn), IPeriodic
{
public DateTime LastExecution { get; set; } = DateTime.UnixEpoch;
public TimeSpan Interval { get; set; } = interval??TimeSpan.FromMinutes(60);
protected override BaseWorker[] DoWorkInternal()
{
IQueryable<MangaConnectorId<Manga>> connectorIdsManga = DbContext.MangaConnectorToManga.Where(id => id.UseForDownload);
List<BaseWorker> newWorkers = new();
foreach (MangaConnectorId<Manga> mangaConnectorId in connectorIdsManga)
newWorkers.Add(new RetrieveMangaChaptersFromMangaconnectorWorker(mangaConnectorId, Tranga.Settings.DownloadLanguage));
return newWorkers.ToArray();
}
}

View File

@ -2,10 +2,11 @@ using API.Schema.MangaContext;
namespace API.Workers.MaintenanceWorkers; namespace API.Workers.MaintenanceWorkers;
public class CleanupMangaCoversWorker(IServiceScope scope, IEnumerable<BaseWorker>? dependsOn = null) : BaseWorkerWithContext<MangaContext>(scope, dependsOn), IPeriodic<CleanupMangaCoversWorker> public class CleanupMangaCoversWorker(TimeSpan? interval = null, IEnumerable<BaseWorker>? dependsOn = null)
: BaseWorkerWithContext<MangaContext>(dependsOn), IPeriodic
{ {
public DateTime LastExecution { get; set; } = DateTime.UtcNow; public DateTime LastExecution { get; set; } = DateTime.UnixEpoch;
public TimeSpan Interval { get; set; } = TimeSpan.FromMinutes(60); public TimeSpan Interval { get; set; } = interval ?? TimeSpan.FromHours(24);
protected override BaseWorker[] DoWorkInternal() protected override BaseWorker[] DoWorkInternal()
{ {

View File

@ -0,0 +1,19 @@
using API.Schema.NotificationsContext;
namespace API.Workers.MaintenanceWorkers;
public class RemoveOldNotificationsWorker(TimeSpan? interval = null, IEnumerable<BaseWorker>? dependsOn = null)
: BaseWorkerWithContext<NotificationsContext>(dependsOn), IPeriodic
{
public DateTime LastExecution { get; set; } = DateTime.UnixEpoch;
public TimeSpan Interval { get; set; } = interval ?? TimeSpan.FromHours(1);
protected override BaseWorker[] DoWorkInternal()
{
IQueryable<Notification> toRemove = DbContext.Notifications.Where(n => n.IsSent || DateTime.UtcNow - n.Date > Interval);
DbContext.RemoveRange(toRemove);
DbContext.Sync();
return [];
}
}

View File

@ -0,0 +1,29 @@
using API.Schema.NotificationsContext;
using API.Schema.NotificationsContext.NotificationConnectors;
namespace API.Workers;
public class SendNotificationsWorker(TimeSpan? interval = null, IEnumerable<BaseWorker>? dependsOn = null)
: BaseWorkerWithContext<NotificationsContext>(dependsOn), IPeriodic
{
public DateTime LastExecution { get; set; } = DateTime.UnixEpoch;
public TimeSpan Interval { get; set; } = interval??TimeSpan.FromMinutes(1);
protected override BaseWorker[] DoWorkInternal()
{
NotificationConnector[] connectors = DbContext.NotificationConnectors.ToArray();
Notification[] notifications = DbContext.Notifications.Where(n => n.IsSent == false).ToArray();
foreach (Notification notification in notifications)
{
foreach (NotificationConnector connector in connectors)
{
connector.SendNotification(notification.Title, notification.Message);
notification.IsSent = true;
}
}
DbContext.Sync();
return [];
}
}

View File

@ -0,0 +1,21 @@
using API.Schema.MangaContext;
namespace API.Workers;
public class StartNewChapterDownloadsWorker(TimeSpan? interval = null, IEnumerable<BaseWorker>? dependsOn = null)
: BaseWorkerWithContext<MangaContext>(dependsOn), IPeriodic
{
public DateTime LastExecution { get; set; } = DateTime.UnixEpoch;
public TimeSpan Interval { get; set; } = interval ?? TimeSpan.FromMinutes(1);
protected override BaseWorker[] DoWorkInternal()
{
IQueryable<MangaConnectorId<Chapter>> mangaConnectorIds = DbContext.MangaConnectorToChapter.Where(id => id.Obj.Downloaded == false && id.UseForDownload);
List<BaseWorker> newWorkers = new();
foreach (MangaConnectorId<Chapter> mangaConnectorId in mangaConnectorIds)
newWorkers.Add(new DownloadChapterFromMangaconnectorWorker(mangaConnectorId));
return newWorkers.ToArray();
}
}

View File

@ -0,0 +1,17 @@
using API.Schema.MangaContext;
namespace API.Workers;
public class UpdateChaptersDownloadedWorker(TimeSpan? interval = null, IEnumerable<BaseWorker>? dependsOn = null)
: BaseWorkerWithContext<MangaContext>(dependsOn), IPeriodic
{
public DateTime LastExecution { get; set; } = DateTime.UnixEpoch;
public TimeSpan Interval { get; set; } = interval??TimeSpan.FromMinutes(60);
protected override BaseWorker[] DoWorkInternal()
{
foreach (Chapter dbContextChapter in DbContext.Chapters)
dbContextChapter.Downloaded = dbContextChapter.CheckDownloaded();
DbContext.Sync();
return [];
}
}

View File

@ -0,0 +1,31 @@
using API.Schema.MangaContext;
using API.Schema.MangaContext.MetadataFetchers;
using Microsoft.EntityFrameworkCore;
namespace API.Workers;
public class UpdateMetadataWorker(TimeSpan? interval = null, IEnumerable<BaseWorker>? dependsOn = null)
: BaseWorkerWithContext<MangaContext>(dependsOn), IPeriodic
{
public DateTime LastExecution { get; set; } = DateTime.UnixEpoch;
public TimeSpan Interval { get; set; } = interval ?? TimeSpan.FromHours(12);
protected override BaseWorker[] DoWorkInternal()
{
IQueryable<string> mangaIds = DbContext.MangaConnectorToManga
.Where(m => m.UseForDownload)
.Select(m => m.ObjId);
IQueryable<MetadataEntry> metadataEntriesToUpdate = DbContext.MetadataEntries
.Include(e => e.MetadataFetcher)
.Where(e =>
mangaIds.Any(id => id == e.MangaId));
foreach (MetadataEntry metadataEntry in metadataEntriesToUpdate)
metadataEntry.MetadataFetcher.UpdateMetadata(metadataEntry, DbContext);
DbContext.Sync();
return [];
}
}

View File

@ -1,15 +0,0 @@
using API.Schema.NotificationsContext;
namespace API.Workers;
public class SendNotificationsWorker(IServiceScope scope, IEnumerable<BaseWorker>? dependsOn = null)
: BaseWorkerWithContext<NotificationsContext>(scope, dependsOn), IPeriodic<SendNotificationsWorker>
{
public DateTime LastExecution { get; set; } = DateTime.UtcNow;
public TimeSpan Interval { get; set; } = TimeSpan.FromMinutes(1);
protected override BaseWorker[] DoWorkInternal()
{
throw new NotImplementedException();
}
}