28 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
57bb87120a WIP 2025-07-02 02:26:02 +02:00
07880fedb5 Create TrangaBaseContext for common OnConfiguring implementation of Contexts 2025-07-01 23:00:47 +02:00
f1d3203ae1 Notifications-Identifiable 2025-07-01 22:35:44 +02:00
111 changed files with 2048 additions and 12486 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,389 +0,0 @@
using API.APIEndpointRecords;
using API.Schema;
using API.Schema.Contexts;
using API.Schema.Jobs;
using Asp.Versioning;
using log4net;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using static Microsoft.AspNetCore.Http.StatusCodes;
// ReSharper disable InconsistentNaming
namespace API.Controllers;
[ApiVersion(2)]
[ApiController]
[Route("v{version:apiVersion}/[controller]")]
public class JobController(PgsqlContext context, ILog Log) : Controller
{
/// <summary>
/// Returns all Jobs
/// </summary>
/// <response code="200"></response>
[HttpGet]
[ProducesResponseType<Job[]>(Status200OK, "application/json")]
public IActionResult GetAllJobs()
{
Job[] ret = context.Jobs.ToArray();
return Ok(ret);
}
/// <summary>
/// Returns Jobs with requested Job-IDs
/// </summary>
/// <param name="ids">Array of Job-IDs</param>
/// <response code="200"></response>
[HttpPost("WithIDs")]
[ProducesResponseType<Job[]>(Status200OK, "application/json")]
public IActionResult GetJobs([FromBody]string[] ids)
{
Job[] ret = context.Jobs.Where(job => ids.Contains(job.Key)).ToArray();
return Ok(ret);
}
/// <summary>
/// Get all Jobs in requested State
/// </summary>
/// <param name="JobState">Requested Job-State</param>
/// <response code="200"></response>
[HttpGet("State/{JobState}")]
[ProducesResponseType<Job[]>(Status200OK, "application/json")]
public IActionResult GetJobsInState(JobState JobState)
{
Job[] jobsInState = context.Jobs.Where(job => job.state == JobState).ToArray();
return Ok(jobsInState);
}
/// <summary>
/// Returns all Jobs of requested Type
/// </summary>
/// <param name="JobType">Requested Job-Type</param>
/// <response code="200"></response>
[HttpGet("Type/{JobType}")]
[ProducesResponseType<Job[]>(Status200OK, "application/json")]
public IActionResult GetJobsOfType(JobType JobType)
{
Job[] jobsOfType = context.Jobs.Where(job => job.JobType == JobType).ToArray();
return Ok(jobsOfType);
}
/// <summary>
/// Returns all Jobs of requested Type and State
/// </summary>
/// <param name="JobType">Requested Job-Type</param>
/// <param name="JobState">Requested Job-State</param>
/// <response code="200"></response>
[HttpGet("TypeAndState/{JobType}/{JobState}")]
[ProducesResponseType<Job[]>(Status200OK, "application/json")]
public IActionResult GetJobsOfType(JobType JobType, JobState JobState)
{
Job[] jobsOfType = context.Jobs.Where(job => job.JobType == JobType && job.state == JobState).ToArray();
return Ok(jobsOfType);
}
/// <summary>
/// Return Job with ID
/// </summary>
/// <param name="JobId">Job-ID</param>
/// <response code="200"></response>
/// <response code="404">Job with ID could not be found</response>
[HttpGet("{JobId}")]
[ProducesResponseType<Job>(Status200OK, "application/json")]
[ProducesResponseType(Status404NotFound)]
public IActionResult GetJob(string JobId)
{
Job? ret = context.Jobs.Find(JobId);
return (ret is not null) switch
{
true => Ok(ret),
false => NotFound()
};
}
/// <summary>
/// Create a new DownloadAvailableChaptersJob
/// </summary>
/// <param name="MangaId">ID of Obj</param>
/// <param name="record">Job-Configuration</param>
/// <response code="201">Job-IDs</response>
/// <response code="400">Could not find ToFileLibrary with ID</response>
/// <response code="404">Could not find Obj with ID</response>
/// <response code="500">Error during Database Operation</response>
[HttpPut("DownloadAvailableChaptersJob/{MangaId}")]
[ProducesResponseType<string[]>(Status201Created, "application/json")]
[ProducesResponseType(Status400BadRequest)]
[ProducesResponseType(Status404NotFound)]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public IActionResult CreateDownloadAvailableChaptersJob(string MangaId, [FromBody]DownloadAvailableChaptersJobRecord record)
{
if (context.Mangas.Find(MangaId) is not { } m)
return NotFound();
else
{
try
{
FileLibrary? l = context.LocalLibraries.Find(record.localLibraryId);
if (l is null)
return BadRequest();
m.Library = l;
context.SaveChanges();
}
catch (Exception e)
{
Log.Error(e);
return StatusCode(500, e.Message);
}
}
Job retrieveChapters = new RetrieveChaptersJob(m, record.language, record.recurrenceTimeMs);
Job updateFilesDownloaded =
new UpdateChaptersDownloadedJob(m, record.recurrenceTimeMs, dependsOnJobs: [retrieveChapters]);
Job downloadChapters = new DownloadAvailableChaptersJob(m, record.recurrenceTimeMs, dependsOnJobs: [retrieveChapters, updateFilesDownloaded]);
Job UpdateCover = new UpdateCoverJob(m, record.recurrenceTimeMs, downloadChapters);
retrieveChapters.ParentJob = downloadChapters;
updateFilesDownloaded.ParentJob = retrieveChapters;
return AddJobs([retrieveChapters, downloadChapters, updateFilesDownloaded, UpdateCover]);
}
/// <summary>
/// Create a new DownloadSingleChapterJob
/// </summary>
/// <param name="ChapterId">ID of the Chapter</param>
/// <response code="201">Job-IDs</response>
/// <response code="404">Could not find Chapter with ID</response>
/// <response code="500">Error during Database Operation</response>
[HttpPut("DownloadSingleChapterJob/{ChapterId}")]
[ProducesResponseType<string[]>(Status201Created, "application/json")]
[ProducesResponseType(Status404NotFound)]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public IActionResult CreateNewDownloadChapterJob(string ChapterId)
{
if(context.Chapters.Find(ChapterId) is not { } c)
return NotFound();
Job job = new DownloadSingleChapterJob(c);
return AddJobs([job]);
}
/// <summary>
/// Create a new UpdateChaptersDownloadedJob
/// </summary>
/// <param name="MangaId">ID of the Obj</param>
/// <response code="201">Job-IDs</response>
/// <response code="201">Could not find Obj with ID</response>
/// <response code="500">Error during Database Operation</response>
[HttpPut("UpdateFilesJob/{MangaId}")]
[ProducesResponseType<string[]>(Status201Created, "application/json")]
[ProducesResponseType(Status404NotFound)]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public IActionResult CreateUpdateFilesDownloadedJob(string MangaId)
{
if(context.Mangas.Find(MangaId) is not { } m)
return NotFound();
Job job = new UpdateChaptersDownloadedJob(m, 0);
return AddJobs([job]);
}
/// <summary>
/// Create a new UpdateMetadataJob for all Obj
/// </summary>
/// <response code="201">Job-IDs</response>
/// <response code="500">Error during Database Operation</response>
[HttpPut("UpdateAllFilesJob")]
[ProducesResponseType<string[]>(Status201Created, "application/json")]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public IActionResult CreateUpdateAllFilesDownloadedJob()
{
List<UpdateChaptersDownloadedJob> jobs = context.Mangas.Select(m => new UpdateChaptersDownloadedJob(m, 0, null, null)).ToList();
try
{
context.Jobs.AddRange(jobs);
context.SaveChanges();
return Created();
}
catch (Exception e)
{
Log.Error(e);
return StatusCode(500, e.Message);
}
}
/// <summary>
/// Not Implemented: Create a new UpdateMetadataJob
/// </summary>
/// <param name="MangaId">ID of the Obj</param>
/// <response code="201">Job-IDs</response>
/// <response code="404">Could not find Obj with ID</response>
/// <response code="500">Error during Database Operation</response>
[HttpPut("UpdateMetadataJob/{MangaId}")]
[ProducesResponseType<string[]>(Status201Created, "application/json")]
[ProducesResponseType(Status404NotFound)]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public IActionResult CreateUpdateMetadataJob(string MangaId)
{
return StatusCode(Status501NotImplemented);
}
/// <summary>
/// Not Implemented: Create a new UpdateMetadataJob for all Obj
/// </summary>
/// <response code="201">Job-IDs</response>
/// <response code="500">Error during Database Operation</response>
[HttpPut("UpdateAllMetadataJob")]
[ProducesResponseType<string[]>(Status201Created, "application/json")]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public IActionResult CreateUpdateAllMetadataJob()
{
return StatusCode(Status501NotImplemented);
}
private IActionResult AddJobs(Job[] jobs)
{
try
{
context.Jobs.AddRange(jobs);
context.SaveChanges();
return new CreatedResult((string?)null, jobs.Select(j => j.Key).ToArray());
}
catch (Exception e)
{
Log.Error(e);
return StatusCode(500, e.Message);
}
}
/// <summary>
/// Delete Job with ID and all children
/// </summary>
/// <param name="JobId">Job-ID</param>
/// <response code="200"></response>
/// <response code="404">Job could not be found</response>
/// <response code="500">Error during Database Operation</response>
[HttpDelete("{JobId}")]
[ProducesResponseType(Status200OK)]
[ProducesResponseType(Status404NotFound)]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public IActionResult DeleteJob(string JobId)
{
try
{
if(context.Jobs.Find(JobId) is not { } ret)
return NotFound();
context.Remove(ret);
context.SaveChanges();
return Ok();
}
catch (Exception e)
{
Log.Error(e);
return StatusCode(500, e.Message);
}
}
/// <summary>
/// Modify Job with ID
/// </summary>
/// <param name="JobId">Job-ID</param>
/// <param name="modifyJobRecord">Fields to modify, set to null to keep previous value</param>
/// <response code="202">Job modified</response>
/// <response code="400">Malformed request</response>
/// <response code="404">Job with ID not found</response>
/// <response code="500">Error during Database Operation</response>
[HttpPatch("{JobId}")]
[ProducesResponseType<Job>(Status202Accepted, "application/json")]
[ProducesResponseType(Status400BadRequest)]
[ProducesResponseType(Status404NotFound)]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public IActionResult ModifyJob(string JobId, [FromBody]ModifyJobRecord modifyJobRecord)
{
try
{
Job? ret = context.Jobs.Find(JobId);
if(ret is null)
return NotFound();
ret.RecurrenceMs = modifyJobRecord.RecurrenceMs ?? ret.RecurrenceMs;
ret.Enabled = modifyJobRecord.Enabled ?? ret.Enabled;
context.SaveChanges();
return new AcceptedResult(ret.Key, ret);
}
catch (Exception e)
{
Log.Error(e);
return StatusCode(500, e.Message);
}
}
/// <summary>
/// Starts the Job with the requested ID
/// </summary>
/// <param name="JobId">Job-ID</param>
/// <param name="startDependencies">Start Jobs necessary for execution</param>
/// <response code="202">Job started</response>
/// <response code="404">Job with ID not found</response>
/// <response code="409">Job was already running</response>
/// <response code="500">Error during Database Operation</response>
[HttpPost("{JobId}/Start")]
[ProducesResponseType(Status202Accepted)]
[ProducesResponseType(Status404NotFound)]
[ProducesResponseType(Status409Conflict)]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public IActionResult StartJob(string JobId, [FromBody(EmptyBodyBehavior = EmptyBodyBehavior.Allow)]bool startDependencies = false)
{
Job? ret = context.Jobs.Find(JobId);
if (ret is null)
return NotFound();
List<Job> dependencies = startDependencies ? ret.GetDependenciesAndSelf() : [ret];
try
{
if(dependencies.Any(d => d.state >= JobState.Running && d.state < JobState.Completed))
return new ConflictResult();
dependencies.ForEach(d =>
{
d.LastExecution = DateTime.UnixEpoch;
d.state = JobState.CompletedWaiting;
});
context.SaveChanges();
return Accepted();
}
catch (Exception e)
{
Log.Error(e);
return StatusCode(500, e.Message);
}
}
/// <summary>
/// Stops the Job with the requested ID
/// </summary>
/// <param name="JobId">Job-ID</param>
/// <remarks><h1>NOT IMPLEMENTED</h1></remarks>
[HttpPost("{JobId}/Stop")]
[ProducesResponseType(Status501NotImplemented)]
public IActionResult StopJob(string JobId)
{
return StatusCode(Status501NotImplemented);
}
/// <summary>
/// Removes failed and completed Jobs (that are not recurring)
/// </summary>
/// <response code="202">Job started</response>
/// <response code="500">Error during Database Operation</response>
[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

@ -1,19 +1,19 @@
using API.Schema.Contexts; using API.Schema.LibraryContext;
using API.Schema.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,164 +0,0 @@
using API.APIEndpointRecords;
using API.Schema;
using API.Schema.Contexts;
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(PgsqlContext 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.Contexts; using API.Schema.MangaContext;
using API.Schema.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(PgsqlContext 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

@ -1,10 +1,8 @@
using API.Schema; using API.Schema.MangaContext;
using API.Schema.Contexts; using API.Schema.MangaContext.MangaConnectors;
using API.Schema.Jobs; 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,10 +16,10 @@ namespace API.Controllers;
[ApiVersion(2)] [ApiVersion(2)]
[ApiController] [ApiController]
[Route("v{v:apiVersion}/[controller]")] [Route("v{v:apiVersion}/[controller]")]
public class MangaController(PgsqlContext context, ILog Log) : Controller public class MangaController(MangaContext context) : Controller
{ {
/// <summary> /// <summary>
/// Returns all cached Obj /// Returns all cached <see cref="Manga"/>
/// </summary> /// </summary>
/// <response code="200"></response> /// <response code="200"></response>
[HttpGet] [HttpGet]
@ -33,41 +31,40 @@ public class MangaController(PgsqlContext context, ILog Log) : Controller
} }
/// <summary> /// <summary>
/// Returns all cached Obj with IDs /// Returns all cached <see cref="Manga"/> with <paramref name="MangaIds"/>
/// </summary> /// </summary>
/// <param name="ids">Array of Obj-IDs</param> /// <param name="MangaIds">Array of <<see cref="Manga"/>.Key</param>
/// <response code="200"></response> /// <response code="200"></response>
[HttpPost("WithIDs")] [HttpPost("WithIDs")]
[ProducesResponseType<Manga[]>(Status200OK, "application/json")] [ProducesResponseType<Manga[]>(Status200OK, "application/json")]
public IActionResult GetManga([FromBody]string[] ids) public IActionResult GetManga([FromBody]string[] MangaIds)
{ {
Manga[] ret = context.Mangas.Where(m => ids.Contains(m.Key)).ToArray(); Manga[] ret = context.Mangas.Where(m => MangaIds.Contains(m.Key)).ToArray();
return Ok(ret); return Ok(ret);
} }
/// <summary> /// <summary>
/// Return Obj with ID /// Return <see cref="Manga"/> with <paramref name="MangaId"/>
/// </summary> /// </summary>
/// <param name="MangaId">Obj-ID</param> /// <param name="MangaId"><see cref="Manga"/>.Key</param>
/// <response code="200"></response> /// <response code="200"></response>
/// <response code="404">Obj with ID not found</response> /// <response code="404"><see cref="Manga"/> with <paramref name="MangaId"/> not found</response>
[HttpGet("{MangaId}")] [HttpGet("{MangaId}")]
[ProducesResponseType<Manga>(Status200OK, "application/json")] [ProducesResponseType<Manga>(Status200OK, "application/json")]
[ProducesResponseType(Status404NotFound)] [ProducesResponseType(Status404NotFound)]
public IActionResult GetManga(string MangaId) public IActionResult GetManga(string MangaId)
{ {
Manga? ret = context.Mangas.Find(MangaId); if (context.Mangas.Find(MangaId) is not { } manga)
if (ret is null) return NotFound(nameof(MangaId));
return NotFound(); return Ok(manga);
return Ok(ret);
} }
/// <summary> /// <summary>
/// Delete Obj with ID /// Delete <see cref="Manga"/> with <paramref name="MangaId"/>
/// </summary> /// </summary>
/// <param name="MangaId">Obj-ID</param> /// <param name="MangaId"><see cref="Manga"/>.Key</param>
/// <response code="200"></response> /// <response code="200"></response>
/// <response code="404">Obj 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>
[HttpDelete("{MangaId}")] [HttpDelete("{MangaId}")]
[ProducesResponseType(Status200OK)] [ProducesResponseType(Status200OK)]
@ -75,62 +72,50 @@ public class MangaController(PgsqlContext context, ILog Log) : Controller
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")] [ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public IActionResult DeleteManga(string MangaId) public IActionResult DeleteManga(string MangaId)
{ {
try if (context.Mangas.Find(MangaId) is not { } manga)
{ return NotFound(nameof(MangaId));
Manga? ret = context.Mangas.Find(MangaId);
if (ret is null) context.Mangas.Remove(manga);
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);
}
} }
/// <summary> /// <summary>
/// Merge two Manga into one. THIS IS NOT REVERSIBLE! /// Merge two <see cref="Manga"/> into one. THIS IS NOT REVERSIBLE!
/// </summary> /// </summary>
/// <param name="MangaIdFrom"><see cref="Manga"/>.Key of <see cref="Manga"/> merging data from (getting deleted)</param>
/// <param name="MangaIdInto"><see cref="Manga"/>.Key of <see cref="Manga"/> merging data into</param>
/// <response code="200"></response> /// <response code="200"></response>
/// <response code="404">MangaId not found</response> /// <response code="404"><see cref="Manga"/> with <paramref name="MangaIdFrom"/> or <paramref name="MangaIdInto"/> not found</response>
/// <response code="500">Error during Database Operation</response> [HttpPatch("{MangaIdFrom}/MergeInto/{MangaIdInto}")]
[HttpPatch("{MangaIdFrom}/MergeInto/{MangaIdTo}")]
[ProducesResponseType<byte[]>(Status200OK,"image/jpeg")] [ProducesResponseType<byte[]>(Status200OK,"image/jpeg")]
[ProducesResponseType(Status404NotFound)] [ProducesResponseType(Status404NotFound)]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")] public IActionResult MergeIntoManga(string MangaIdFrom, string MangaIdInto)
public IActionResult MergeIntoManga(string MangaIdFrom, string MangaIdTo)
{ {
if(context.Mangas.Find(MangaIdFrom) is not { } from) if (context.Mangas.Find(MangaIdFrom) is not { } from)
return NotFound(MangaIdFrom); return NotFound(nameof(MangaIdFrom));
if(context.Mangas.Find(MangaIdTo) is not { } to) if (context.Mangas.Find(MangaIdInto) is not { } into)
return NotFound(MangaIdTo); return NotFound(nameof(MangaIdInto));
try
{ BaseWorker[] newJobs = into.MergeFrom(from, context);
to.MergeFrom(from, context); Tranga.AddWorkers(newJobs);
return Ok();
} return Ok();
catch (DbUpdateException e)
{
Log.Error(e);
return StatusCode(500, e.Message);
}
} }
/// <summary> /// <summary>
/// Returns Cover of Obj /// Returns Cover of <see cref="Manga"/> with <paramref name="MangaId"/>
/// </summary> /// </summary>
/// <param name="MangaId">Obj-ID</param> /// <param name="MangaId"><see cref="Manga"/>.Key</param>
/// <param name="width">If width is provided, height needs to also be provided</param> /// <param name="width">If <paramref name="width"/> is provided, <paramref name="height"/> needs to also be provided</param>
/// <param name="height">If height is provided, width needs to also be provided</param> /// <param name="height">If <paramref name="height"/> is provided, <paramref name="width"/> needs to also be provided</param>
/// <response code="200">JPEG Image</response> /// <response code="200">JPEG Image</response>
/// <response code="204">Cover not loaded</response> /// <response code="204">Cover not loaded</response>
/// <response code="400">The formatting-request was invalid</response> /// <response code="400">The formatting-request was invalid</response>
/// <response code="404">Obj with ID not found</response> /// <response code="404"><see cref="Manga"/> with <paramref name="MangaId"/> not found</response>
/// <response code="503">Retry later, downloading cover</response> /// <response code="503">Retry later, downloading cover</response>
[HttpGet("{MangaId}/Cover")] [HttpGet("{MangaId}/Cover")]
[ProducesResponseType<byte[]>(Status200OK,"image/jpeg")] [ProducesResponseType<byte[]>(Status200OK,"image/jpeg")]
@ -140,22 +125,21 @@ public class MangaController(PgsqlContext context, ILog Log) : 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)
{ {
if(context.Mangas.Find(MangaId) is not { } m) if (context.Mangas.Find(MangaId) is not { } manga)
return NotFound(); return NotFound(nameof(MangaId));
if (!System.IO.File.Exists(m.CoverFileNameInCache)) if (!System.IO.File.Exists(manga.CoverFileNameInCache))
{ {
List<Job> coverDownloadJobs = context.Jobs.Where(j => j.JobType == JobType.DownloadMangaCoverJob).Include(j => ((DownloadMangaCoverJob)j).Manga).ToList(); if (Tranga.GetRunningWorkers().Any(worker => worker is DownloadCoverFromMangaconnectorWorker w && context.MangaConnectorToManga.Find(w.MangaConnectorIdId)?.ObjId == MangaId))
if (coverDownloadJobs.Any(j => j is DownloadMangaCoverJob dmc && dmc.MangaId == MangaId && dmc.state < JobState.Completed))
{ {
Response.Headers.Append("Retry-After", $"{TrangaSettings.startNewJobTimeoutMs * coverDownloadJobs.Count() * 2 / 1000:D}"); Response.Headers.Append("Retry-After", $"{Tranga.Settings.WorkCycleTimeoutMs * 2 / 1000:D}");
return StatusCode(Status503ServiceUnavailable, TrangaSettings.startNewJobTimeoutMs * coverDownloadJobs.Count() * 2 / 1000); return StatusCode(Status503ServiceUnavailable, Tranga.Settings.WorkCycleTimeoutMs * 2 / 1000);
} }
else else
return NoContent(); return NoContent();
} }
Image image = Image.Load(m.CoverFileNameInCache); Image image = Image.Load(manga.CoverFileNameInCache);
if (width is { } w && height is { } h) if (width is { } w && height is { } h)
{ {
@ -170,46 +154,46 @@ public class MangaController(PgsqlContext context, ILog Log) : Controller
using MemoryStream ms = new(); using MemoryStream ms = new();
image.Save(ms, new JpegEncoder(){Quality = 100}); image.Save(ms, new JpegEncoder(){Quality = 100});
DateTime lastModified = new FileInfo(m.CoverFileNameInCache).LastWriteTime; DateTime lastModified = new FileInfo(manga.CoverFileNameInCache).LastWriteTime;
HttpContext.Response.Headers.CacheControl = "public"; HttpContext.Response.Headers.CacheControl = "public";
return File(ms.GetBuffer(), "image/jpeg", new DateTimeOffset(lastModified), EntityTagHeaderValue.Parse($"\"{lastModified.Ticks}\"")); return File(ms.GetBuffer(), "image/jpeg", new DateTimeOffset(lastModified), EntityTagHeaderValue.Parse($"\"{lastModified.Ticks}\""));
} }
/// <summary> /// <summary>
/// Returns all Chapters of Obj /// Returns all <see cref="Chapter"/> of <see cref="Manga"/> with <paramref name="MangaId"/>
/// </summary> /// </summary>
/// <param name="MangaId">Obj-ID</param> /// <param name="MangaId"><see cref="Manga"/>.Key</param>
/// <response code="200"></response> /// <response code="200"></response>
/// <response code="404">Obj with ID not found</response> /// <response code="404"><see cref="Manga"/> with <paramref name="MangaId"/> not found</response>
[HttpGet("{MangaId}/Chapters")] [HttpGet("{MangaId}/Chapters")]
[ProducesResponseType<Chapter[]>(Status200OK, "application/json")] [ProducesResponseType<Chapter[]>(Status200OK, "application/json")]
[ProducesResponseType(Status404NotFound)] [ProducesResponseType(Status404NotFound)]
public IActionResult GetChapters(string MangaId) public IActionResult GetChapters(string MangaId)
{ {
if(context.Mangas.Find(MangaId) is not { } m) if (context.Mangas.Find(MangaId) is not { } manga)
return NotFound(); return NotFound(nameof(MangaId));
Chapter[] chapters = m.Chapters.ToArray(); Chapter[] chapters = manga.Chapters.ToArray();
return Ok(chapters); return Ok(chapters);
} }
/// <summary> /// <summary>
/// Returns all downloaded Chapters for Obj with ID /// Returns all downloaded <see cref="Chapter"/> for <see cref="Manga"/> with <paramref name="MangaId"/>
/// </summary> /// </summary>
/// <param name="MangaId">Obj-ID</param> /// <param name="MangaId"><see cref="Manga"/>.Key</param>
/// <response code="200"></response> /// <response code="200"></response>
/// <response code="204">No available chapters</response> /// <response code="204">No available chapters</response>
/// <response code="404">Obj with ID not found.</response> /// <response code="404"><see cref="Manga"/> with <paramref name="MangaId"/> not found.</response>
[HttpGet("{MangaId}/Chapters/Downloaded")] [HttpGet("{MangaId}/Chapters/Downloaded")]
[ProducesResponseType<Chapter[]>(Status200OK, "application/json")] [ProducesResponseType<Chapter[]>(Status200OK, "application/json")]
[ProducesResponseType(Status204NoContent)] [ProducesResponseType(Status204NoContent)]
[ProducesResponseType(Status404NotFound)] [ProducesResponseType(Status404NotFound)]
public IActionResult GetChaptersDownloaded(string MangaId) public IActionResult GetChaptersDownloaded(string MangaId)
{ {
if(context.Mangas.Find(MangaId) is not { } m) if (context.Mangas.Find(MangaId) is not { } manga)
return NotFound(); return NotFound(nameof(MangaId));
List<Chapter> chapters = m.Chapters.ToList(); List<Chapter> chapters = manga.Chapters.Where(c => c.Downloaded).ToList();
if (chapters.Count == 0) if (chapters.Count == 0)
return NoContent(); return NoContent();
@ -217,22 +201,22 @@ public class MangaController(PgsqlContext context, ILog Log) : Controller
} }
/// <summary> /// <summary>
/// Returns all Chapters not downloaded for Obj with ID /// Returns all <see cref="Chapter"/> not downloaded for <see cref="Manga"/> with <paramref name="MangaId"/>
/// </summary> /// </summary>
/// <param name="MangaId">Obj-ID</param> /// <param name="MangaId"><see cref="Manga"/>.Key</param>
/// <response code="200"></response> /// <response code="200"></response>
/// <response code="204">No available chapters</response> /// <response code="204">No available chapters</response>
/// <response code="404">Obj with ID not found.</response> /// <response code="404"><see cref="Manga"/> with <paramref name="MangaId"/> not found.</response>
[HttpGet("{MangaId}/Chapters/NotDownloaded")] [HttpGet("{MangaId}/Chapters/NotDownloaded")]
[ProducesResponseType<Chapter[]>(Status200OK, "application/json")] [ProducesResponseType<Chapter[]>(Status200OK, "application/json")]
[ProducesResponseType(Status204NoContent)] [ProducesResponseType(Status204NoContent)]
[ProducesResponseType(Status404NotFound)] [ProducesResponseType(Status404NotFound)]
public IActionResult GetChaptersNotDownloaded(string MangaId) public IActionResult GetChaptersNotDownloaded(string MangaId)
{ {
if(context.Mangas.Find(MangaId) is not { } m) if (context.Mangas.Find(MangaId) is not { } manga)
return NotFound(); return NotFound(nameof(MangaId));
List<Chapter> chapters = m.Chapters.ToList(); List<Chapter> chapters = manga.Chapters.Where(c => c.Downloaded == false).ToList();
if (chapters.Count == 0) if (chapters.Count == 0)
return NoContent(); return NoContent();
@ -240,13 +224,13 @@ public class MangaController(PgsqlContext context, ILog Log) : Controller
} }
/// <summary> /// <summary>
/// Returns the latest Chapter of requested Obj available on Website /// Returns the latest <see cref="Chapter"/> of requested <see cref="Manga"/> available on <see cref="MangaConnector"/>
/// </summary> /// </summary>
/// <param name="MangaId">Obj-ID</param> /// <param name="MangaId"><see cref="Manga"/>.Key</param>
/// <response code="200"></response> /// <response code="200"></response>
/// <response code="204">No available chapters</response> /// <response code="204">No available chapters</response>
/// <response code="404">Obj with ID not found.</response> /// <response code="404"><see cref="Manga"/> with <paramref name="MangaId"/> not found.</response>
/// <response code="500">Could not retrieve the maximum chapter-number</response> /// <response code="412">Could not retrieve the maximum chapter-number</response>
/// <response code="503">Retry after timeout, updating value</response> /// <response code="503">Retry after timeout, updating value</response>
[HttpGet("{MangaId}/Chapter/LatestAvailable")] [HttpGet("{MangaId}/Chapter/LatestAvailable")]
[ProducesResponseType<Chapter>(Status200OK, "application/json")] [ProducesResponseType<Chapter>(Status200OK, "application/json")]
@ -256,130 +240,151 @@ public class MangaController(PgsqlContext context, ILog Log) : Controller
[ProducesResponseType<int>(Status503ServiceUnavailable, "text/plain")] [ProducesResponseType<int>(Status503ServiceUnavailable, "text/plain")]
public IActionResult GetLatestChapter(string MangaId) public IActionResult GetLatestChapter(string MangaId)
{ {
if(context.Mangas.Find(MangaId) is not { } m) if (context.Mangas.Find(MangaId) is not { } manga)
return NotFound(); return NotFound(nameof(MangaId));
List<Chapter> chapters = m.Chapters.ToList(); List<Chapter> chapters = manga.Chapters.ToList();
if (chapters.Count == 0) if (chapters.Count == 0)
{ {
List<Job> retrieveChapterJobs = context.Jobs.Where(j => j.JobType == JobType.RetrieveChaptersJob).Include(j => ((RetrieveChaptersJob)j).Manga).ToList(); if (Tranga.GetRunningWorkers().Any(worker => worker is RetrieveMangaChaptersFromMangaconnectorWorker w && context.MangaConnectorToManga.Find(w.MangaConnectorIdId)?.ObjId == MangaId && w.State < WorkerExecutionState.Completed))
if (retrieveChapterJobs.Any(j => j is RetrieveChaptersJob rcj && rcj.MangaId == MangaId && rcj.state < JobState.Completed))
{ {
Response.Headers.Append("Retry-After", $"{TrangaSettings.startNewJobTimeoutMs * retrieveChapterJobs.Count() * 2 / 1000:D}"); Response.Headers.Append("Retry-After", $"{Tranga.Settings.WorkCycleTimeoutMs * 2 / 1000:D}");
return StatusCode(Status503ServiceUnavailable, TrangaSettings.startNewJobTimeoutMs * retrieveChapterJobs.Count() * 2/ 1000); return StatusCode(Status503ServiceUnavailable, Tranga.Settings.WorkCycleTimeoutMs * 2/ 1000);
}else }else
return Ok(0); return Ok(0);
} }
Chapter? max = chapters.Max(); Chapter? max = chapters.Max();
if (max is null) if (max is null)
return StatusCode(500, "Max chapter could not be found"); return StatusCode(Status500InternalServerError, "Max chapter could not be found");
return Ok(max); return Ok(max);
} }
/// <summary> /// <summary>
/// Returns the latest Chapter of requested Obj that is downloaded /// Returns the latest <see cref="Chapter"/> of requested <see cref="Manga"/> that is downloaded
/// </summary> /// </summary>
/// <param name="MangaId">Obj-ID</param> /// <param name="MangaId"><see cref="Manga"/>.Key</param>
/// <response code="200"></response> /// <response code="200"></response>
/// <response code="204">No available chapters</response> /// <response code="204">No available chapters</response>
/// <response code="404">Obj with ID not found.</response> /// <response code="404"><see cref="Manga"/> with <paramref name="MangaId"/> not found.</response>
/// <response code="500">Could not retrieve the maximum chapter-number</response> /// <response code="412">Could not retrieve the maximum chapter-number</response>
/// <response code="503">Retry after timeout, updating value</response> /// <response code="503">Retry after timeout, updating value</response>
[HttpGet("{MangaId}/Chapter/LatestDownloaded")] [HttpGet("{MangaId}/Chapter/LatestDownloaded")]
[ProducesResponseType<Chapter>(Status200OK, "application/json")] [ProducesResponseType<Chapter>(Status200OK, "application/json")]
[ProducesResponseType(Status204NoContent)] [ProducesResponseType(Status204NoContent)]
[ProducesResponseType(Status404NotFound)] [ProducesResponseType(Status404NotFound)]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")] [ProducesResponseType<string>(Status412PreconditionFailed, "text/plain")]
[ProducesResponseType<int>(Status503ServiceUnavailable, "text/plain")] [ProducesResponseType<int>(Status503ServiceUnavailable, "text/plain")]
public IActionResult GetLatestChapterDownloaded(string MangaId) public IActionResult GetLatestChapterDownloaded(string MangaId)
{ {
if(context.Mangas.Find(MangaId) is not { } m) if (context.Mangas.Find(MangaId) is not { } manga)
return NotFound(); return NotFound(nameof(MangaId));
List<Chapter> chapters = m.Chapters.ToList(); List<Chapter> chapters = manga.Chapters.ToList();
if (chapters.Count == 0) if (chapters.Count == 0)
{ {
List<Job> retrieveChapterJobs = context.Jobs.Where(j => j.JobType == JobType.RetrieveChaptersJob).Include(j => ((RetrieveChaptersJob)j).Manga).ToList(); if (Tranga.GetRunningWorkers().Any(worker => worker is RetrieveMangaChaptersFromMangaconnectorWorker w && context.MangaConnectorToManga.Find(w.MangaConnectorIdId)?.ObjId == MangaId && w.State < WorkerExecutionState.Completed))
if (retrieveChapterJobs.Any(j => j is RetrieveChaptersJob rcj && rcj.MangaId == MangaId && rcj.state < JobState.Completed))
{ {
Response.Headers.Append("Retry-After", $"{TrangaSettings.startNewJobTimeoutMs * retrieveChapterJobs.Count() * 2 / 1000:D}"); Response.Headers.Append("Retry-After", $"{Tranga.Settings.WorkCycleTimeoutMs * 2 / 1000:D}");
return StatusCode(Status503ServiceUnavailable, TrangaSettings.startNewJobTimeoutMs * retrieveChapterJobs.Count() * 2 / 1000); return StatusCode(Status503ServiceUnavailable, Tranga.Settings.WorkCycleTimeoutMs * 2/ 1000);
}else }else
return NoContent(); return NoContent();
} }
Chapter? max = chapters.Max(); Chapter? max = chapters.Max();
if (max is null) if (max is null)
return StatusCode(500, "Max chapter could not be found"); return StatusCode(Status412PreconditionFailed, "Max chapter could not be found");
return Ok(max); return Ok(max);
} }
/// <summary> /// <summary>
/// Configure the cut-off for Obj /// Configure the <see cref="Chapter"/> cut-off for <see cref="Manga"/>
/// </summary> /// </summary>
/// <param name="MangaId">Obj-ID</param> /// <param name="MangaId"><see cref="Manga"/>.Key</param>
/// <param name="chapterThreshold">Threshold (Chapter Number)</param> /// <param name="chapterThreshold">Threshold (<see cref="Chapter"/> ChapterNumber)</param>
/// <response code="200"></response> /// <response code="202"></response>
/// <response code="404">Obj 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>
[HttpPatch("{MangaId}/IgnoreChaptersBefore")] [HttpPatch("{MangaId}/IgnoreChaptersBefore")]
[ProducesResponseType(Status200OK)] [ProducesResponseType(Status202Accepted)]
[ProducesResponseType(Status404NotFound)] [ProducesResponseType(Status404NotFound)]
[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)
{ {
Manga? m = context.Mangas.Find(MangaId); if (context.Mangas.Find(MangaId) is not { } manga)
if (m is null)
return NotFound(); return NotFound();
try manga.IgnoreChaptersBefore = chapterThreshold;
{ if(context.Sync() is { success: false } result)
m.IgnoreChaptersBefore = chapterThreshold; return StatusCode(Status500InternalServerError, result.exceptionMessage);
context.SaveChanges();
return Ok(); return Accepted();
}
catch (Exception e)
{
Log.Error(e);
return StatusCode(500, e.Message);
}
} }
/// <summary> /// <summary>
/// Move Obj to different ToFileLibrary /// Move <see cref="Manga"/> to different <see cref="FileLibrary"/>
/// </summary> /// </summary>
/// <param name="MangaId">Obj-ID</param> /// <param name="MangaId"><see cref="Manga"/>.Key</param>
/// <param name="LibraryId">ToFileLibrary-Id</param> /// <param name="LibraryId"><see cref="FileLibrary"/>.Key</param>
/// <response code="202">Folder is going to be moved</response> /// <response code="202">Folder is going to be moved</response>
/// <response code="404">MangaId or LibraryId not found</response> /// <response code="404"><paramref name="MangaId"/> or <paramref name="LibraryId"/> not found</response>
/// <response code="500">Error during Database Operation</response>
[HttpPost("{MangaId}/ChangeLibrary/{LibraryId}")] [HttpPost("{MangaId}/ChangeLibrary/{LibraryId}")]
[ProducesResponseType(Status202Accepted)] [ProducesResponseType(Status202Accepted)]
[ProducesResponseType(Status404NotFound)] [ProducesResponseType(Status404NotFound)]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public IActionResult MoveFolder(string MangaId, string LibraryId) public IActionResult MoveFolder(string MangaId, string LibraryId)
{ {
if (context.Mangas.Find(MangaId) is not { } manga) if (context.Mangas.Find(MangaId) is not { } manga)
return NotFound(); return NotFound(nameof(MangaId));
if(context.LocalLibraries.Find(LibraryId) is not { } library) if(context.FileLibraries.Find(LibraryId) is not { } library)
return NotFound(); return NotFound(nameof(LibraryId));
MoveMangaLibraryJob moveLibrary = new(manga, library); MoveMangaLibraryWorker moveLibrary = new(manga, library);
UpdateChaptersDownloadedJob updateDownloadedFiles = new(manga, 0, dependsOnJobs: [moveLibrary]);
try Tranga.AddWorkers([moveLibrary]);
{
context.Jobs.AddRange(moveLibrary, updateDownloadedFiles); return Accepted();
context.SaveChanges(); }
return Accepted();
} /// <summary>
catch (Exception e) /// (Un-)Marks <see cref="Manga"/> as requested for Download from <see cref="MangaConnector"/>
{ /// </summary>
Log.Error(e); /// <param name="MangaId"><see cref="Manga"/> with <paramref name="MangaId"/></param>
return StatusCode(500, e.Message); /// <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,8 +1,6 @@
using API.Schema; using API.Schema.MangaContext;
using API.Schema.Contexts; using API.Schema.MangaContext.MetadataFetchers;
using API.Schema.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;
@ -13,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(PgsqlContext 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)]
@ -53,7 +52,7 @@ public class MetadataFetcherController(PgsqlContext 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);
@ -61,14 +60,17 @@ public class MetadataFetcherController(PgsqlContext 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")]
@ -76,30 +78,24 @@ public class MetadataFetcherController(PgsqlContext 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)]
@ -109,58 +105,17 @@ public class MetadataFetcherController(PgsqlContext 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

@ -1,11 +1,11 @@
using System.Text; using System.Text;
using API.APIEndpointRecords; using API.APIEndpointRecords;
using API.Schema.Contexts; using API.Schema.NotificationsContext;
using API.Schema.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,7 +1,5 @@
using API.Schema; using API.Schema.MangaContext;
using API.Schema.Contexts;
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
@ -11,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(PgsqlContext 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,10 +1,7 @@
using API.Schema; using API.Schema.MangaContext;
using API.Schema.Contexts; 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
@ -13,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(PgsqlContext 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)
@ -89,51 +61,10 @@ public class SearchController(PgsqlContext 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, PgsqlContext 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,29 +1,25 @@
using API.MangaDownloadClients; using API.MangaDownloadClients;
using API.Schema;
using API.Schema.Contexts;
using API.Schema.Jobs;
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(PgsqlContext 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>
@ -34,7 +30,7 @@ public class SettingsController(PgsqlContext 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>
@ -45,7 +41,8 @@ public class SettingsController(PgsqlContext 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();
} }
@ -57,7 +54,7 @@ public class SettingsController(PgsqlContext 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();
} }
@ -69,7 +66,7 @@ public class SettingsController(PgsqlContext 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>
@ -97,7 +94,7 @@ public class SettingsController(PgsqlContext 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();
} }
@ -109,7 +106,7 @@ public class SettingsController(PgsqlContext 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();
} }
@ -121,35 +118,35 @@ public class SettingsController(PgsqlContext 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();
} }
@ -161,7 +158,7 @@ public class SettingsController(PgsqlContext 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>
@ -169,37 +166,11 @@ public class SettingsController(PgsqlContext 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();
} }
@ -225,7 +196,7 @@ public class SettingsController(PgsqlContext 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>
@ -238,58 +209,20 @@ public class SettingsController(PgsqlContext 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>
@ -301,7 +234,7 @@ public class SettingsController(PgsqlContext 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();
} }
@ -313,7 +246,7 @@ public class SettingsController(PgsqlContext 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();
} }
@ -332,4 +265,28 @@ public class SettingsController(PgsqlContext 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

@ -0,0 +1,154 @@
using API.APIEndpointRecords;
using API.Workers;
using Asp.Versioning;
using log4net;
using Microsoft.AspNetCore.Mvc;
using static Microsoft.AspNetCore.Http.StatusCodes;
// ReSharper disable InconsistentNaming
namespace API.Controllers;
[ApiVersion(2)]
[ApiController]
[Route("v{version:apiVersion}/[controller]")]
public class WorkerController() : Controller
{
/// <summary>
/// Returns all <see cref="BaseWorker"/>
/// </summary>
/// <response code="200"></response>
[HttpGet]
[ProducesResponseType<BaseWorker[]>(Status200OK, "application/json")]
public IActionResult GetAllWorkers()
{
return Ok(Tranga.AllWorkers.ToArray());
}
/// <summary>
/// Returns <see cref="BaseWorker"/> with requested <paramref name="WorkerIds"/>
/// </summary>
/// <param name="WorkerIds">Array of <see cref="BaseWorker"/>.Key</param>
/// <response code="200"></response>
[HttpPost("WithIDs")]
[ProducesResponseType<BaseWorker[]>(Status200OK, "application/json")]
public IActionResult GetJobs([FromBody]string[] WorkerIds)
{
return Ok(Tranga.AllWorkers.Where(worker => WorkerIds.Contains(worker.Key)).ToArray());
}
/// <summary>
/// Get all <see cref="BaseWorker"/> in requested <see cref="WorkerExecutionState"/>
/// </summary>
/// <param name="State">Requested <see cref="WorkerExecutionState"/></param>
/// <response code="200"></response>
[HttpGet("State/{State}")]
[ProducesResponseType<BaseWorker[]>(Status200OK, "application/json")]
public IActionResult GetJobsInState(WorkerExecutionState State)
{
return Ok(Tranga.AllWorkers.Where(worker => worker.State == State).ToArray());
}
/// <summary>
/// Return <see cref="BaseWorker"/> with <paramref name="WorkerId"/>
/// </summary>
/// <param name="WorkerId"><see cref="BaseWorker"/>.Key</param>
/// <response code="200"></response>
/// <response code="404"><see cref="BaseWorker"/> with <paramref name="WorkerId"/> could not be found</response>
[HttpGet("{WorkerId}")]
[ProducesResponseType<BaseWorker>(Status200OK, "application/json")]
[ProducesResponseType(Status404NotFound)]
public IActionResult GetJob(string WorkerId)
{
if(Tranga.AllWorkers.FirstOrDefault(w => w.Key == WorkerId) is not { } worker)
return NotFound(nameof(WorkerId));
return Ok(worker);
}
/// <summary>
/// Delete <see cref="BaseWorker"/> with <paramref name="WorkerId"/> and all child-<see cref="BaseWorker"/>s
/// </summary>
/// <param name="WorkerId"><see cref="BaseWorker"/>.Key</param>
/// <response code="200"></response>
/// <response code="404"><see cref="BaseWorker"/> with <paramref name="WorkerId"/> could not be found</response>
[HttpDelete("{WorkerId}")]
[ProducesResponseType(Status200OK)]
[ProducesResponseType(Status404NotFound)]
public IActionResult DeleteJob(string WorkerId)
{
if(Tranga.AllWorkers.FirstOrDefault(w => w.Key == WorkerId) is not { } worker)
return NotFound(nameof(WorkerId));
Tranga.RemoveWorker(worker);
return Ok();
}
/// <summary>
/// Modify <see cref="BaseWorker"/> with <paramref name="WorkerId"/>
/// </summary>
/// <param name="WorkerId"><see cref="BaseWorker"/>.Key</param>
/// <param name="modifyWorkerRecord">Fields to modify, set to null to keep previous value</param>
/// <response code="202"></response>
/// <response code="400"></response>
/// <response code="404"><see cref="BaseWorker"/> with <paramref name="WorkerId"/> could not be found</response>
/// <response code="409"><see cref="BaseWorker"/> is not <see cref="IPeriodic"/>, can not modify <paramref name="modifyWorkerRecord.IntervalMs"/></response>
[HttpPatch("{WorkerId}")]
[ProducesResponseType<BaseWorker>(Status202Accepted, "application/json")]
[ProducesResponseType(Status400BadRequest)]
[ProducesResponseType(Status404NotFound)]
[ProducesResponseType<string>(Status409Conflict, "text/plain")]
public IActionResult ModifyJob(string WorkerId, [FromBody]ModifyWorkerRecord modifyWorkerRecord)
{
if(Tranga.AllWorkers.FirstOrDefault(w => w.Key == WorkerId) is not { } worker)
return NotFound(nameof(WorkerId));
if(modifyWorkerRecord.IntervalMs is not null && worker is not IPeriodic)
return Conflict("Can not modify Interval of non-Periodic worker");
else if(modifyWorkerRecord.IntervalMs is not null && worker is IPeriodic periodic)
periodic.Interval = TimeSpan.FromMilliseconds((long)modifyWorkerRecord.IntervalMs);
return Accepted(worker);
}
/// <summary>
/// Starts <see cref="BaseWorker"/> with <paramref name="WorkerId"/>
/// </summary>
/// <param name="WorkerId"><see cref="BaseWorker"/>.Key</param>
/// <response code="200"></response>
/// <response code="404"><see cref="BaseWorker"/> with <paramref name="WorkerId"/> could not be found</response>
/// <response code="412"><see cref="BaseWorker"/> was already running</response>
[HttpPost("{WorkerId}/Start")]
[ProducesResponseType(Status202Accepted)]
[ProducesResponseType(Status404NotFound)]
[ProducesResponseType<string>(Status412PreconditionFailed, "text/plain")]
public IActionResult StartJob(string WorkerId)
{
if(Tranga.AllWorkers.FirstOrDefault(w => w.Key == WorkerId) is not { } worker)
return NotFound(nameof(WorkerId));
if (worker.State >= WorkerExecutionState.Waiting)
return StatusCode(Status412PreconditionFailed, "Already running");
Tranga.MarkWorkerForStart(worker);
return Ok();
}
/// <summary>
/// Stops <see cref="BaseWorker"/> with <paramref name="WorkerId"/>
/// </summary>
/// <param name="WorkerId"><see cref="BaseWorker"/>.Key</param>
/// <response code="200"></response>
/// <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)]
public IActionResult StopJob(string WorkerId)
{
if(Tranga.AllWorkers.FirstOrDefault(w => w.Key == WorkerId) is not { } worker)
return NotFound(nameof(WorkerId));
if(worker.State is < WorkerExecutionState.Running or >= WorkerExecutionState.Completed)
return StatusCode(Status208AlreadyReported, "Not running");
Tranga.StopWorker(worker);
return Ok();
}
}

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

@ -1,5 +1,5 @@
// <auto-generated /> // <auto-generated />
using API.Schema.Contexts; using API.Schema.LibraryContext;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Migrations;
@ -8,10 +8,10 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable #nullable disable
namespace API.Migrations.library namespace API.Migrations.Library
{ {
[DbContext(typeof(LibraryContext))] [DbContext(typeof(LibraryContext))]
[Migration("20250515120732_Initial")] [Migration("20250703191925_Initial")]
partial class Initial partial class Initial
{ {
/// <inheritdoc /> /// <inheritdoc />
@ -19,16 +19,15 @@ namespace API.Migrations.library
{ {
#pragma warning disable 612, 618 #pragma warning disable 612, 618
modelBuilder modelBuilder
.HasAnnotation("ProductVersion", "9.0.3") .HasAnnotation("ProductVersion", "9.0.5")
.HasAnnotation("Relational:MaxIdentifierLength", 63); .HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("API.Schema.LibraryConnectors.LibraryConnector", b => modelBuilder.Entity("API.Schema.LibraryContext.LibraryConnectors.LibraryConnector", b =>
{ {
b.Property<string>("LibraryConnectorId") b.Property<string>("Key")
.HasMaxLength(64) .HasColumnType("text");
.HasColumnType("character varying(64)");
b.Property<string>("Auth") b.Property<string>("Auth")
.IsRequired() .IsRequired()
@ -43,7 +42,7 @@ namespace API.Migrations.library
b.Property<byte>("LibraryType") b.Property<byte>("LibraryType")
.HasColumnType("smallint"); .HasColumnType("smallint");
b.HasKey("LibraryConnectorId"); b.HasKey("Key");
b.ToTable("LibraryConnectors"); b.ToTable("LibraryConnectors");
@ -52,16 +51,16 @@ namespace API.Migrations.library
b.UseTphMappingStrategy(); b.UseTphMappingStrategy();
}); });
modelBuilder.Entity("API.Schema.LibraryConnectors.Kavita", b => modelBuilder.Entity("API.Schema.LibraryContext.LibraryConnectors.Kavita", b =>
{ {
b.HasBaseType("API.Schema.LibraryConnectors.LibraryConnector"); b.HasBaseType("API.Schema.LibraryContext.LibraryConnectors.LibraryConnector");
b.HasDiscriminator().HasValue((byte)1); b.HasDiscriminator().HasValue((byte)1);
}); });
modelBuilder.Entity("API.Schema.LibraryConnectors.Komga", b => modelBuilder.Entity("API.Schema.LibraryContext.LibraryConnectors.Komga", b =>
{ {
b.HasBaseType("API.Schema.LibraryConnectors.LibraryConnector"); b.HasBaseType("API.Schema.LibraryContext.LibraryConnectors.LibraryConnector");
b.HasDiscriminator().HasValue((byte)0); b.HasDiscriminator().HasValue((byte)0);
}); });

View File

@ -2,7 +2,7 @@
#nullable disable #nullable disable
namespace API.Migrations.library namespace API.Migrations.Library
{ {
/// <inheritdoc /> /// <inheritdoc />
public partial class Initial : Migration public partial class Initial : Migration
@ -14,14 +14,14 @@ namespace API.Migrations.library
name: "LibraryConnectors", name: "LibraryConnectors",
columns: table => new columns: table => new
{ {
LibraryConnectorId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false), Key = table.Column<string>(type: "text", nullable: false),
LibraryType = table.Column<byte>(type: "smallint", nullable: false), LibraryType = table.Column<byte>(type: "smallint", nullable: false),
BaseUrl = table.Column<string>(type: "character varying(256)", maxLength: 256, 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) Auth = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false)
}, },
constraints: table => constraints: table =>
{ {
table.PrimaryKey("PK_LibraryConnectors", x => x.LibraryConnectorId); table.PrimaryKey("PK_LibraryConnectors", x => x.Key);
}); });
} }

View File

@ -1,5 +1,5 @@
// <auto-generated /> // <auto-generated />
using API.Schema.Contexts; using API.Schema.LibraryContext;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
@ -7,7 +7,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable #nullable disable
namespace API.Migrations.library namespace API.Migrations.Library
{ {
[DbContext(typeof(LibraryContext))] [DbContext(typeof(LibraryContext))]
partial class LibraryContextModelSnapshot : ModelSnapshot partial class LibraryContextModelSnapshot : ModelSnapshot
@ -16,16 +16,15 @@ namespace API.Migrations.library
{ {
#pragma warning disable 612, 618 #pragma warning disable 612, 618
modelBuilder modelBuilder
.HasAnnotation("ProductVersion", "9.0.3") .HasAnnotation("ProductVersion", "9.0.5")
.HasAnnotation("Relational:MaxIdentifierLength", 63); .HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("API.Schema.LibraryConnectors.LibraryConnector", b => modelBuilder.Entity("API.Schema.LibraryContext.LibraryConnectors.LibraryConnector", b =>
{ {
b.Property<string>("LibraryConnectorId") b.Property<string>("Key")
.HasMaxLength(64) .HasColumnType("text");
.HasColumnType("character varying(64)");
b.Property<string>("Auth") b.Property<string>("Auth")
.IsRequired() .IsRequired()
@ -40,7 +39,7 @@ namespace API.Migrations.library
b.Property<byte>("LibraryType") b.Property<byte>("LibraryType")
.HasColumnType("smallint"); .HasColumnType("smallint");
b.HasKey("LibraryConnectorId"); b.HasKey("Key");
b.ToTable("LibraryConnectors"); b.ToTable("LibraryConnectors");
@ -49,16 +48,16 @@ namespace API.Migrations.library
b.UseTphMappingStrategy(); b.UseTphMappingStrategy();
}); });
modelBuilder.Entity("API.Schema.LibraryConnectors.Kavita", b => modelBuilder.Entity("API.Schema.LibraryContext.LibraryConnectors.Kavita", b =>
{ {
b.HasBaseType("API.Schema.LibraryConnectors.LibraryConnector"); b.HasBaseType("API.Schema.LibraryContext.LibraryConnectors.LibraryConnector");
b.HasDiscriminator().HasValue((byte)1); b.HasDiscriminator().HasValue((byte)1);
}); });
modelBuilder.Entity("API.Schema.LibraryConnectors.Komga", b => modelBuilder.Entity("API.Schema.LibraryContext.LibraryConnectors.Komga", b =>
{ {
b.HasBaseType("API.Schema.LibraryConnectors.LibraryConnector"); b.HasBaseType("API.Schema.LibraryContext.LibraryConnectors.LibraryConnector");
b.HasDiscriminator().HasValue((byte)0); b.HasDiscriminator().HasValue((byte)0);
}); });

View File

@ -1,6 +1,5 @@
// <auto-generated /> // <auto-generated />
using System; using API.Schema.MangaContext;
using API.Schema.Contexts;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Migrations;
@ -9,11 +8,11 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable #nullable disable
namespace API.Migrations.pgsql namespace API.Migrations.Manga
{ {
[DbContext(typeof(PgsqlContext))] [DbContext(typeof(MangaContext))]
[Migration("20250630182650_OofV2.1")] [Migration("20250703192023_Initial")]
partial class OofV21 partial class Initial
{ {
/// <inheritdoc /> /// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder) protected override void BuildTargetModel(ModelBuilder modelBuilder)
@ -25,7 +24,7 @@ namespace API.Migrations.pgsql
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("API.Schema.Author", b => modelBuilder.Entity("API.Schema.MangaContext.Author", b =>
{ {
b.Property<string>("Key") b.Property<string>("Key")
.HasColumnType("text"); .HasColumnType("text");
@ -40,7 +39,7 @@ namespace API.Migrations.pgsql
b.ToTable("Authors"); b.ToTable("Authors");
}); });
modelBuilder.Entity("API.Schema.Chapter", b => modelBuilder.Entity("API.Schema.MangaContext.Chapter", b =>
{ {
b.Property<string>("Key") b.Property<string>("Key")
.HasColumnType("text"); .HasColumnType("text");
@ -77,7 +76,7 @@ namespace API.Migrations.pgsql
b.ToTable("Chapters"); b.ToTable("Chapters");
}); });
modelBuilder.Entity("API.Schema.FileLibrary", b => modelBuilder.Entity("API.Schema.MangaContext.FileLibrary", b =>
{ {
b.Property<string>("Key") b.Property<string>("Key")
.HasColumnType("text"); .HasColumnType("text");
@ -94,45 +93,10 @@ namespace API.Migrations.pgsql
b.HasKey("Key"); b.HasKey("Key");
b.ToTable("LocalLibraries"); b.ToTable("FileLibraries");
}); });
modelBuilder.Entity("API.Schema.Jobs.Job", b => modelBuilder.Entity("API.Schema.MangaContext.Manga", b =>
{
b.Property<string>("Key")
.HasColumnType("text");
b.Property<bool>("Enabled")
.HasColumnType("boolean");
b.Property<byte>("JobType")
.HasColumnType("smallint");
b.Property<DateTime>("LastExecution")
.HasColumnType("timestamp with time zone");
b.Property<string>("ParentJobId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<decimal>("RecurrenceMs")
.HasColumnType("numeric(20,0)");
b.Property<byte>("state")
.HasColumnType("smallint");
b.HasKey("Key");
b.HasIndex("ParentJobId");
b.ToTable("Jobs");
b.HasDiscriminator<byte>("JobType");
b.UseTphMappingStrategy();
});
modelBuilder.Entity("API.Schema.Manga", b =>
{ {
b.Property<string>("Key") b.Property<string>("Key")
.HasColumnType("text"); .HasColumnType("text");
@ -184,7 +148,7 @@ namespace API.Migrations.pgsql
b.ToTable("Mangas"); b.ToTable("Mangas");
}); });
modelBuilder.Entity("API.Schema.MangaConnectorId<API.Schema.Chapter>", b => modelBuilder.Entity("API.Schema.MangaContext.MangaConnectorId<API.Schema.MangaContext.Chapter>", b =>
{ {
b.Property<string>("Key") b.Property<string>("Key")
.HasColumnType("text"); .HasColumnType("text");
@ -204,6 +168,9 @@ namespace API.Migrations.pgsql
.HasMaxLength(64) .HasMaxLength(64)
.HasColumnType("character varying(64)"); .HasColumnType("character varying(64)");
b.Property<bool>("UseForDownload")
.HasColumnType("boolean");
b.Property<string>("WebsiteUrl") b.Property<string>("WebsiteUrl")
.HasMaxLength(512) .HasMaxLength(512)
.HasColumnType("character varying(512)"); .HasColumnType("character varying(512)");
@ -217,7 +184,7 @@ namespace API.Migrations.pgsql
b.ToTable("MangaConnectorToChapter"); b.ToTable("MangaConnectorToChapter");
}); });
modelBuilder.Entity("API.Schema.MangaConnectorId<API.Schema.Manga>", b => modelBuilder.Entity("API.Schema.MangaContext.MangaConnectorId<API.Schema.MangaContext.Manga>", b =>
{ {
b.Property<string>("Key") b.Property<string>("Key")
.HasColumnType("text"); .HasColumnType("text");
@ -237,6 +204,9 @@ namespace API.Migrations.pgsql
.HasMaxLength(64) .HasMaxLength(64)
.HasColumnType("character varying(64)"); .HasColumnType("character varying(64)");
b.Property<bool>("UseForDownload")
.HasColumnType("boolean");
b.Property<string>("WebsiteUrl") b.Property<string>("WebsiteUrl")
.HasMaxLength(512) .HasMaxLength(512)
.HasColumnType("character varying(512)"); .HasColumnType("character varying(512)");
@ -250,7 +220,7 @@ namespace API.Migrations.pgsql
b.ToTable("MangaConnectorToManga"); b.ToTable("MangaConnectorToManga");
}); });
modelBuilder.Entity("API.Schema.MangaConnectors.MangaConnector", b => modelBuilder.Entity("API.Schema.MangaContext.MangaConnectors.MangaConnector", b =>
{ {
b.Property<string>("Name") b.Property<string>("Name")
.HasMaxLength(32) .HasMaxLength(32)
@ -283,7 +253,7 @@ namespace API.Migrations.pgsql
b.UseTphMappingStrategy(); b.UseTphMappingStrategy();
}); });
modelBuilder.Entity("API.Schema.MangaTag", b => modelBuilder.Entity("API.Schema.MangaContext.MangaTag", b =>
{ {
b.Property<string>("Tag") b.Property<string>("Tag")
.HasMaxLength(64) .HasMaxLength(64)
@ -294,6 +264,44 @@ namespace API.Migrations.pgsql
b.ToTable("Tags"); 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 => modelBuilder.Entity("AuthorToManga", b =>
{ {
b.Property<string>("AuthorIds") b.Property<string>("AuthorIds")
@ -309,21 +317,6 @@ namespace API.Migrations.pgsql
b.ToTable("AuthorToManga"); b.ToTable("AuthorToManga");
}); });
modelBuilder.Entity("JobJob", b =>
{
b.Property<string>("DependsOnJobsKey")
.HasColumnType("text");
b.Property<string>("JobKey")
.HasColumnType("text");
b.HasKey("DependsOnJobsKey", "JobKey");
b.HasIndex("JobKey");
b.ToTable("JobJob");
});
modelBuilder.Entity("MangaTagToManga", b => modelBuilder.Entity("MangaTagToManga", b =>
{ {
b.Property<string>("MangaTagIds") b.Property<string>("MangaTagIds")
@ -339,187 +332,37 @@ namespace API.Migrations.pgsql
b.ToTable("MangaTagToManga"); b.ToTable("MangaTagToManga");
}); });
modelBuilder.Entity("API.Schema.Jobs.DownloadAvailableChaptersJob", b => modelBuilder.Entity("API.Schema.MangaContext.MangaConnectors.ComickIo", b =>
{ {
b.HasBaseType("API.Schema.Jobs.Job"); b.HasBaseType("API.Schema.MangaContext.MangaConnectors.MangaConnector");
b.Property<string>("MangaId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.HasIndex("MangaId");
b.ToTable("Jobs", t =>
{
t.Property("MangaId")
.HasColumnName("DownloadAvailableChaptersJob_MangaId");
});
b.HasDiscriminator().HasValue((byte)1);
});
modelBuilder.Entity("API.Schema.Jobs.DownloadMangaCoverJob", b =>
{
b.HasBaseType("API.Schema.Jobs.Job");
b.Property<string>("MangaId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.HasIndex("MangaId");
b.HasDiscriminator().HasValue((byte)4);
});
modelBuilder.Entity("API.Schema.Jobs.DownloadSingleChapterJob", b =>
{
b.HasBaseType("API.Schema.Jobs.Job");
b.Property<string>("ChapterId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.HasIndex("ChapterId");
b.HasDiscriminator().HasValue((byte)0);
});
modelBuilder.Entity("API.Schema.Jobs.MoveFileOrFolderJob", b =>
{
b.HasBaseType("API.Schema.Jobs.Job");
b.Property<string>("FromLocation")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("ToLocation")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.HasDiscriminator().HasValue((byte)3);
});
modelBuilder.Entity("API.Schema.Jobs.MoveMangaLibraryJob", b =>
{
b.HasBaseType("API.Schema.Jobs.Job");
b.Property<string>("MangaId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("ToLibraryId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.HasIndex("MangaId");
b.HasIndex("ToLibraryId");
b.ToTable("Jobs", t =>
{
t.Property("MangaId")
.HasColumnName("MoveMangaLibraryJob_MangaId");
});
b.HasDiscriminator().HasValue((byte)7);
});
modelBuilder.Entity("API.Schema.Jobs.RetrieveChaptersJob", b =>
{
b.HasBaseType("API.Schema.Jobs.Job");
b.Property<string>("Language")
.IsRequired()
.HasMaxLength(8)
.HasColumnType("character varying(8)");
b.Property<string>("MangaId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.HasIndex("MangaId");
b.ToTable("Jobs", t =>
{
t.Property("MangaId")
.HasColumnName("RetrieveChaptersJob_MangaId");
});
b.HasDiscriminator().HasValue((byte)5);
});
modelBuilder.Entity("API.Schema.Jobs.UpdateChaptersDownloadedJob", b =>
{
b.HasBaseType("API.Schema.Jobs.Job");
b.Property<string>("MangaId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.HasIndex("MangaId");
b.ToTable("Jobs", t =>
{
t.Property("MangaId")
.HasColumnName("UpdateChaptersDownloadedJob_MangaId");
});
b.HasDiscriminator().HasValue((byte)6);
});
modelBuilder.Entity("API.Schema.Jobs.UpdateCoverJob", b =>
{
b.HasBaseType("API.Schema.Jobs.Job");
b.Property<string>("MangaId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.HasIndex("MangaId");
b.ToTable("Jobs", t =>
{
t.Property("MangaId")
.HasColumnName("UpdateCoverJob_MangaId");
});
b.HasDiscriminator().HasValue((byte)9);
});
modelBuilder.Entity("API.Schema.MangaConnectors.ComickIo", b =>
{
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
b.HasDiscriminator().HasValue("ComickIo"); b.HasDiscriminator().HasValue("ComickIo");
}); });
modelBuilder.Entity("API.Schema.MangaConnectors.Global", b => modelBuilder.Entity("API.Schema.MangaContext.MangaConnectors.Global", b =>
{ {
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector"); b.HasBaseType("API.Schema.MangaContext.MangaConnectors.MangaConnector");
b.HasDiscriminator().HasValue("Global"); b.HasDiscriminator().HasValue("Global");
}); });
modelBuilder.Entity("API.Schema.MangaConnectors.MangaDex", b => modelBuilder.Entity("API.Schema.MangaContext.MangaConnectors.MangaDex", b =>
{ {
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector"); b.HasBaseType("API.Schema.MangaContext.MangaConnectors.MangaConnector");
b.HasDiscriminator().HasValue("MangaDex"); b.HasDiscriminator().HasValue("MangaDex");
}); });
modelBuilder.Entity("API.Schema.Chapter", b => modelBuilder.Entity("API.Schema.MangaContext.MetadataFetchers.MyAnimeList", b =>
{ {
b.HasOne("API.Schema.Manga", "ParentManga") 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") .WithMany("Chapters")
.HasForeignKey("ParentMangaId") .HasForeignKey("ParentMangaId")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
@ -528,24 +371,14 @@ namespace API.Migrations.pgsql
b.Navigation("ParentManga"); b.Navigation("ParentManga");
}); });
modelBuilder.Entity("API.Schema.Jobs.Job", b => modelBuilder.Entity("API.Schema.MangaContext.Manga", b =>
{ {
b.HasOne("API.Schema.Jobs.Job", "ParentJob") b.HasOne("API.Schema.MangaContext.FileLibrary", "Library")
.WithMany()
.HasForeignKey("ParentJobId")
.OnDelete(DeleteBehavior.Cascade);
b.Navigation("ParentJob");
});
modelBuilder.Entity("API.Schema.Manga", b =>
{
b.HasOne("API.Schema.FileLibrary", "Library")
.WithMany() .WithMany()
.HasForeignKey("LibraryId") .HasForeignKey("LibraryId")
.OnDelete(DeleteBehavior.SetNull); .OnDelete(DeleteBehavior.SetNull);
b.OwnsMany("API.Schema.AltTitle", "AltTitles", b1 => b.OwnsMany("API.Schema.MangaContext.AltTitle", "AltTitles", b1 =>
{ {
b1.Property<string>("Key") b1.Property<string>("Key")
.HasColumnType("text"); .HasColumnType("text");
@ -574,7 +407,7 @@ namespace API.Migrations.pgsql
.HasForeignKey("MangaKey"); .HasForeignKey("MangaKey");
}); });
b.OwnsMany("API.Schema.Link", "Links", b1 => b.OwnsMany("API.Schema.MangaContext.Link", "Links", b1 =>
{ {
b1.Property<string>("Key") b1.Property<string>("Key")
.HasColumnType("text"); .HasColumnType("text");
@ -610,15 +443,15 @@ namespace API.Migrations.pgsql
b.Navigation("Links"); b.Navigation("Links");
}); });
modelBuilder.Entity("API.Schema.MangaConnectorId<API.Schema.Chapter>", b => modelBuilder.Entity("API.Schema.MangaContext.MangaConnectorId<API.Schema.MangaContext.Chapter>", b =>
{ {
b.HasOne("API.Schema.MangaConnectors.MangaConnector", "MangaConnector") b.HasOne("API.Schema.MangaContext.MangaConnectors.MangaConnector", "MangaConnector")
.WithMany() .WithMany()
.HasForeignKey("MangaConnectorName") .HasForeignKey("MangaConnectorName")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
b.HasOne("API.Schema.Chapter", "Obj") b.HasOne("API.Schema.MangaContext.Chapter", "Obj")
.WithMany("MangaConnectorIds") .WithMany("MangaConnectorIds")
.HasForeignKey("ObjId") .HasForeignKey("ObjId")
.OnDelete(DeleteBehavior.NoAction) .OnDelete(DeleteBehavior.NoAction)
@ -629,15 +462,15 @@ namespace API.Migrations.pgsql
b.Navigation("Obj"); b.Navigation("Obj");
}); });
modelBuilder.Entity("API.Schema.MangaConnectorId<API.Schema.Manga>", b => modelBuilder.Entity("API.Schema.MangaContext.MangaConnectorId<API.Schema.MangaContext.Manga>", b =>
{ {
b.HasOne("API.Schema.MangaConnectors.MangaConnector", "MangaConnector") b.HasOne("API.Schema.MangaContext.MangaConnectors.MangaConnector", "MangaConnector")
.WithMany() .WithMany()
.HasForeignKey("MangaConnectorName") .HasForeignKey("MangaConnectorName")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
b.HasOne("API.Schema.Manga", "Obj") b.HasOne("API.Schema.MangaContext.Manga", "Obj")
.WithMany("MangaConnectorIds") .WithMany("MangaConnectorIds")
.HasForeignKey("ObjId") .HasForeignKey("ObjId")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
@ -648,142 +481,61 @@ namespace API.Migrations.pgsql
b.Navigation("Obj"); 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 => modelBuilder.Entity("AuthorToManga", b =>
{ {
b.HasOne("API.Schema.Author", null) b.HasOne("API.Schema.MangaContext.Author", null)
.WithMany() .WithMany()
.HasForeignKey("AuthorIds") .HasForeignKey("AuthorIds")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
b.HasOne("API.Schema.Manga", null) b.HasOne("API.Schema.MangaContext.Manga", null)
.WithMany() .WithMany()
.HasForeignKey("MangaIds") .HasForeignKey("MangaIds")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
}); });
modelBuilder.Entity("JobJob", b =>
{
b.HasOne("API.Schema.Jobs.Job", null)
.WithMany()
.HasForeignKey("DependsOnJobsKey")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("API.Schema.Jobs.Job", null)
.WithMany()
.HasForeignKey("JobKey")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("MangaTagToManga", b => modelBuilder.Entity("MangaTagToManga", b =>
{ {
b.HasOne("API.Schema.Manga", null) b.HasOne("API.Schema.MangaContext.Manga", null)
.WithMany() .WithMany()
.HasForeignKey("MangaIds") .HasForeignKey("MangaIds")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
b.HasOne("API.Schema.MangaTag", null) b.HasOne("API.Schema.MangaContext.MangaTag", null)
.WithMany() .WithMany()
.HasForeignKey("MangaTagIds") .HasForeignKey("MangaTagIds")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
}); });
modelBuilder.Entity("API.Schema.Jobs.DownloadAvailableChaptersJob", b => modelBuilder.Entity("API.Schema.MangaContext.Chapter", b =>
{
b.HasOne("API.Schema.Manga", "Manga")
.WithMany()
.HasForeignKey("MangaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Manga");
});
modelBuilder.Entity("API.Schema.Jobs.DownloadMangaCoverJob", b =>
{
b.HasOne("API.Schema.Manga", "Manga")
.WithMany()
.HasForeignKey("MangaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Manga");
});
modelBuilder.Entity("API.Schema.Jobs.DownloadSingleChapterJob", b =>
{
b.HasOne("API.Schema.Chapter", "Chapter")
.WithMany()
.HasForeignKey("ChapterId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Chapter");
});
modelBuilder.Entity("API.Schema.Jobs.MoveMangaLibraryJob", b =>
{
b.HasOne("API.Schema.Manga", "Manga")
.WithMany()
.HasForeignKey("MangaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("API.Schema.FileLibrary", "ToFileLibrary")
.WithMany()
.HasForeignKey("ToLibraryId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Manga");
b.Navigation("ToFileLibrary");
});
modelBuilder.Entity("API.Schema.Jobs.RetrieveChaptersJob", b =>
{
b.HasOne("API.Schema.Manga", "Manga")
.WithMany()
.HasForeignKey("MangaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Manga");
});
modelBuilder.Entity("API.Schema.Jobs.UpdateChaptersDownloadedJob", b =>
{
b.HasOne("API.Schema.Manga", "Manga")
.WithMany()
.HasForeignKey("MangaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Manga");
});
modelBuilder.Entity("API.Schema.Jobs.UpdateCoverJob", b =>
{
b.HasOne("API.Schema.Manga", "Manga")
.WithMany()
.HasForeignKey("MangaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Manga");
});
modelBuilder.Entity("API.Schema.Chapter", b =>
{ {
b.Navigation("MangaConnectorIds"); b.Navigation("MangaConnectorIds");
}); });
modelBuilder.Entity("API.Schema.Manga", b => modelBuilder.Entity("API.Schema.MangaContext.Manga", b =>
{ {
b.Navigation("Chapters"); b.Navigation("Chapters");

View File

@ -1,12 +1,11 @@
using System; using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable #nullable disable
namespace API.Migrations.pgsql namespace API.Migrations.Manga
{ {
/// <inheritdoc /> /// <inheritdoc />
public partial class Initial1 : Migration public partial class Initial : Migration
{ {
/// <inheritdoc /> /// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder) protected override void Up(MigrationBuilder migrationBuilder)
@ -15,25 +14,25 @@ namespace API.Migrations.pgsql
name: "Authors", name: "Authors",
columns: table => new columns: table => new
{ {
AuthorId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false), Key = table.Column<string>(type: "text", nullable: false),
AuthorName = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: false) AuthorName = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: false)
}, },
constraints: table => constraints: table =>
{ {
table.PrimaryKey("PK_Authors", x => x.AuthorId); table.PrimaryKey("PK_Authors", x => x.Key);
}); });
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
name: "LocalLibraries", name: "FileLibraries",
columns: table => new columns: table => new
{ {
LocalLibraryId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false), Key = table.Column<string>(type: "text", nullable: false),
BasePath = table.Column<string>(type: "character varying(256)", maxLength: 256, 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) LibraryName = table.Column<string>(type: "character varying(512)", maxLength: 512, nullable: false)
}, },
constraints: table => constraints: table =>
{ {
table.PrimaryKey("PK_LocalLibraries", x => x.LocalLibraryId); table.PrimaryKey("PK_FileLibraries", x => x.Key);
}); });
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
@ -51,6 +50,18 @@ namespace API.Migrations.pgsql
table.PrimaryKey("PK_MangaConnectors", x => x.Name); 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( migrationBuilder.CreateTable(
name: "Tags", name: "Tags",
columns: table => new columns: table => new
@ -66,35 +77,46 @@ namespace API.Migrations.pgsql
name: "Mangas", name: "Mangas",
columns: table => new columns: table => new
{ {
MangaId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false), Key = table.Column<string>(type: "text", nullable: false),
IdOnConnectorSite = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
Name = table.Column<string>(type: "character varying(512)", maxLength: 512, nullable: false), Name = table.Column<string>(type: "character varying(512)", maxLength: 512, nullable: false),
Description = table.Column<string>(type: "text", nullable: false), Description = table.Column<string>(type: "text", nullable: false),
WebsiteUrl = table.Column<string>(type: "character varying(512)", maxLength: 512, nullable: false),
CoverUrl = table.Column<string>(type: "character varying(512)", maxLength: 512, nullable: false), CoverUrl = table.Column<string>(type: "character varying(512)", maxLength: 512, nullable: false),
ReleaseStatus = table.Column<byte>(type: "smallint", nullable: false), ReleaseStatus = table.Column<byte>(type: "smallint", nullable: false),
LibraryId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: true), LibraryId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: true),
MangaConnectorName = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: false),
IgnoreChaptersBefore = table.Column<float>(type: "real", nullable: false), IgnoreChaptersBefore = table.Column<float>(type: "real", nullable: false),
DirectoryName = table.Column<string>(type: "character varying(1024)", maxLength: 1024, 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), CoverFileNameInCache = table.Column<string>(type: "character varying(512)", maxLength: 512, nullable: true),
Year = table.Column<long>(type: "bigint", nullable: false), Year = table.Column<long>(type: "bigint", nullable: true),
OriginalLanguage = table.Column<string>(type: "character varying(8)", maxLength: 8, nullable: true) OriginalLanguage = table.Column<string>(type: "character varying(8)", maxLength: 8, nullable: true)
}, },
constraints: table => constraints: table =>
{ {
table.PrimaryKey("PK_Mangas", x => x.MangaId); table.PrimaryKey("PK_Mangas", x => x.Key);
table.ForeignKey( table.ForeignKey(
name: "FK_Mangas_LocalLibraries_LibraryId", name: "FK_Mangas_FileLibraries_LibraryId",
column: x => x.LibraryId, column: x => x.LibraryId,
principalTable: "LocalLibraries", principalTable: "FileLibraries",
principalColumn: "LocalLibraryId", principalColumn: "Key",
onDelete: ReferentialAction.SetNull); 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( table.ForeignKey(
name: "FK_Mangas_MangaConnectors_MangaConnectorName", name: "FK_AltTitle_Mangas_MangaKey",
column: x => x.MangaConnectorName, column: x => x.MangaKey,
principalTable: "MangaConnectors", principalTable: "Mangas",
principalColumn: "Name", principalColumn: "Key",
onDelete: ReferentialAction.Cascade); onDelete: ReferentialAction.Cascade);
}); });
@ -102,8 +124,8 @@ namespace API.Migrations.pgsql
name: "AuthorToManga", name: "AuthorToManga",
columns: table => new columns: table => new
{ {
AuthorIds = table.Column<string>(type: "character varying(64)", nullable: false), AuthorIds = table.Column<string>(type: "text", nullable: false),
MangaIds = table.Column<string>(type: "character varying(64)", nullable: false) MangaIds = table.Column<string>(type: "text", nullable: false)
}, },
constraints: table => constraints: table =>
{ {
@ -112,13 +134,13 @@ namespace API.Migrations.pgsql
name: "FK_AuthorToManga_Authors_AuthorIds", name: "FK_AuthorToManga_Authors_AuthorIds",
column: x => x.AuthorIds, column: x => x.AuthorIds,
principalTable: "Authors", principalTable: "Authors",
principalColumn: "AuthorId", principalColumn: "Key",
onDelete: ReferentialAction.Cascade); onDelete: ReferentialAction.Cascade);
table.ForeignKey( table.ForeignKey(
name: "FK_AuthorToManga_Mangas_MangaIds", name: "FK_AuthorToManga_Mangas_MangaIds",
column: x => x.MangaIds, column: x => x.MangaIds,
principalTable: "Mangas", principalTable: "Mangas",
principalColumn: "MangaId", principalColumn: "Key",
onDelete: ReferentialAction.Cascade); onDelete: ReferentialAction.Cascade);
}); });
@ -126,23 +148,22 @@ namespace API.Migrations.pgsql
name: "Chapters", name: "Chapters",
columns: table => new columns: table => new
{ {
ChapterId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false), Key = table.Column<string>(type: "text", nullable: false),
ParentMangaId = table.Column<string>(type: "character varying(64)", nullable: false), ParentMangaId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
VolumeNumber = table.Column<int>(type: "integer", nullable: true), VolumeNumber = table.Column<int>(type: "integer", nullable: true),
ChapterNumber = table.Column<string>(type: "character varying(10)", maxLength: 10, nullable: false), ChapterNumber = table.Column<string>(type: "character varying(10)", maxLength: 10, nullable: false),
Url = table.Column<string>(type: "character varying(2048)", maxLength: 2048, nullable: false),
Title = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true), Title = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
FileName = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false), FileName = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
Downloaded = table.Column<bool>(type: "boolean", nullable: false) Downloaded = table.Column<bool>(type: "boolean", nullable: false)
}, },
constraints: table => constraints: table =>
{ {
table.PrimaryKey("PK_Chapters", x => x.ChapterId); table.PrimaryKey("PK_Chapters", x => x.Key);
table.ForeignKey( table.ForeignKey(
name: "FK_Chapters_Mangas_ParentMangaId", name: "FK_Chapters_Mangas_ParentMangaId",
column: x => x.ParentMangaId, column: x => x.ParentMangaId,
principalTable: "Mangas", principalTable: "Mangas",
principalColumn: "MangaId", principalColumn: "Key",
onDelete: ReferentialAction.Cascade); onDelete: ReferentialAction.Cascade);
}); });
@ -150,39 +171,47 @@ namespace API.Migrations.pgsql
name: "Link", name: "Link",
columns: table => new columns: table => new
{ {
LinkId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false), Key = table.Column<string>(type: "text", nullable: false),
LinkProvider = table.Column<string>(type: "character varying(64)", maxLength: 64, 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), LinkUrl = table.Column<string>(type: "character varying(2048)", maxLength: 2048, nullable: false),
MangaId = table.Column<string>(type: "character varying(64)", nullable: false) MangaKey = table.Column<string>(type: "text", nullable: false)
}, },
constraints: table => constraints: table =>
{ {
table.PrimaryKey("PK_Link", x => x.LinkId); table.PrimaryKey("PK_Link", x => x.Key);
table.ForeignKey( table.ForeignKey(
name: "FK_Link_Mangas_MangaId", name: "FK_Link_Mangas_MangaKey",
column: x => x.MangaId, column: x => x.MangaKey,
principalTable: "Mangas", principalTable: "Mangas",
principalColumn: "MangaId", principalColumn: "Key",
onDelete: ReferentialAction.Cascade); onDelete: ReferentialAction.Cascade);
}); });
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
name: "MangaAltTitle", name: "MangaConnectorToManga",
columns: table => new columns: table => new
{ {
AltTitleId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false), Key = table.Column<string>(type: "text", nullable: false),
Language = table.Column<string>(type: "character varying(8)", maxLength: 8, nullable: false), ObjId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
Title = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false), MangaConnectorName = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: false),
MangaId = table.Column<string>(type: "character varying(64)", 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 => constraints: table =>
{ {
table.PrimaryKey("PK_MangaAltTitle", x => x.AltTitleId); table.PrimaryKey("PK_MangaConnectorToManga", x => x.Key);
table.ForeignKey( table.ForeignKey(
name: "FK_MangaAltTitle_Mangas_MangaId", name: "FK_MangaConnectorToManga_MangaConnectors_MangaConnectorName",
column: x => x.MangaId, column: x => x.MangaConnectorName,
principalTable: "MangaConnectors",
principalColumn: "Name",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_MangaConnectorToManga_Mangas_ObjId",
column: x => x.ObjId,
principalTable: "Mangas", principalTable: "Mangas",
principalColumn: "MangaId", principalColumn: "Key",
onDelete: ReferentialAction.Cascade); onDelete: ReferentialAction.Cascade);
}); });
@ -191,7 +220,7 @@ namespace API.Migrations.pgsql
columns: table => new columns: table => new
{ {
MangaTagIds = table.Column<string>(type: "character varying(64)", nullable: false), MangaTagIds = table.Column<string>(type: "character varying(64)", nullable: false),
MangaIds = table.Column<string>(type: "character varying(64)", nullable: false) MangaIds = table.Column<string>(type: "text", nullable: false)
}, },
constraints: table => constraints: table =>
{ {
@ -200,7 +229,7 @@ namespace API.Migrations.pgsql
name: "FK_MangaTagToManga_Mangas_MangaIds", name: "FK_MangaTagToManga_Mangas_MangaIds",
column: x => x.MangaIds, column: x => x.MangaIds,
principalTable: "Mangas", principalTable: "Mangas",
principalColumn: "MangaId", principalColumn: "Key",
onDelete: ReferentialAction.Cascade); onDelete: ReferentialAction.Cascade);
table.ForeignKey( table.ForeignKey(
name: "FK_MangaTagToManga_Tags_MangaTagIds", name: "FK_MangaTagToManga_Tags_MangaTagIds",
@ -211,104 +240,62 @@ namespace API.Migrations.pgsql
}); });
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
name: "Jobs", name: "MetadataEntries",
columns: table => new columns: table => new
{ {
JobId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false), MetadataFetcherName = table.Column<string>(type: "text", nullable: false),
ParentJobId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: true), Identifier = table.Column<string>(type: "text", nullable: false),
JobType = table.Column<byte>(type: "smallint", nullable: false), MangaId = table.Column<string>(type: "text", nullable: false)
RecurrenceMs = table.Column<decimal>(type: "numeric(20,0)", nullable: false),
LastExecution = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
state = table.Column<byte>(type: "smallint", nullable: false),
Enabled = table.Column<bool>(type: "boolean", nullable: false),
DownloadAvailableChaptersJob_MangaId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: true),
MangaId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: true),
ChapterId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: true),
FromLocation = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
ToLocation = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
MoveMangaLibraryJob_MangaId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: true),
ToLibraryId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: true),
RetrieveChaptersJob_MangaId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: true),
Language = table.Column<string>(type: "character varying(8)", maxLength: 8, nullable: true),
UpdateFilesDownloadedJob_MangaId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: true)
}, },
constraints: table => constraints: table =>
{ {
table.PrimaryKey("PK_Jobs", x => x.JobId); table.PrimaryKey("PK_MetadataEntries", x => new { x.MetadataFetcherName, x.Identifier });
table.ForeignKey( table.ForeignKey(
name: "FK_Jobs_Chapters_ChapterId", name: "FK_MetadataEntries_Mangas_MangaId",
column: x => x.ChapterId,
principalTable: "Chapters",
principalColumn: "ChapterId",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_Jobs_Jobs_ParentJobId",
column: x => x.ParentJobId,
principalTable: "Jobs",
principalColumn: "JobId",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_Jobs_LocalLibraries_ToLibraryId",
column: x => x.ToLibraryId,
principalTable: "LocalLibraries",
principalColumn: "LocalLibraryId",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_Jobs_Mangas_DownloadAvailableChaptersJob_MangaId",
column: x => x.DownloadAvailableChaptersJob_MangaId,
principalTable: "Mangas",
principalColumn: "MangaId",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_Jobs_Mangas_MangaId",
column: x => x.MangaId, column: x => x.MangaId,
principalTable: "Mangas", principalTable: "Mangas",
principalColumn: "MangaId", principalColumn: "Key",
onDelete: ReferentialAction.Cascade); onDelete: ReferentialAction.Cascade);
table.ForeignKey( table.ForeignKey(
name: "FK_Jobs_Mangas_MoveMangaLibraryJob_MangaId", name: "FK_MetadataEntries_MetadataFetcher_MetadataFetcherName",
column: x => x.MoveMangaLibraryJob_MangaId, column: x => x.MetadataFetcherName,
principalTable: "Mangas", principalTable: "MetadataFetcher",
principalColumn: "MangaId", principalColumn: "Name",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_Jobs_Mangas_RetrieveChaptersJob_MangaId",
column: x => x.RetrieveChaptersJob_MangaId,
principalTable: "Mangas",
principalColumn: "MangaId",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_Jobs_Mangas_UpdateFilesDownloadedJob_MangaId",
column: x => x.UpdateFilesDownloadedJob_MangaId,
principalTable: "Mangas",
principalColumn: "MangaId",
onDelete: ReferentialAction.Cascade); onDelete: ReferentialAction.Cascade);
}); });
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
name: "JobJob", name: "MangaConnectorToChapter",
columns: table => new columns: table => new
{ {
DependsOnJobsJobId = table.Column<string>(type: "character varying(64)", nullable: false), Key = table.Column<string>(type: "text", nullable: false),
JobId = table.Column<string>(type: "character varying(64)", 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 => constraints: table =>
{ {
table.PrimaryKey("PK_JobJob", x => new { x.DependsOnJobsJobId, x.JobId }); table.PrimaryKey("PK_MangaConnectorToChapter", x => x.Key);
table.ForeignKey( table.ForeignKey(
name: "FK_JobJob_Jobs_DependsOnJobsJobId", name: "FK_MangaConnectorToChapter_Chapters_ObjId",
column: x => x.DependsOnJobsJobId, column: x => x.ObjId,
principalTable: "Jobs", principalTable: "Chapters",
principalColumn: "JobId", principalColumn: "Key");
onDelete: ReferentialAction.Cascade);
table.ForeignKey( table.ForeignKey(
name: "FK_JobJob_Jobs_JobId", name: "FK_MangaConnectorToChapter_MangaConnectors_MangaConnectorName",
column: x => x.JobId, column: x => x.MangaConnectorName,
principalTable: "Jobs", principalTable: "MangaConnectors",
principalColumn: "JobId", principalColumn: "Name",
onDelete: ReferentialAction.Cascade); onDelete: ReferentialAction.Cascade);
}); });
migrationBuilder.CreateIndex(
name: "IX_AltTitle_MangaKey",
table: "AltTitle",
column: "MangaKey");
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_AuthorToManga_MangaIds", name: "IX_AuthorToManga_MangaIds",
table: "AuthorToManga", table: "AuthorToManga",
@ -320,114 +307,90 @@ namespace API.Migrations.pgsql
column: "ParentMangaId"); column: "ParentMangaId");
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_JobJob_JobId", name: "IX_Link_MangaKey",
table: "JobJob",
column: "JobId");
migrationBuilder.CreateIndex(
name: "IX_Jobs_ChapterId",
table: "Jobs",
column: "ChapterId");
migrationBuilder.CreateIndex(
name: "IX_Jobs_DownloadAvailableChaptersJob_MangaId",
table: "Jobs",
column: "DownloadAvailableChaptersJob_MangaId");
migrationBuilder.CreateIndex(
name: "IX_Jobs_MangaId",
table: "Jobs",
column: "MangaId");
migrationBuilder.CreateIndex(
name: "IX_Jobs_MoveMangaLibraryJob_MangaId",
table: "Jobs",
column: "MoveMangaLibraryJob_MangaId");
migrationBuilder.CreateIndex(
name: "IX_Jobs_ParentJobId",
table: "Jobs",
column: "ParentJobId");
migrationBuilder.CreateIndex(
name: "IX_Jobs_RetrieveChaptersJob_MangaId",
table: "Jobs",
column: "RetrieveChaptersJob_MangaId");
migrationBuilder.CreateIndex(
name: "IX_Jobs_ToLibraryId",
table: "Jobs",
column: "ToLibraryId");
migrationBuilder.CreateIndex(
name: "IX_Jobs_UpdateFilesDownloadedJob_MangaId",
table: "Jobs",
column: "UpdateFilesDownloadedJob_MangaId");
migrationBuilder.CreateIndex(
name: "IX_Link_MangaId",
table: "Link", table: "Link",
column: "MangaId"); column: "MangaKey");
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_MangaAltTitle_MangaId", name: "IX_MangaConnectorToChapter_MangaConnectorName",
table: "MangaAltTitle", table: "MangaConnectorToChapter",
column: "MangaId"); 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( migrationBuilder.CreateIndex(
name: "IX_Mangas_LibraryId", name: "IX_Mangas_LibraryId",
table: "Mangas", table: "Mangas",
column: "LibraryId"); column: "LibraryId");
migrationBuilder.CreateIndex(
name: "IX_Mangas_MangaConnectorName",
table: "Mangas",
column: "MangaConnectorName");
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_MangaTagToManga_MangaIds", name: "IX_MangaTagToManga_MangaIds",
table: "MangaTagToManga", table: "MangaTagToManga",
column: "MangaIds"); column: "MangaIds");
migrationBuilder.CreateIndex(
name: "IX_MetadataEntries_MangaId",
table: "MetadataEntries",
column: "MangaId");
} }
/// <inheritdoc /> /// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder) protected override void Down(MigrationBuilder migrationBuilder)
{ {
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "AuthorToManga"); name: "AltTitle");
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "JobJob"); name: "AuthorToManga");
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "Link"); name: "Link");
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "MangaAltTitle"); name: "MangaConnectorToChapter");
migrationBuilder.DropTable(
name: "MangaConnectorToManga");
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "MangaTagToManga"); name: "MangaTagToManga");
migrationBuilder.DropTable(
name: "MetadataEntries");
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "Authors"); name: "Authors");
migrationBuilder.DropTable(
name: "Jobs");
migrationBuilder.DropTable(
name: "Tags");
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "Chapters"); name: "Chapters");
migrationBuilder.DropTable(
name: "MangaConnectors");
migrationBuilder.DropTable(
name: "Tags");
migrationBuilder.DropTable(
name: "MetadataFetcher");
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "Mangas"); name: "Mangas");
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "LocalLibraries"); name: "FileLibraries");
migrationBuilder.DropTable(
name: "MangaConnectors");
} }
} }
} }

View File

@ -1,6 +1,5 @@
// <auto-generated /> // <auto-generated />
using System; using API.Schema.MangaContext;
using API.Schema.Contexts;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
@ -8,10 +7,10 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable #nullable disable
namespace API.Migrations.pgsql namespace API.Migrations.Manga
{ {
[DbContext(typeof(PgsqlContext))] [DbContext(typeof(MangaContext))]
partial class PgsqlContextModelSnapshot : ModelSnapshot partial class MangaContextModelSnapshot : ModelSnapshot
{ {
protected override void BuildModel(ModelBuilder modelBuilder) protected override void BuildModel(ModelBuilder modelBuilder)
{ {
@ -22,7 +21,7 @@ namespace API.Migrations.pgsql
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("API.Schema.Author", b => modelBuilder.Entity("API.Schema.MangaContext.Author", b =>
{ {
b.Property<string>("Key") b.Property<string>("Key")
.HasColumnType("text"); .HasColumnType("text");
@ -37,7 +36,7 @@ namespace API.Migrations.pgsql
b.ToTable("Authors"); b.ToTable("Authors");
}); });
modelBuilder.Entity("API.Schema.Chapter", b => modelBuilder.Entity("API.Schema.MangaContext.Chapter", b =>
{ {
b.Property<string>("Key") b.Property<string>("Key")
.HasColumnType("text"); .HasColumnType("text");
@ -74,7 +73,7 @@ namespace API.Migrations.pgsql
b.ToTable("Chapters"); b.ToTable("Chapters");
}); });
modelBuilder.Entity("API.Schema.FileLibrary", b => modelBuilder.Entity("API.Schema.MangaContext.FileLibrary", b =>
{ {
b.Property<string>("Key") b.Property<string>("Key")
.HasColumnType("text"); .HasColumnType("text");
@ -91,45 +90,10 @@ namespace API.Migrations.pgsql
b.HasKey("Key"); b.HasKey("Key");
b.ToTable("LocalLibraries"); b.ToTable("FileLibraries");
}); });
modelBuilder.Entity("API.Schema.Jobs.Job", b => modelBuilder.Entity("API.Schema.MangaContext.Manga", b =>
{
b.Property<string>("Key")
.HasColumnType("text");
b.Property<bool>("Enabled")
.HasColumnType("boolean");
b.Property<byte>("JobType")
.HasColumnType("smallint");
b.Property<DateTime>("LastExecution")
.HasColumnType("timestamp with time zone");
b.Property<string>("ParentJobId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<decimal>("RecurrenceMs")
.HasColumnType("numeric(20,0)");
b.Property<byte>("state")
.HasColumnType("smallint");
b.HasKey("Key");
b.HasIndex("ParentJobId");
b.ToTable("Jobs");
b.HasDiscriminator<byte>("JobType");
b.UseTphMappingStrategy();
});
modelBuilder.Entity("API.Schema.Manga", b =>
{ {
b.Property<string>("Key") b.Property<string>("Key")
.HasColumnType("text"); .HasColumnType("text");
@ -181,7 +145,7 @@ namespace API.Migrations.pgsql
b.ToTable("Mangas"); b.ToTable("Mangas");
}); });
modelBuilder.Entity("API.Schema.MangaConnectorId<API.Schema.Chapter>", b => modelBuilder.Entity("API.Schema.MangaContext.MangaConnectorId<API.Schema.MangaContext.Chapter>", b =>
{ {
b.Property<string>("Key") b.Property<string>("Key")
.HasColumnType("text"); .HasColumnType("text");
@ -201,6 +165,9 @@ namespace API.Migrations.pgsql
.HasMaxLength(64) .HasMaxLength(64)
.HasColumnType("character varying(64)"); .HasColumnType("character varying(64)");
b.Property<bool>("UseForDownload")
.HasColumnType("boolean");
b.Property<string>("WebsiteUrl") b.Property<string>("WebsiteUrl")
.HasMaxLength(512) .HasMaxLength(512)
.HasColumnType("character varying(512)"); .HasColumnType("character varying(512)");
@ -214,7 +181,7 @@ namespace API.Migrations.pgsql
b.ToTable("MangaConnectorToChapter"); b.ToTable("MangaConnectorToChapter");
}); });
modelBuilder.Entity("API.Schema.MangaConnectorId<API.Schema.Manga>", b => modelBuilder.Entity("API.Schema.MangaContext.MangaConnectorId<API.Schema.MangaContext.Manga>", b =>
{ {
b.Property<string>("Key") b.Property<string>("Key")
.HasColumnType("text"); .HasColumnType("text");
@ -234,6 +201,9 @@ namespace API.Migrations.pgsql
.HasMaxLength(64) .HasMaxLength(64)
.HasColumnType("character varying(64)"); .HasColumnType("character varying(64)");
b.Property<bool>("UseForDownload")
.HasColumnType("boolean");
b.Property<string>("WebsiteUrl") b.Property<string>("WebsiteUrl")
.HasMaxLength(512) .HasMaxLength(512)
.HasColumnType("character varying(512)"); .HasColumnType("character varying(512)");
@ -247,7 +217,7 @@ namespace API.Migrations.pgsql
b.ToTable("MangaConnectorToManga"); b.ToTable("MangaConnectorToManga");
}); });
modelBuilder.Entity("API.Schema.MangaConnectors.MangaConnector", b => modelBuilder.Entity("API.Schema.MangaContext.MangaConnectors.MangaConnector", b =>
{ {
b.Property<string>("Name") b.Property<string>("Name")
.HasMaxLength(32) .HasMaxLength(32)
@ -280,7 +250,7 @@ namespace API.Migrations.pgsql
b.UseTphMappingStrategy(); b.UseTphMappingStrategy();
}); });
modelBuilder.Entity("API.Schema.MangaTag", b => modelBuilder.Entity("API.Schema.MangaContext.MangaTag", b =>
{ {
b.Property<string>("Tag") b.Property<string>("Tag")
.HasMaxLength(64) .HasMaxLength(64)
@ -291,7 +261,7 @@ namespace API.Migrations.pgsql
b.ToTable("Tags"); b.ToTable("Tags");
}); });
modelBuilder.Entity("API.Schema.MetadataFetchers.MetadataEntry", b => modelBuilder.Entity("API.Schema.MangaContext.MetadataFetchers.MetadataEntry", b =>
{ {
b.Property<string>("MetadataFetcherName") b.Property<string>("MetadataFetcherName")
.HasColumnType("text"); .HasColumnType("text");
@ -301,7 +271,7 @@ namespace API.Migrations.pgsql
b.Property<string>("MangaId") b.Property<string>("MangaId")
.IsRequired() .IsRequired()
.HasColumnType("character varying(64)"); .HasColumnType("text");
b.HasKey("MetadataFetcherName", "Identifier"); b.HasKey("MetadataFetcherName", "Identifier");
@ -310,9 +280,9 @@ namespace API.Migrations.pgsql
b.ToTable("MetadataEntries"); b.ToTable("MetadataEntries");
}); });
modelBuilder.Entity("API.Schema.MetadataFetchers.MetadataFetcher", b => modelBuilder.Entity("API.Schema.MangaContext.MetadataFetchers.MetadataFetcher", b =>
{ {
b.Property<string>("MetadataFetcherName") b.Property<string>("Name")
.HasColumnType("text"); .HasColumnType("text");
b.Property<string>("MetadataEntry") b.Property<string>("MetadataEntry")
@ -320,7 +290,7 @@ namespace API.Migrations.pgsql
.HasMaxLength(21) .HasMaxLength(21)
.HasColumnType("character varying(21)"); .HasColumnType("character varying(21)");
b.HasKey("MetadataFetcherName"); b.HasKey("Name");
b.ToTable("MetadataFetcher"); b.ToTable("MetadataFetcher");
@ -344,21 +314,6 @@ namespace API.Migrations.pgsql
b.ToTable("AuthorToManga"); b.ToTable("AuthorToManga");
}); });
modelBuilder.Entity("JobJob", b =>
{
b.Property<string>("DependsOnJobsKey")
.HasColumnType("text");
b.Property<string>("JobKey")
.HasColumnType("text");
b.HasKey("DependsOnJobsKey", "JobKey");
b.HasIndex("JobKey");
b.ToTable("JobJob");
});
modelBuilder.Entity("MangaTagToManga", b => modelBuilder.Entity("MangaTagToManga", b =>
{ {
b.Property<string>("MangaTagIds") b.Property<string>("MangaTagIds")
@ -374,194 +329,37 @@ namespace API.Migrations.pgsql
b.ToTable("MangaTagToManga"); b.ToTable("MangaTagToManga");
}); });
modelBuilder.Entity("API.Schema.Jobs.DownloadAvailableChaptersJob", b => modelBuilder.Entity("API.Schema.MangaContext.MangaConnectors.ComickIo", b =>
{ {
b.HasBaseType("API.Schema.Jobs.Job"); b.HasBaseType("API.Schema.MangaContext.MangaConnectors.MangaConnector");
b.Property<string>("MangaId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.HasIndex("MangaId");
b.ToTable("Jobs", t =>
{
t.Property("MangaId")
.HasColumnName("DownloadAvailableChaptersJob_MangaId");
});
b.HasDiscriminator().HasValue((byte)1);
});
modelBuilder.Entity("API.Schema.Jobs.DownloadMangaCoverJob", b =>
{
b.HasBaseType("API.Schema.Jobs.Job");
b.Property<string>("MangaId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.HasIndex("MangaId");
b.HasDiscriminator().HasValue((byte)4);
});
modelBuilder.Entity("API.Schema.Jobs.DownloadSingleChapterJob", b =>
{
b.HasBaseType("API.Schema.Jobs.Job");
b.Property<string>("ChapterId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.HasIndex("ChapterId");
b.HasDiscriminator().HasValue((byte)0);
});
modelBuilder.Entity("API.Schema.Jobs.MoveFileOrFolderJob", b =>
{
b.HasBaseType("API.Schema.Jobs.Job");
b.Property<string>("FromLocation")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("ToLocation")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.HasDiscriminator().HasValue((byte)3);
});
modelBuilder.Entity("API.Schema.Jobs.MoveMangaLibraryJob", b =>
{
b.HasBaseType("API.Schema.Jobs.Job");
b.Property<string>("MangaId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("ToLibraryId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.HasIndex("MangaId");
b.HasIndex("ToLibraryId");
b.ToTable("Jobs", t =>
{
t.Property("MangaId")
.HasColumnName("MoveMangaLibraryJob_MangaId");
});
b.HasDiscriminator().HasValue((byte)7);
});
modelBuilder.Entity("API.Schema.Jobs.RetrieveChaptersJob", b =>
{
b.HasBaseType("API.Schema.Jobs.Job");
b.Property<string>("Language")
.IsRequired()
.HasMaxLength(8)
.HasColumnType("character varying(8)");
b.Property<string>("MangaId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.HasIndex("MangaId");
b.ToTable("Jobs", t =>
{
t.Property("MangaId")
.HasColumnName("RetrieveChaptersJob_MangaId");
});
b.HasDiscriminator().HasValue((byte)5);
});
modelBuilder.Entity("API.Schema.Jobs.UpdateChaptersDownloadedJob", b =>
{
b.HasBaseType("API.Schema.Jobs.Job");
b.Property<string>("MangaId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.HasIndex("MangaId");
b.ToTable("Jobs", t =>
{
t.Property("MangaId")
.HasColumnName("UpdateChaptersDownloadedJob_MangaId");
});
b.HasDiscriminator().HasValue((byte)6);
});
modelBuilder.Entity("API.Schema.Jobs.UpdateCoverJob", b =>
{
b.HasBaseType("API.Schema.Jobs.Job");
b.Property<string>("MangaId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.HasIndex("MangaId");
b.ToTable("Jobs", t =>
{
t.Property("MangaId")
.HasColumnName("UpdateCoverJob_MangaId");
});
b.HasDiscriminator().HasValue((byte)9);
});
modelBuilder.Entity("API.Schema.MangaConnectors.ComickIo", b =>
{
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
b.HasDiscriminator().HasValue("ComickIo"); b.HasDiscriminator().HasValue("ComickIo");
}); });
modelBuilder.Entity("API.Schema.MangaConnectors.Global", b => modelBuilder.Entity("API.Schema.MangaContext.MangaConnectors.Global", b =>
{ {
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector"); b.HasBaseType("API.Schema.MangaContext.MangaConnectors.MangaConnector");
b.HasDiscriminator().HasValue("Global"); b.HasDiscriminator().HasValue("Global");
}); });
modelBuilder.Entity("API.Schema.MangaConnectors.MangaDex", b => modelBuilder.Entity("API.Schema.MangaContext.MangaConnectors.MangaDex", b =>
{ {
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector"); b.HasBaseType("API.Schema.MangaContext.MangaConnectors.MangaConnector");
b.HasDiscriminator().HasValue("MangaDex"); b.HasDiscriminator().HasValue("MangaDex");
}); });
modelBuilder.Entity("API.Schema.MetadataFetchers.MyAnimeList", b => modelBuilder.Entity("API.Schema.MangaContext.MetadataFetchers.MyAnimeList", b =>
{ {
b.HasBaseType("API.Schema.MetadataFetchers.MetadataFetcher"); b.HasBaseType("API.Schema.MangaContext.MetadataFetchers.MetadataFetcher");
b.HasDiscriminator().HasValue("MyAnimeList"); b.HasDiscriminator().HasValue("MyAnimeList");
}); });
modelBuilder.Entity("API.Schema.Chapter", b => modelBuilder.Entity("API.Schema.MangaContext.Chapter", b =>
{ {
b.HasOne("API.Schema.Manga", "ParentManga") b.HasOne("API.Schema.MangaContext.Manga", "ParentManga")
.WithMany("Chapters") .WithMany("Chapters")
.HasForeignKey("ParentMangaId") .HasForeignKey("ParentMangaId")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
@ -570,24 +368,14 @@ namespace API.Migrations.pgsql
b.Navigation("ParentManga"); b.Navigation("ParentManga");
}); });
modelBuilder.Entity("API.Schema.Jobs.Job", b => modelBuilder.Entity("API.Schema.MangaContext.Manga", b =>
{ {
b.HasOne("API.Schema.Jobs.Job", "ParentJob") b.HasOne("API.Schema.MangaContext.FileLibrary", "Library")
.WithMany()
.HasForeignKey("ParentJobId")
.OnDelete(DeleteBehavior.Cascade);
b.Navigation("ParentJob");
});
modelBuilder.Entity("API.Schema.Manga", b =>
{
b.HasOne("API.Schema.FileLibrary", "Library")
.WithMany() .WithMany()
.HasForeignKey("LibraryId") .HasForeignKey("LibraryId")
.OnDelete(DeleteBehavior.SetNull); .OnDelete(DeleteBehavior.SetNull);
b.OwnsMany("API.Schema.AltTitle", "AltTitles", b1 => b.OwnsMany("API.Schema.MangaContext.AltTitle", "AltTitles", b1 =>
{ {
b1.Property<string>("Key") b1.Property<string>("Key")
.HasColumnType("text"); .HasColumnType("text");
@ -616,7 +404,7 @@ namespace API.Migrations.pgsql
.HasForeignKey("MangaKey"); .HasForeignKey("MangaKey");
}); });
b.OwnsMany("API.Schema.Link", "Links", b1 => b.OwnsMany("API.Schema.MangaContext.Link", "Links", b1 =>
{ {
b1.Property<string>("Key") b1.Property<string>("Key")
.HasColumnType("text"); .HasColumnType("text");
@ -652,15 +440,15 @@ namespace API.Migrations.pgsql
b.Navigation("Links"); b.Navigation("Links");
}); });
modelBuilder.Entity("API.Schema.MangaConnectorId<API.Schema.Chapter>", b => modelBuilder.Entity("API.Schema.MangaContext.MangaConnectorId<API.Schema.MangaContext.Chapter>", b =>
{ {
b.HasOne("API.Schema.MangaConnectors.MangaConnector", "MangaConnector") b.HasOne("API.Schema.MangaContext.MangaConnectors.MangaConnector", "MangaConnector")
.WithMany() .WithMany()
.HasForeignKey("MangaConnectorName") .HasForeignKey("MangaConnectorName")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
b.HasOne("API.Schema.Chapter", "Obj") b.HasOne("API.Schema.MangaContext.Chapter", "Obj")
.WithMany("MangaConnectorIds") .WithMany("MangaConnectorIds")
.HasForeignKey("ObjId") .HasForeignKey("ObjId")
.OnDelete(DeleteBehavior.NoAction) .OnDelete(DeleteBehavior.NoAction)
@ -671,15 +459,15 @@ namespace API.Migrations.pgsql
b.Navigation("Obj"); b.Navigation("Obj");
}); });
modelBuilder.Entity("API.Schema.MangaConnectorId<API.Schema.Manga>", b => modelBuilder.Entity("API.Schema.MangaContext.MangaConnectorId<API.Schema.MangaContext.Manga>", b =>
{ {
b.HasOne("API.Schema.MangaConnectors.MangaConnector", "MangaConnector") b.HasOne("API.Schema.MangaContext.MangaConnectors.MangaConnector", "MangaConnector")
.WithMany() .WithMany()
.HasForeignKey("MangaConnectorName") .HasForeignKey("MangaConnectorName")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
b.HasOne("API.Schema.Manga", "Obj") b.HasOne("API.Schema.MangaContext.Manga", "Obj")
.WithMany("MangaConnectorIds") .WithMany("MangaConnectorIds")
.HasForeignKey("ObjId") .HasForeignKey("ObjId")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
@ -690,15 +478,15 @@ namespace API.Migrations.pgsql
b.Navigation("Obj"); b.Navigation("Obj");
}); });
modelBuilder.Entity("API.Schema.MetadataFetchers.MetadataEntry", b => modelBuilder.Entity("API.Schema.MangaContext.MetadataFetchers.MetadataEntry", b =>
{ {
b.HasOne("API.Schema.Manga", "Manga") b.HasOne("API.Schema.MangaContext.Manga", "Manga")
.WithMany() .WithMany()
.HasForeignKey("MangaId") .HasForeignKey("MangaId")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
b.HasOne("API.Schema.MetadataFetchers.MetadataFetcher", "MetadataFetcher") b.HasOne("API.Schema.MangaContext.MetadataFetchers.MetadataFetcher", "MetadataFetcher")
.WithMany() .WithMany()
.HasForeignKey("MetadataFetcherName") .HasForeignKey("MetadataFetcherName")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
@ -711,140 +499,40 @@ namespace API.Migrations.pgsql
modelBuilder.Entity("AuthorToManga", b => modelBuilder.Entity("AuthorToManga", b =>
{ {
b.HasOne("API.Schema.Author", null) b.HasOne("API.Schema.MangaContext.Author", null)
.WithMany() .WithMany()
.HasForeignKey("AuthorIds") .HasForeignKey("AuthorIds")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
b.HasOne("API.Schema.Manga", null) b.HasOne("API.Schema.MangaContext.Manga", null)
.WithMany() .WithMany()
.HasForeignKey("MangaIds") .HasForeignKey("MangaIds")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
}); });
modelBuilder.Entity("JobJob", b =>
{
b.HasOne("API.Schema.Jobs.Job", null)
.WithMany()
.HasForeignKey("DependsOnJobsKey")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("API.Schema.Jobs.Job", null)
.WithMany()
.HasForeignKey("JobKey")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("MangaTagToManga", b => modelBuilder.Entity("MangaTagToManga", b =>
{ {
b.HasOne("API.Schema.Manga", null) b.HasOne("API.Schema.MangaContext.Manga", null)
.WithMany() .WithMany()
.HasForeignKey("MangaIds") .HasForeignKey("MangaIds")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
b.HasOne("API.Schema.MangaTag", null) b.HasOne("API.Schema.MangaContext.MangaTag", null)
.WithMany() .WithMany()
.HasForeignKey("MangaTagIds") .HasForeignKey("MangaTagIds")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
}); });
modelBuilder.Entity("API.Schema.Jobs.DownloadAvailableChaptersJob", b => modelBuilder.Entity("API.Schema.MangaContext.Chapter", b =>
{
b.HasOne("API.Schema.Manga", "Manga")
.WithMany()
.HasForeignKey("MangaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Manga");
});
modelBuilder.Entity("API.Schema.Jobs.DownloadMangaCoverJob", b =>
{
b.HasOne("API.Schema.Manga", "Manga")
.WithMany()
.HasForeignKey("MangaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Manga");
});
modelBuilder.Entity("API.Schema.Jobs.DownloadSingleChapterJob", b =>
{
b.HasOne("API.Schema.Chapter", "Chapter")
.WithMany()
.HasForeignKey("ChapterId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Chapter");
});
modelBuilder.Entity("API.Schema.Jobs.MoveMangaLibraryJob", b =>
{
b.HasOne("API.Schema.Manga", "Manga")
.WithMany()
.HasForeignKey("MangaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("API.Schema.FileLibrary", "ToFileLibrary")
.WithMany()
.HasForeignKey("ToLibraryId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Manga");
b.Navigation("ToFileLibrary");
});
modelBuilder.Entity("API.Schema.Jobs.RetrieveChaptersJob", b =>
{
b.HasOne("API.Schema.Manga", "Manga")
.WithMany()
.HasForeignKey("MangaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Manga");
});
modelBuilder.Entity("API.Schema.Jobs.UpdateChaptersDownloadedJob", b =>
{
b.HasOne("API.Schema.Manga", "Manga")
.WithMany()
.HasForeignKey("MangaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Manga");
});
modelBuilder.Entity("API.Schema.Jobs.UpdateCoverJob", b =>
{
b.HasOne("API.Schema.Manga", "Manga")
.WithMany()
.HasForeignKey("MangaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Manga");
});
modelBuilder.Entity("API.Schema.Chapter", b =>
{ {
b.Navigation("MangaConnectorIds"); b.Navigation("MangaConnectorIds");
}); });
modelBuilder.Entity("API.Schema.Manga", b => modelBuilder.Entity("API.Schema.MangaContext.Manga", b =>
{ {
b.Navigation("Chapters"); b.Navigation("Chapters");

View File

@ -1,7 +1,7 @@
// <auto-generated /> // <auto-generated />
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using API.Schema.Contexts; using API.Schema.NotificationsContext;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Migrations;
@ -10,10 +10,10 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable #nullable disable
namespace API.Migrations.notifications namespace API.Migrations.Notifications
{ {
[DbContext(typeof(NotificationsContext))] [DbContext(typeof(NotificationsContext))]
[Migration("20250515120746_Initial")] [Migration("20250703191820_Initial")]
partial class Initial partial class Initial
{ {
/// <inheritdoc /> /// <inheritdoc />
@ -21,21 +21,23 @@ namespace API.Migrations.notifications
{ {
#pragma warning disable 612, 618 #pragma warning disable 612, 618
modelBuilder modelBuilder
.HasAnnotation("ProductVersion", "9.0.3") .HasAnnotation("ProductVersion", "9.0.5")
.HasAnnotation("Relational:MaxIdentifierLength", 63); .HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "hstore"); NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "hstore");
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("API.Schema.Notification", b => modelBuilder.Entity("API.Schema.NotificationsContext.Notification", b =>
{ {
b.Property<string>("NotificationId") b.Property<string>("Key")
.HasMaxLength(64) .HasColumnType("text");
.HasColumnType("character varying(64)");
b.Property<DateTime>("Date") b.Property<DateTime>("Date")
.HasColumnType("timestamp with time zone"); .HasColumnType("timestamp with time zone");
b.Property<bool>("IsSent")
.HasColumnType("boolean");
b.Property<string>("Message") b.Property<string>("Message")
.IsRequired() .IsRequired()
.HasMaxLength(512) .HasMaxLength(512)
@ -49,12 +51,12 @@ namespace API.Migrations.notifications
b.Property<byte>("Urgency") b.Property<byte>("Urgency")
.HasColumnType("smallint"); .HasColumnType("smallint");
b.HasKey("NotificationId"); b.HasKey("Key");
b.ToTable("Notifications"); b.ToTable("Notifications");
}); });
modelBuilder.Entity("API.Schema.NotificationConnectors.NotificationConnector", b => modelBuilder.Entity("API.Schema.NotificationsContext.NotificationConnectors.NotificationConnector", b =>
{ {
b.Property<string>("Name") b.Property<string>("Name")
.HasMaxLength(64) .HasMaxLength(64)

View File

@ -4,7 +4,7 @@ using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable #nullable disable
namespace API.Migrations.notifications namespace API.Migrations.Notifications
{ {
/// <inheritdoc /> /// <inheritdoc />
public partial class Initial : Migration public partial class Initial : Migration
@ -34,15 +34,16 @@ namespace API.Migrations.notifications
name: "Notifications", name: "Notifications",
columns: table => new columns: table => new
{ {
NotificationId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false), Key = table.Column<string>(type: "text", nullable: false),
Urgency = table.Column<byte>(type: "smallint", nullable: false), Urgency = table.Column<byte>(type: "smallint", nullable: false),
Title = table.Column<string>(type: "character varying(128)", maxLength: 128, 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), Message = table.Column<string>(type: "character varying(512)", maxLength: 512, nullable: false),
Date = table.Column<DateTime>(type: "timestamp with time zone", nullable: false) Date = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
IsSent = table.Column<bool>(type: "boolean", nullable: false)
}, },
constraints: table => constraints: table =>
{ {
table.PrimaryKey("PK_Notifications", x => x.NotificationId); table.PrimaryKey("PK_Notifications", x => x.Key);
}); });
} }

View File

@ -1,7 +1,7 @@
// <auto-generated /> // <auto-generated />
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using API.Schema.Contexts; using API.Schema.NotificationsContext;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
@ -9,7 +9,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable #nullable disable
namespace API.Migrations.notifications namespace API.Migrations.Notifications
{ {
[DbContext(typeof(NotificationsContext))] [DbContext(typeof(NotificationsContext))]
partial class NotificationsContextModelSnapshot : ModelSnapshot partial class NotificationsContextModelSnapshot : ModelSnapshot
@ -18,21 +18,23 @@ namespace API.Migrations.notifications
{ {
#pragma warning disable 612, 618 #pragma warning disable 612, 618
modelBuilder modelBuilder
.HasAnnotation("ProductVersion", "9.0.3") .HasAnnotation("ProductVersion", "9.0.5")
.HasAnnotation("Relational:MaxIdentifierLength", 63); .HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "hstore"); NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "hstore");
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("API.Schema.Notification", b => modelBuilder.Entity("API.Schema.NotificationsContext.Notification", b =>
{ {
b.Property<string>("NotificationId") b.Property<string>("Key")
.HasMaxLength(64) .HasColumnType("text");
.HasColumnType("character varying(64)");
b.Property<DateTime>("Date") b.Property<DateTime>("Date")
.HasColumnType("timestamp with time zone"); .HasColumnType("timestamp with time zone");
b.Property<bool>("IsSent")
.HasColumnType("boolean");
b.Property<string>("Message") b.Property<string>("Message")
.IsRequired() .IsRequired()
.HasMaxLength(512) .HasMaxLength(512)
@ -46,12 +48,12 @@ namespace API.Migrations.notifications
b.Property<byte>("Urgency") b.Property<byte>("Urgency")
.HasColumnType("smallint"); .HasColumnType("smallint");
b.HasKey("NotificationId"); b.HasKey("Key");
b.ToTable("Notifications"); b.ToTable("Notifications");
}); });
modelBuilder.Entity("API.Schema.NotificationConnectors.NotificationConnector", b => modelBuilder.Entity("API.Schema.NotificationsContext.NotificationConnectors.NotificationConnector", b =>
{ {
b.Property<string>("Name") b.Property<string>("Name")
.HasMaxLength(64) .HasMaxLength(64)

View File

@ -1,682 +0,0 @@
// <auto-generated />
using System;
using API.Schema.Contexts;
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.pgsql
{
[DbContext(typeof(PgsqlContext))]
[Migration("20250515120724_Initial-1")]
partial class Initial1
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "9.0.3")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("API.Schema.Author", b =>
{
b.Property<string>("AuthorId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("AuthorName")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("character varying(128)");
b.HasKey("AuthorId");
b.ToTable("Authors");
});
modelBuilder.Entity("API.Schema.Chapter", b =>
{
b.Property<string>("ChapterId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
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()
.HasColumnType("character varying(64)");
b.Property<string>("Title")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("Url")
.IsRequired()
.HasMaxLength(2048)
.HasColumnType("character varying(2048)");
b.Property<int?>("VolumeNumber")
.HasColumnType("integer");
b.HasKey("ChapterId");
b.HasIndex("ParentMangaId");
b.ToTable("Chapters");
});
modelBuilder.Entity("API.Schema.Jobs.Job", b =>
{
b.Property<string>("JobId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<bool>("Enabled")
.HasColumnType("boolean");
b.Property<byte>("JobType")
.HasColumnType("smallint");
b.Property<DateTime>("LastExecution")
.HasColumnType("timestamp with time zone");
b.Property<string>("ParentJobId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<decimal>("RecurrenceMs")
.HasColumnType("numeric(20,0)");
b.Property<byte>("state")
.HasColumnType("smallint");
b.HasKey("JobId");
b.HasIndex("ParentJobId");
b.ToTable("Jobs");
b.HasDiscriminator<byte>("JobType");
b.UseTphMappingStrategy();
});
modelBuilder.Entity("API.Schema.LocalLibrary", b =>
{
b.Property<string>("LocalLibraryId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("BasePath")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("LibraryName")
.IsRequired()
.HasMaxLength(512)
.HasColumnType("character varying(512)");
b.HasKey("LocalLibraryId");
b.ToTable("LocalLibraries");
});
modelBuilder.Entity("API.Schema.Manga", b =>
{
b.Property<string>("MangaId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
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<string>("IdOnConnectorSite")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<float>("IgnoreChaptersBefore")
.HasColumnType("real");
b.Property<string>("LibraryId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("MangaConnectorName")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("character varying(32)");
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<string>("WebsiteUrl")
.IsRequired()
.HasMaxLength(512)
.HasColumnType("character varying(512)");
b.Property<long>("Year")
.HasColumnType("bigint");
b.HasKey("MangaId");
b.HasIndex("LibraryId");
b.HasIndex("MangaConnectorName");
b.ToTable("Mangas");
});
modelBuilder.Entity("API.Schema.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.MangaTag", b =>
{
b.Property<string>("Tag")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.HasKey("Tag");
b.ToTable("Tags");
});
modelBuilder.Entity("AuthorToManga", b =>
{
b.Property<string>("AuthorIds")
.HasColumnType("character varying(64)");
b.Property<string>("MangaIds")
.HasColumnType("character varying(64)");
b.HasKey("AuthorIds", "MangaIds");
b.HasIndex("MangaIds");
b.ToTable("AuthorToManga");
});
modelBuilder.Entity("JobJob", b =>
{
b.Property<string>("DependsOnJobsJobId")
.HasColumnType("character varying(64)");
b.Property<string>("JobId")
.HasColumnType("character varying(64)");
b.HasKey("DependsOnJobsJobId", "JobId");
b.HasIndex("JobId");
b.ToTable("JobJob");
});
modelBuilder.Entity("MangaTagToManga", b =>
{
b.Property<string>("MangaTagIds")
.HasColumnType("character varying(64)");
b.Property<string>("MangaIds")
.HasColumnType("character varying(64)");
b.HasKey("MangaTagIds", "MangaIds");
b.HasIndex("MangaIds");
b.ToTable("MangaTagToManga");
});
modelBuilder.Entity("API.Schema.Jobs.DownloadAvailableChaptersJob", b =>
{
b.HasBaseType("API.Schema.Jobs.Job");
b.Property<string>("MangaId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.HasIndex("MangaId");
b.ToTable("Jobs", t =>
{
t.Property("MangaId")
.HasColumnName("DownloadAvailableChaptersJob_MangaId");
});
b.HasDiscriminator().HasValue((byte)1);
});
modelBuilder.Entity("API.Schema.Jobs.DownloadMangaCoverJob", b =>
{
b.HasBaseType("API.Schema.Jobs.Job");
b.Property<string>("MangaId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.HasIndex("MangaId");
b.HasDiscriminator().HasValue((byte)4);
});
modelBuilder.Entity("API.Schema.Jobs.DownloadSingleChapterJob", b =>
{
b.HasBaseType("API.Schema.Jobs.Job");
b.Property<string>("ChapterId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.HasIndex("ChapterId");
b.HasDiscriminator().HasValue((byte)0);
});
modelBuilder.Entity("API.Schema.Jobs.MoveFileOrFolderJob", b =>
{
b.HasBaseType("API.Schema.Jobs.Job");
b.Property<string>("FromLocation")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("ToLocation")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.HasDiscriminator().HasValue((byte)3);
});
modelBuilder.Entity("API.Schema.Jobs.MoveMangaLibraryJob", b =>
{
b.HasBaseType("API.Schema.Jobs.Job");
b.Property<string>("MangaId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("ToLibraryId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.HasIndex("MangaId");
b.HasIndex("ToLibraryId");
b.ToTable("Jobs", t =>
{
t.Property("MangaId")
.HasColumnName("MoveMangaLibraryJob_MangaId");
});
b.HasDiscriminator().HasValue((byte)7);
});
modelBuilder.Entity("API.Schema.Jobs.RetrieveChaptersJob", b =>
{
b.HasBaseType("API.Schema.Jobs.Job");
b.Property<string>("Language")
.IsRequired()
.HasMaxLength(8)
.HasColumnType("character varying(8)");
b.Property<string>("MangaId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.HasIndex("MangaId");
b.ToTable("Jobs", t =>
{
t.Property("MangaId")
.HasColumnName("RetrieveChaptersJob_MangaId");
});
b.HasDiscriminator().HasValue((byte)5);
});
modelBuilder.Entity("API.Schema.Jobs.UpdateChaptersDownloadedJob", b =>
{
b.HasBaseType("API.Schema.Jobs.Job");
b.Property<string>("MangaId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.HasIndex("MangaId");
b.ToTable("Jobs", t =>
{
t.Property("MangaId")
.HasColumnName("UpdateFilesDownloadedJob_MangaId");
});
b.HasDiscriminator().HasValue((byte)6);
});
modelBuilder.Entity("API.Schema.MangaConnectors.Global", b =>
{
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
b.HasDiscriminator().HasValue("Global");
});
modelBuilder.Entity("API.Schema.MangaConnectors.MangaDex", b =>
{
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
b.HasDiscriminator().HasValue("MangaDex");
});
modelBuilder.Entity("API.Schema.Chapter", b =>
{
b.HasOne("API.Schema.Manga", "ParentManga")
.WithMany("Chapters")
.HasForeignKey("ParentMangaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("ParentManga");
});
modelBuilder.Entity("API.Schema.Jobs.Job", b =>
{
b.HasOne("API.Schema.Jobs.Job", "ParentJob")
.WithMany()
.HasForeignKey("ParentJobId")
.OnDelete(DeleteBehavior.Cascade);
b.Navigation("ParentJob");
});
modelBuilder.Entity("API.Schema.Manga", b =>
{
b.HasOne("API.Schema.LocalLibrary", "Library")
.WithMany()
.HasForeignKey("LibraryId")
.OnDelete(DeleteBehavior.SetNull);
b.HasOne("API.Schema.MangaConnectors.MangaConnector", "MangaConnector")
.WithMany()
.HasForeignKey("MangaConnectorName")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.OwnsMany("API.Schema.Link", "Links", b1 =>
{
b1.Property<string>("LinkId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
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>("MangaId")
.IsRequired()
.HasColumnType("character varying(64)");
b1.HasKey("LinkId");
b1.HasIndex("MangaId");
b1.ToTable("Link");
b1.WithOwner()
.HasForeignKey("MangaId");
});
b.OwnsMany("API.Schema.MangaAltTitle", "AltTitles", b1 =>
{
b1.Property<string>("AltTitleId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b1.Property<string>("Language")
.IsRequired()
.HasMaxLength(8)
.HasColumnType("character varying(8)");
b1.Property<string>("MangaId")
.IsRequired()
.HasColumnType("character varying(64)");
b1.Property<string>("Title")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b1.HasKey("AltTitleId");
b1.HasIndex("MangaId");
b1.ToTable("MangaAltTitle");
b1.WithOwner()
.HasForeignKey("MangaId");
});
b.Navigation("AltTitles");
b.Navigation("Library");
b.Navigation("Links");
b.Navigation("MangaConnector");
});
modelBuilder.Entity("AuthorToManga", b =>
{
b.HasOne("API.Schema.Author", null)
.WithMany()
.HasForeignKey("AuthorIds")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("API.Schema.Manga", null)
.WithMany()
.HasForeignKey("MangaIds")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("JobJob", b =>
{
b.HasOne("API.Schema.Jobs.Job", null)
.WithMany()
.HasForeignKey("DependsOnJobsJobId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("API.Schema.Jobs.Job", null)
.WithMany()
.HasForeignKey("JobId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("MangaTagToManga", b =>
{
b.HasOne("API.Schema.Manga", null)
.WithMany()
.HasForeignKey("MangaIds")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("API.Schema.MangaTag", null)
.WithMany()
.HasForeignKey("MangaTagIds")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("API.Schema.Jobs.DownloadAvailableChaptersJob", b =>
{
b.HasOne("API.Schema.Manga", "Manga")
.WithMany()
.HasForeignKey("MangaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Manga");
});
modelBuilder.Entity("API.Schema.Jobs.DownloadMangaCoverJob", b =>
{
b.HasOne("API.Schema.Manga", "Manga")
.WithMany()
.HasForeignKey("MangaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Manga");
});
modelBuilder.Entity("API.Schema.Jobs.DownloadSingleChapterJob", b =>
{
b.HasOne("API.Schema.Chapter", "Chapter")
.WithMany()
.HasForeignKey("ChapterId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Chapter");
});
modelBuilder.Entity("API.Schema.Jobs.MoveMangaLibraryJob", b =>
{
b.HasOne("API.Schema.Manga", "Manga")
.WithMany()
.HasForeignKey("MangaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("API.Schema.LocalLibrary", "ToLibrary")
.WithMany()
.HasForeignKey("ToLibraryId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Manga");
b.Navigation("ToLibrary");
});
modelBuilder.Entity("API.Schema.Jobs.RetrieveChaptersJob", b =>
{
b.HasOne("API.Schema.Manga", "Manga")
.WithMany()
.HasForeignKey("MangaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Manga");
});
modelBuilder.Entity("API.Schema.Jobs.UpdateChaptersDownloadedJob", b =>
{
b.HasOne("API.Schema.Manga", "Manga")
.WithMany()
.HasForeignKey("MangaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Manga");
});
modelBuilder.Entity("API.Schema.Manga", b =>
{
b.Navigation("Chapters");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -1,688 +0,0 @@
// <auto-generated />
using System;
using API.Schema.Contexts;
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.pgsql
{
[DbContext(typeof(PgsqlContext))]
[Migration("20250516121442_AltTitle-Owned")]
partial class AltTitleOwned
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "9.0.3")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("API.Schema.Author", b =>
{
b.Property<string>("AuthorId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("AuthorName")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("character varying(128)");
b.HasKey("AuthorId");
b.ToTable("Authors");
});
modelBuilder.Entity("API.Schema.Chapter", b =>
{
b.Property<string>("ChapterId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
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()
.HasColumnType("character varying(64)");
b.Property<string>("Title")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("Url")
.IsRequired()
.HasMaxLength(2048)
.HasColumnType("character varying(2048)");
b.Property<int?>("VolumeNumber")
.HasColumnType("integer");
b.HasKey("ChapterId");
b.HasIndex("ParentMangaId");
b.ToTable("Chapters");
});
modelBuilder.Entity("API.Schema.Jobs.Job", b =>
{
b.Property<string>("JobId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<bool>("Enabled")
.HasColumnType("boolean");
b.Property<byte>("JobType")
.HasColumnType("smallint");
b.Property<DateTime>("LastExecution")
.HasColumnType("timestamp with time zone");
b.Property<string>("ParentJobId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<decimal>("RecurrenceMs")
.HasColumnType("numeric(20,0)");
b.Property<byte>("state")
.HasColumnType("smallint");
b.HasKey("JobId");
b.HasIndex("ParentJobId");
b.ToTable("Jobs");
b.HasDiscriminator<byte>("JobType");
b.UseTphMappingStrategy();
});
modelBuilder.Entity("API.Schema.LocalLibrary", b =>
{
b.Property<string>("LocalLibraryId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("BasePath")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("LibraryName")
.IsRequired()
.HasMaxLength(512)
.HasColumnType("character varying(512)");
b.HasKey("LocalLibraryId");
b.ToTable("LocalLibraries");
});
modelBuilder.Entity("API.Schema.Manga", b =>
{
b.Property<string>("MangaId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
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<string>("IdOnConnectorSite")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<float>("IgnoreChaptersBefore")
.HasColumnType("real");
b.Property<string>("LibraryId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("MangaConnectorName")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("character varying(32)");
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<string>("WebsiteUrl")
.IsRequired()
.HasMaxLength(512)
.HasColumnType("character varying(512)");
b.Property<long>("Year")
.HasColumnType("bigint");
b.HasKey("MangaId");
b.HasIndex("LibraryId");
b.HasIndex("MangaConnectorName");
b.ToTable("Mangas");
});
modelBuilder.Entity("API.Schema.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.MangaTag", b =>
{
b.Property<string>("Tag")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.HasKey("Tag");
b.ToTable("Tags");
});
modelBuilder.Entity("AuthorToManga", b =>
{
b.Property<string>("AuthorIds")
.HasColumnType("character varying(64)");
b.Property<string>("MangaIds")
.HasColumnType("character varying(64)");
b.HasKey("AuthorIds", "MangaIds");
b.HasIndex("MangaIds");
b.ToTable("AuthorToManga");
});
modelBuilder.Entity("JobJob", b =>
{
b.Property<string>("DependsOnJobsJobId")
.HasColumnType("character varying(64)");
b.Property<string>("JobId")
.HasColumnType("character varying(64)");
b.HasKey("DependsOnJobsJobId", "JobId");
b.HasIndex("JobId");
b.ToTable("JobJob");
});
modelBuilder.Entity("MangaTagToManga", b =>
{
b.Property<string>("MangaTagIds")
.HasColumnType("character varying(64)");
b.Property<string>("MangaIds")
.HasColumnType("character varying(64)");
b.HasKey("MangaTagIds", "MangaIds");
b.HasIndex("MangaIds");
b.ToTable("MangaTagToManga");
});
modelBuilder.Entity("API.Schema.Jobs.DownloadAvailableChaptersJob", b =>
{
b.HasBaseType("API.Schema.Jobs.Job");
b.Property<string>("MangaId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.HasIndex("MangaId");
b.ToTable("Jobs", t =>
{
t.Property("MangaId")
.HasColumnName("DownloadAvailableChaptersJob_MangaId");
});
b.HasDiscriminator().HasValue((byte)1);
});
modelBuilder.Entity("API.Schema.Jobs.DownloadMangaCoverJob", b =>
{
b.HasBaseType("API.Schema.Jobs.Job");
b.Property<string>("MangaId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.HasIndex("MangaId");
b.HasDiscriminator().HasValue((byte)4);
});
modelBuilder.Entity("API.Schema.Jobs.DownloadSingleChapterJob", b =>
{
b.HasBaseType("API.Schema.Jobs.Job");
b.Property<string>("ChapterId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.HasIndex("ChapterId");
b.HasDiscriminator().HasValue((byte)0);
});
modelBuilder.Entity("API.Schema.Jobs.MoveFileOrFolderJob", b =>
{
b.HasBaseType("API.Schema.Jobs.Job");
b.Property<string>("FromLocation")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("ToLocation")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.HasDiscriminator().HasValue((byte)3);
});
modelBuilder.Entity("API.Schema.Jobs.MoveMangaLibraryJob", b =>
{
b.HasBaseType("API.Schema.Jobs.Job");
b.Property<string>("MangaId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("ToLibraryId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.HasIndex("MangaId");
b.HasIndex("ToLibraryId");
b.ToTable("Jobs", t =>
{
t.Property("MangaId")
.HasColumnName("MoveMangaLibraryJob_MangaId");
});
b.HasDiscriminator().HasValue((byte)7);
});
modelBuilder.Entity("API.Schema.Jobs.RetrieveChaptersJob", b =>
{
b.HasBaseType("API.Schema.Jobs.Job");
b.Property<string>("Language")
.IsRequired()
.HasMaxLength(8)
.HasColumnType("character varying(8)");
b.Property<string>("MangaId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.HasIndex("MangaId");
b.ToTable("Jobs", t =>
{
t.Property("MangaId")
.HasColumnName("RetrieveChaptersJob_MangaId");
});
b.HasDiscriminator().HasValue((byte)5);
});
modelBuilder.Entity("API.Schema.Jobs.UpdateChaptersDownloadedJob", b =>
{
b.HasBaseType("API.Schema.Jobs.Job");
b.Property<string>("MangaId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.HasIndex("MangaId");
b.ToTable("Jobs", t =>
{
t.Property("MangaId")
.HasColumnName("UpdateFilesDownloadedJob_MangaId");
});
b.HasDiscriminator().HasValue((byte)6);
});
modelBuilder.Entity("API.Schema.MangaConnectors.ComickIo", b =>
{
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
b.HasDiscriminator().HasValue("ComickIo");
});
modelBuilder.Entity("API.Schema.MangaConnectors.Global", b =>
{
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
b.HasDiscriminator().HasValue("Global");
});
modelBuilder.Entity("API.Schema.MangaConnectors.MangaDex", b =>
{
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
b.HasDiscriminator().HasValue("MangaDex");
});
modelBuilder.Entity("API.Schema.Chapter", b =>
{
b.HasOne("API.Schema.Manga", "ParentManga")
.WithMany("Chapters")
.HasForeignKey("ParentMangaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("ParentManga");
});
modelBuilder.Entity("API.Schema.Jobs.Job", b =>
{
b.HasOne("API.Schema.Jobs.Job", "ParentJob")
.WithMany()
.HasForeignKey("ParentJobId")
.OnDelete(DeleteBehavior.Cascade);
b.Navigation("ParentJob");
});
modelBuilder.Entity("API.Schema.Manga", b =>
{
b.HasOne("API.Schema.LocalLibrary", "Library")
.WithMany()
.HasForeignKey("LibraryId")
.OnDelete(DeleteBehavior.SetNull);
b.HasOne("API.Schema.MangaConnectors.MangaConnector", "MangaConnector")
.WithMany()
.HasForeignKey("MangaConnectorName")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.OwnsMany("API.Schema.Link", "Links", b1 =>
{
b1.Property<string>("LinkId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
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>("MangaId")
.IsRequired()
.HasColumnType("character varying(64)");
b1.HasKey("LinkId");
b1.HasIndex("MangaId");
b1.ToTable("Link");
b1.WithOwner()
.HasForeignKey("MangaId");
});
b.OwnsMany("API.Schema.MangaAltTitle", "AltTitles", b1 =>
{
b1.Property<string>("MangaId")
.HasColumnType("character varying(64)");
b1.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property<int>("Id"));
b1.Property<string>("Language")
.IsRequired()
.HasMaxLength(8)
.HasColumnType("character varying(8)");
b1.Property<string>("Title")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b1.HasKey("MangaId", "Id");
b1.ToTable("MangaAltTitle");
b1.WithOwner()
.HasForeignKey("MangaId");
});
b.Navigation("AltTitles");
b.Navigation("Library");
b.Navigation("Links");
b.Navigation("MangaConnector");
});
modelBuilder.Entity("AuthorToManga", b =>
{
b.HasOne("API.Schema.Author", null)
.WithMany()
.HasForeignKey("AuthorIds")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("API.Schema.Manga", null)
.WithMany()
.HasForeignKey("MangaIds")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("JobJob", b =>
{
b.HasOne("API.Schema.Jobs.Job", null)
.WithMany()
.HasForeignKey("DependsOnJobsJobId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("API.Schema.Jobs.Job", null)
.WithMany()
.HasForeignKey("JobId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("MangaTagToManga", b =>
{
b.HasOne("API.Schema.Manga", null)
.WithMany()
.HasForeignKey("MangaIds")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("API.Schema.MangaTag", null)
.WithMany()
.HasForeignKey("MangaTagIds")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("API.Schema.Jobs.DownloadAvailableChaptersJob", b =>
{
b.HasOne("API.Schema.Manga", "Manga")
.WithMany()
.HasForeignKey("MangaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Manga");
});
modelBuilder.Entity("API.Schema.Jobs.DownloadMangaCoverJob", b =>
{
b.HasOne("API.Schema.Manga", "Manga")
.WithMany()
.HasForeignKey("MangaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Manga");
});
modelBuilder.Entity("API.Schema.Jobs.DownloadSingleChapterJob", b =>
{
b.HasOne("API.Schema.Chapter", "Chapter")
.WithMany()
.HasForeignKey("ChapterId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Chapter");
});
modelBuilder.Entity("API.Schema.Jobs.MoveMangaLibraryJob", b =>
{
b.HasOne("API.Schema.Manga", "Manga")
.WithMany()
.HasForeignKey("MangaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("API.Schema.LocalLibrary", "ToLibrary")
.WithMany()
.HasForeignKey("ToLibraryId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Manga");
b.Navigation("ToLibrary");
});
modelBuilder.Entity("API.Schema.Jobs.RetrieveChaptersJob", b =>
{
b.HasOne("API.Schema.Manga", "Manga")
.WithMany()
.HasForeignKey("MangaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Manga");
});
modelBuilder.Entity("API.Schema.Jobs.UpdateChaptersDownloadedJob", b =>
{
b.HasOne("API.Schema.Manga", "Manga")
.WithMany()
.HasForeignKey("MangaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Manga");
});
modelBuilder.Entity("API.Schema.Manga", b =>
{
b.Navigation("Chapters");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -1,70 +0,0 @@
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace API.Migrations.pgsql
{
/// <inheritdoc />
public partial class AltTitleOwned : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropPrimaryKey(
name: "PK_MangaAltTitle",
table: "MangaAltTitle");
migrationBuilder.DropIndex(
name: "IX_MangaAltTitle_MangaId",
table: "MangaAltTitle");
migrationBuilder.DropColumn(
name: "AltTitleId",
table: "MangaAltTitle");
migrationBuilder.AddColumn<int>(
name: "Id",
table: "MangaAltTitle",
type: "integer",
nullable: false,
defaultValue: 0)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
migrationBuilder.AddPrimaryKey(
name: "PK_MangaAltTitle",
table: "MangaAltTitle",
columns: new[] { "MangaId", "Id" });
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropPrimaryKey(
name: "PK_MangaAltTitle",
table: "MangaAltTitle");
migrationBuilder.DropColumn(
name: "Id",
table: "MangaAltTitle");
migrationBuilder.AddColumn<string>(
name: "AltTitleId",
table: "MangaAltTitle",
type: "character varying(64)",
maxLength: 64,
nullable: false,
defaultValue: "");
migrationBuilder.AddPrimaryKey(
name: "PK_MangaAltTitle",
table: "MangaAltTitle",
column: "AltTitleId");
migrationBuilder.CreateIndex(
name: "IX_MangaAltTitle_MangaId",
table: "MangaAltTitle",
column: "MangaId");
}
}
}

View File

@ -1,688 +0,0 @@
// <auto-generated />
using System;
using API.Schema.Contexts;
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.pgsql
{
[DbContext(typeof(PgsqlContext))]
[Migration("20250516121725_Manga-Year-Nullable")]
partial class MangaYearNullable
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "9.0.3")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("API.Schema.Author", b =>
{
b.Property<string>("AuthorId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("AuthorName")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("character varying(128)");
b.HasKey("AuthorId");
b.ToTable("Authors");
});
modelBuilder.Entity("API.Schema.Chapter", b =>
{
b.Property<string>("ChapterId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
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()
.HasColumnType("character varying(64)");
b.Property<string>("Title")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("Url")
.IsRequired()
.HasMaxLength(2048)
.HasColumnType("character varying(2048)");
b.Property<int?>("VolumeNumber")
.HasColumnType("integer");
b.HasKey("ChapterId");
b.HasIndex("ParentMangaId");
b.ToTable("Chapters");
});
modelBuilder.Entity("API.Schema.Jobs.Job", b =>
{
b.Property<string>("JobId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<bool>("Enabled")
.HasColumnType("boolean");
b.Property<byte>("JobType")
.HasColumnType("smallint");
b.Property<DateTime>("LastExecution")
.HasColumnType("timestamp with time zone");
b.Property<string>("ParentJobId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<decimal>("RecurrenceMs")
.HasColumnType("numeric(20,0)");
b.Property<byte>("state")
.HasColumnType("smallint");
b.HasKey("JobId");
b.HasIndex("ParentJobId");
b.ToTable("Jobs");
b.HasDiscriminator<byte>("JobType");
b.UseTphMappingStrategy();
});
modelBuilder.Entity("API.Schema.LocalLibrary", b =>
{
b.Property<string>("LocalLibraryId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("BasePath")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("LibraryName")
.IsRequired()
.HasMaxLength(512)
.HasColumnType("character varying(512)");
b.HasKey("LocalLibraryId");
b.ToTable("LocalLibraries");
});
modelBuilder.Entity("API.Schema.Manga", b =>
{
b.Property<string>("MangaId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
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<string>("IdOnConnectorSite")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<float>("IgnoreChaptersBefore")
.HasColumnType("real");
b.Property<string>("LibraryId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("MangaConnectorName")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("character varying(32)");
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<string>("WebsiteUrl")
.IsRequired()
.HasMaxLength(512)
.HasColumnType("character varying(512)");
b.Property<long?>("Year")
.HasColumnType("bigint");
b.HasKey("MangaId");
b.HasIndex("LibraryId");
b.HasIndex("MangaConnectorName");
b.ToTable("Mangas");
});
modelBuilder.Entity("API.Schema.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.MangaTag", b =>
{
b.Property<string>("Tag")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.HasKey("Tag");
b.ToTable("Tags");
});
modelBuilder.Entity("AuthorToManga", b =>
{
b.Property<string>("AuthorIds")
.HasColumnType("character varying(64)");
b.Property<string>("MangaIds")
.HasColumnType("character varying(64)");
b.HasKey("AuthorIds", "MangaIds");
b.HasIndex("MangaIds");
b.ToTable("AuthorToManga");
});
modelBuilder.Entity("JobJob", b =>
{
b.Property<string>("DependsOnJobsJobId")
.HasColumnType("character varying(64)");
b.Property<string>("JobId")
.HasColumnType("character varying(64)");
b.HasKey("DependsOnJobsJobId", "JobId");
b.HasIndex("JobId");
b.ToTable("JobJob");
});
modelBuilder.Entity("MangaTagToManga", b =>
{
b.Property<string>("MangaTagIds")
.HasColumnType("character varying(64)");
b.Property<string>("MangaIds")
.HasColumnType("character varying(64)");
b.HasKey("MangaTagIds", "MangaIds");
b.HasIndex("MangaIds");
b.ToTable("MangaTagToManga");
});
modelBuilder.Entity("API.Schema.Jobs.DownloadAvailableChaptersJob", b =>
{
b.HasBaseType("API.Schema.Jobs.Job");
b.Property<string>("MangaId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.HasIndex("MangaId");
b.ToTable("Jobs", t =>
{
t.Property("MangaId")
.HasColumnName("DownloadAvailableChaptersJob_MangaId");
});
b.HasDiscriminator().HasValue((byte)1);
});
modelBuilder.Entity("API.Schema.Jobs.DownloadMangaCoverJob", b =>
{
b.HasBaseType("API.Schema.Jobs.Job");
b.Property<string>("MangaId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.HasIndex("MangaId");
b.HasDiscriminator().HasValue((byte)4);
});
modelBuilder.Entity("API.Schema.Jobs.DownloadSingleChapterJob", b =>
{
b.HasBaseType("API.Schema.Jobs.Job");
b.Property<string>("ChapterId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.HasIndex("ChapterId");
b.HasDiscriminator().HasValue((byte)0);
});
modelBuilder.Entity("API.Schema.Jobs.MoveFileOrFolderJob", b =>
{
b.HasBaseType("API.Schema.Jobs.Job");
b.Property<string>("FromLocation")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("ToLocation")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.HasDiscriminator().HasValue((byte)3);
});
modelBuilder.Entity("API.Schema.Jobs.MoveMangaLibraryJob", b =>
{
b.HasBaseType("API.Schema.Jobs.Job");
b.Property<string>("MangaId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("ToLibraryId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.HasIndex("MangaId");
b.HasIndex("ToLibraryId");
b.ToTable("Jobs", t =>
{
t.Property("MangaId")
.HasColumnName("MoveMangaLibraryJob_MangaId");
});
b.HasDiscriminator().HasValue((byte)7);
});
modelBuilder.Entity("API.Schema.Jobs.RetrieveChaptersJob", b =>
{
b.HasBaseType("API.Schema.Jobs.Job");
b.Property<string>("Language")
.IsRequired()
.HasMaxLength(8)
.HasColumnType("character varying(8)");
b.Property<string>("MangaId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.HasIndex("MangaId");
b.ToTable("Jobs", t =>
{
t.Property("MangaId")
.HasColumnName("RetrieveChaptersJob_MangaId");
});
b.HasDiscriminator().HasValue((byte)5);
});
modelBuilder.Entity("API.Schema.Jobs.UpdateChaptersDownloadedJob", b =>
{
b.HasBaseType("API.Schema.Jobs.Job");
b.Property<string>("MangaId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.HasIndex("MangaId");
b.ToTable("Jobs", t =>
{
t.Property("MangaId")
.HasColumnName("UpdateFilesDownloadedJob_MangaId");
});
b.HasDiscriminator().HasValue((byte)6);
});
modelBuilder.Entity("API.Schema.MangaConnectors.ComickIo", b =>
{
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
b.HasDiscriminator().HasValue("ComickIo");
});
modelBuilder.Entity("API.Schema.MangaConnectors.Global", b =>
{
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
b.HasDiscriminator().HasValue("Global");
});
modelBuilder.Entity("API.Schema.MangaConnectors.MangaDex", b =>
{
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
b.HasDiscriminator().HasValue("MangaDex");
});
modelBuilder.Entity("API.Schema.Chapter", b =>
{
b.HasOne("API.Schema.Manga", "ParentManga")
.WithMany("Chapters")
.HasForeignKey("ParentMangaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("ParentManga");
});
modelBuilder.Entity("API.Schema.Jobs.Job", b =>
{
b.HasOne("API.Schema.Jobs.Job", "ParentJob")
.WithMany()
.HasForeignKey("ParentJobId")
.OnDelete(DeleteBehavior.Cascade);
b.Navigation("ParentJob");
});
modelBuilder.Entity("API.Schema.Manga", b =>
{
b.HasOne("API.Schema.LocalLibrary", "Library")
.WithMany()
.HasForeignKey("LibraryId")
.OnDelete(DeleteBehavior.SetNull);
b.HasOne("API.Schema.MangaConnectors.MangaConnector", "MangaConnector")
.WithMany()
.HasForeignKey("MangaConnectorName")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.OwnsMany("API.Schema.Link", "Links", b1 =>
{
b1.Property<string>("LinkId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
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>("MangaId")
.IsRequired()
.HasColumnType("character varying(64)");
b1.HasKey("LinkId");
b1.HasIndex("MangaId");
b1.ToTable("Link");
b1.WithOwner()
.HasForeignKey("MangaId");
});
b.OwnsMany("API.Schema.MangaAltTitle", "AltTitles", b1 =>
{
b1.Property<string>("MangaId")
.HasColumnType("character varying(64)");
b1.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property<int>("Id"));
b1.Property<string>("Language")
.IsRequired()
.HasMaxLength(8)
.HasColumnType("character varying(8)");
b1.Property<string>("Title")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b1.HasKey("MangaId", "Id");
b1.ToTable("MangaAltTitle");
b1.WithOwner()
.HasForeignKey("MangaId");
});
b.Navigation("AltTitles");
b.Navigation("Library");
b.Navigation("Links");
b.Navigation("MangaConnector");
});
modelBuilder.Entity("AuthorToManga", b =>
{
b.HasOne("API.Schema.Author", null)
.WithMany()
.HasForeignKey("AuthorIds")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("API.Schema.Manga", null)
.WithMany()
.HasForeignKey("MangaIds")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("JobJob", b =>
{
b.HasOne("API.Schema.Jobs.Job", null)
.WithMany()
.HasForeignKey("DependsOnJobsJobId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("API.Schema.Jobs.Job", null)
.WithMany()
.HasForeignKey("JobId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("MangaTagToManga", b =>
{
b.HasOne("API.Schema.Manga", null)
.WithMany()
.HasForeignKey("MangaIds")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("API.Schema.MangaTag", null)
.WithMany()
.HasForeignKey("MangaTagIds")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("API.Schema.Jobs.DownloadAvailableChaptersJob", b =>
{
b.HasOne("API.Schema.Manga", "Manga")
.WithMany()
.HasForeignKey("MangaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Manga");
});
modelBuilder.Entity("API.Schema.Jobs.DownloadMangaCoverJob", b =>
{
b.HasOne("API.Schema.Manga", "Manga")
.WithMany()
.HasForeignKey("MangaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Manga");
});
modelBuilder.Entity("API.Schema.Jobs.DownloadSingleChapterJob", b =>
{
b.HasOne("API.Schema.Chapter", "Chapter")
.WithMany()
.HasForeignKey("ChapterId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Chapter");
});
modelBuilder.Entity("API.Schema.Jobs.MoveMangaLibraryJob", b =>
{
b.HasOne("API.Schema.Manga", "Manga")
.WithMany()
.HasForeignKey("MangaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("API.Schema.LocalLibrary", "ToLibrary")
.WithMany()
.HasForeignKey("ToLibraryId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Manga");
b.Navigation("ToLibrary");
});
modelBuilder.Entity("API.Schema.Jobs.RetrieveChaptersJob", b =>
{
b.HasOne("API.Schema.Manga", "Manga")
.WithMany()
.HasForeignKey("MangaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Manga");
});
modelBuilder.Entity("API.Schema.Jobs.UpdateChaptersDownloadedJob", b =>
{
b.HasOne("API.Schema.Manga", "Manga")
.WithMany()
.HasForeignKey("MangaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Manga");
});
modelBuilder.Entity("API.Schema.Manga", b =>
{
b.Navigation("Chapters");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -1,36 +0,0 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace API.Migrations.pgsql
{
/// <inheritdoc />
public partial class MangaYearNullable : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<long>(
name: "Year",
table: "Mangas",
type: "bigint",
nullable: true,
oldClrType: typeof(long),
oldType: "bigint");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<long>(
name: "Year",
table: "Mangas",
type: "bigint",
nullable: false,
defaultValue: 0L,
oldClrType: typeof(long),
oldType: "bigint",
oldNullable: true);
}
}
}

View File

@ -1,689 +0,0 @@
// <auto-generated />
using System;
using API.Schema.Contexts;
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.pgsql
{
[DbContext(typeof(PgsqlContext))]
[Migration("20250516122242_AltTitle-Owned-WithId")]
partial class AltTitleOwnedWithId
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "9.0.3")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("API.Schema.Author", b =>
{
b.Property<string>("AuthorId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("AuthorName")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("character varying(128)");
b.HasKey("AuthorId");
b.ToTable("Authors");
});
modelBuilder.Entity("API.Schema.Chapter", b =>
{
b.Property<string>("ChapterId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
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()
.HasColumnType("character varying(64)");
b.Property<string>("Title")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("Url")
.IsRequired()
.HasMaxLength(2048)
.HasColumnType("character varying(2048)");
b.Property<int?>("VolumeNumber")
.HasColumnType("integer");
b.HasKey("ChapterId");
b.HasIndex("ParentMangaId");
b.ToTable("Chapters");
});
modelBuilder.Entity("API.Schema.Jobs.Job", b =>
{
b.Property<string>("JobId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<bool>("Enabled")
.HasColumnType("boolean");
b.Property<byte>("JobType")
.HasColumnType("smallint");
b.Property<DateTime>("LastExecution")
.HasColumnType("timestamp with time zone");
b.Property<string>("ParentJobId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<decimal>("RecurrenceMs")
.HasColumnType("numeric(20,0)");
b.Property<byte>("state")
.HasColumnType("smallint");
b.HasKey("JobId");
b.HasIndex("ParentJobId");
b.ToTable("Jobs");
b.HasDiscriminator<byte>("JobType");
b.UseTphMappingStrategy();
});
modelBuilder.Entity("API.Schema.LocalLibrary", b =>
{
b.Property<string>("LocalLibraryId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("BasePath")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("LibraryName")
.IsRequired()
.HasMaxLength(512)
.HasColumnType("character varying(512)");
b.HasKey("LocalLibraryId");
b.ToTable("LocalLibraries");
});
modelBuilder.Entity("API.Schema.Manga", b =>
{
b.Property<string>("MangaId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
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<string>("IdOnConnectorSite")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<float>("IgnoreChaptersBefore")
.HasColumnType("real");
b.Property<string>("LibraryId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("MangaConnectorName")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("character varying(32)");
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<string>("WebsiteUrl")
.IsRequired()
.HasMaxLength(512)
.HasColumnType("character varying(512)");
b.Property<long?>("Year")
.HasColumnType("bigint");
b.HasKey("MangaId");
b.HasIndex("LibraryId");
b.HasIndex("MangaConnectorName");
b.ToTable("Mangas");
});
modelBuilder.Entity("API.Schema.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.MangaTag", b =>
{
b.Property<string>("Tag")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.HasKey("Tag");
b.ToTable("Tags");
});
modelBuilder.Entity("AuthorToManga", b =>
{
b.Property<string>("AuthorIds")
.HasColumnType("character varying(64)");
b.Property<string>("MangaIds")
.HasColumnType("character varying(64)");
b.HasKey("AuthorIds", "MangaIds");
b.HasIndex("MangaIds");
b.ToTable("AuthorToManga");
});
modelBuilder.Entity("JobJob", b =>
{
b.Property<string>("DependsOnJobsJobId")
.HasColumnType("character varying(64)");
b.Property<string>("JobId")
.HasColumnType("character varying(64)");
b.HasKey("DependsOnJobsJobId", "JobId");
b.HasIndex("JobId");
b.ToTable("JobJob");
});
modelBuilder.Entity("MangaTagToManga", b =>
{
b.Property<string>("MangaTagIds")
.HasColumnType("character varying(64)");
b.Property<string>("MangaIds")
.HasColumnType("character varying(64)");
b.HasKey("MangaTagIds", "MangaIds");
b.HasIndex("MangaIds");
b.ToTable("MangaTagToManga");
});
modelBuilder.Entity("API.Schema.Jobs.DownloadAvailableChaptersJob", b =>
{
b.HasBaseType("API.Schema.Jobs.Job");
b.Property<string>("MangaId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.HasIndex("MangaId");
b.ToTable("Jobs", t =>
{
t.Property("MangaId")
.HasColumnName("DownloadAvailableChaptersJob_MangaId");
});
b.HasDiscriminator().HasValue((byte)1);
});
modelBuilder.Entity("API.Schema.Jobs.DownloadMangaCoverJob", b =>
{
b.HasBaseType("API.Schema.Jobs.Job");
b.Property<string>("MangaId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.HasIndex("MangaId");
b.HasDiscriminator().HasValue((byte)4);
});
modelBuilder.Entity("API.Schema.Jobs.DownloadSingleChapterJob", b =>
{
b.HasBaseType("API.Schema.Jobs.Job");
b.Property<string>("ChapterId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.HasIndex("ChapterId");
b.HasDiscriminator().HasValue((byte)0);
});
modelBuilder.Entity("API.Schema.Jobs.MoveFileOrFolderJob", b =>
{
b.HasBaseType("API.Schema.Jobs.Job");
b.Property<string>("FromLocation")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("ToLocation")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.HasDiscriminator().HasValue((byte)3);
});
modelBuilder.Entity("API.Schema.Jobs.MoveMangaLibraryJob", b =>
{
b.HasBaseType("API.Schema.Jobs.Job");
b.Property<string>("MangaId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("ToLibraryId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.HasIndex("MangaId");
b.HasIndex("ToLibraryId");
b.ToTable("Jobs", t =>
{
t.Property("MangaId")
.HasColumnName("MoveMangaLibraryJob_MangaId");
});
b.HasDiscriminator().HasValue((byte)7);
});
modelBuilder.Entity("API.Schema.Jobs.RetrieveChaptersJob", b =>
{
b.HasBaseType("API.Schema.Jobs.Job");
b.Property<string>("Language")
.IsRequired()
.HasMaxLength(8)
.HasColumnType("character varying(8)");
b.Property<string>("MangaId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.HasIndex("MangaId");
b.ToTable("Jobs", t =>
{
t.Property("MangaId")
.HasColumnName("RetrieveChaptersJob_MangaId");
});
b.HasDiscriminator().HasValue((byte)5);
});
modelBuilder.Entity("API.Schema.Jobs.UpdateChaptersDownloadedJob", b =>
{
b.HasBaseType("API.Schema.Jobs.Job");
b.Property<string>("MangaId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.HasIndex("MangaId");
b.ToTable("Jobs", t =>
{
t.Property("MangaId")
.HasColumnName("UpdateFilesDownloadedJob_MangaId");
});
b.HasDiscriminator().HasValue((byte)6);
});
modelBuilder.Entity("API.Schema.MangaConnectors.ComickIo", b =>
{
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
b.HasDiscriminator().HasValue("ComickIo");
});
modelBuilder.Entity("API.Schema.MangaConnectors.Global", b =>
{
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
b.HasDiscriminator().HasValue("Global");
});
modelBuilder.Entity("API.Schema.MangaConnectors.MangaDex", b =>
{
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
b.HasDiscriminator().HasValue("MangaDex");
});
modelBuilder.Entity("API.Schema.Chapter", b =>
{
b.HasOne("API.Schema.Manga", "ParentManga")
.WithMany("Chapters")
.HasForeignKey("ParentMangaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("ParentManga");
});
modelBuilder.Entity("API.Schema.Jobs.Job", b =>
{
b.HasOne("API.Schema.Jobs.Job", "ParentJob")
.WithMany()
.HasForeignKey("ParentJobId")
.OnDelete(DeleteBehavior.Cascade);
b.Navigation("ParentJob");
});
modelBuilder.Entity("API.Schema.Manga", b =>
{
b.HasOne("API.Schema.LocalLibrary", "Library")
.WithMany()
.HasForeignKey("LibraryId")
.OnDelete(DeleteBehavior.SetNull);
b.HasOne("API.Schema.MangaConnectors.MangaConnector", "MangaConnector")
.WithMany()
.HasForeignKey("MangaConnectorName")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.OwnsMany("API.Schema.Link", "Links", b1 =>
{
b1.Property<string>("LinkId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
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>("MangaId")
.IsRequired()
.HasColumnType("character varying(64)");
b1.HasKey("LinkId");
b1.HasIndex("MangaId");
b1.ToTable("Link");
b1.WithOwner()
.HasForeignKey("MangaId");
});
b.OwnsMany("API.Schema.MangaAltTitle", "AltTitles", b1 =>
{
b1.Property<string>("AltTitleId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b1.Property<string>("Language")
.IsRequired()
.HasMaxLength(8)
.HasColumnType("character varying(8)");
b1.Property<string>("MangaId")
.IsRequired()
.HasColumnType("character varying(64)");
b1.Property<string>("Title")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b1.HasKey("AltTitleId");
b1.HasIndex("MangaId");
b1.ToTable("MangaAltTitle");
b1.WithOwner()
.HasForeignKey("MangaId");
});
b.Navigation("AltTitles");
b.Navigation("Library");
b.Navigation("Links");
b.Navigation("MangaConnector");
});
modelBuilder.Entity("AuthorToManga", b =>
{
b.HasOne("API.Schema.Author", null)
.WithMany()
.HasForeignKey("AuthorIds")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("API.Schema.Manga", null)
.WithMany()
.HasForeignKey("MangaIds")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("JobJob", b =>
{
b.HasOne("API.Schema.Jobs.Job", null)
.WithMany()
.HasForeignKey("DependsOnJobsJobId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("API.Schema.Jobs.Job", null)
.WithMany()
.HasForeignKey("JobId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("MangaTagToManga", b =>
{
b.HasOne("API.Schema.Manga", null)
.WithMany()
.HasForeignKey("MangaIds")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("API.Schema.MangaTag", null)
.WithMany()
.HasForeignKey("MangaTagIds")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("API.Schema.Jobs.DownloadAvailableChaptersJob", b =>
{
b.HasOne("API.Schema.Manga", "Manga")
.WithMany()
.HasForeignKey("MangaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Manga");
});
modelBuilder.Entity("API.Schema.Jobs.DownloadMangaCoverJob", b =>
{
b.HasOne("API.Schema.Manga", "Manga")
.WithMany()
.HasForeignKey("MangaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Manga");
});
modelBuilder.Entity("API.Schema.Jobs.DownloadSingleChapterJob", b =>
{
b.HasOne("API.Schema.Chapter", "Chapter")
.WithMany()
.HasForeignKey("ChapterId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Chapter");
});
modelBuilder.Entity("API.Schema.Jobs.MoveMangaLibraryJob", b =>
{
b.HasOne("API.Schema.Manga", "Manga")
.WithMany()
.HasForeignKey("MangaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("API.Schema.LocalLibrary", "ToLibrary")
.WithMany()
.HasForeignKey("ToLibraryId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Manga");
b.Navigation("ToLibrary");
});
modelBuilder.Entity("API.Schema.Jobs.RetrieveChaptersJob", b =>
{
b.HasOne("API.Schema.Manga", "Manga")
.WithMany()
.HasForeignKey("MangaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Manga");
});
modelBuilder.Entity("API.Schema.Jobs.UpdateChaptersDownloadedJob", b =>
{
b.HasOne("API.Schema.Manga", "Manga")
.WithMany()
.HasForeignKey("MangaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Manga");
});
modelBuilder.Entity("API.Schema.Manga", b =>
{
b.Navigation("Chapters");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -1,70 +0,0 @@
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace API.Migrations.pgsql
{
/// <inheritdoc />
public partial class AltTitleOwnedWithId : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropPrimaryKey(
name: "PK_MangaAltTitle",
table: "MangaAltTitle");
migrationBuilder.DropColumn(
name: "Id",
table: "MangaAltTitle");
migrationBuilder.AddColumn<string>(
name: "AltTitleId",
table: "MangaAltTitle",
type: "character varying(64)",
maxLength: 64,
nullable: false,
defaultValue: "");
migrationBuilder.AddPrimaryKey(
name: "PK_MangaAltTitle",
table: "MangaAltTitle",
column: "AltTitleId");
migrationBuilder.CreateIndex(
name: "IX_MangaAltTitle_MangaId",
table: "MangaAltTitle",
column: "MangaId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropPrimaryKey(
name: "PK_MangaAltTitle",
table: "MangaAltTitle");
migrationBuilder.DropIndex(
name: "IX_MangaAltTitle_MangaId",
table: "MangaAltTitle");
migrationBuilder.DropColumn(
name: "AltTitleId",
table: "MangaAltTitle");
migrationBuilder.AddColumn<int>(
name: "Id",
table: "MangaAltTitle",
type: "integer",
nullable: false,
defaultValue: 0)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
migrationBuilder.AddPrimaryKey(
name: "PK_MangaAltTitle",
table: "MangaAltTitle",
columns: new[] { "MangaId", "Id" });
}
}
}

View File

@ -1,720 +0,0 @@
// <auto-generated />
using System;
using API.Schema.Contexts;
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.pgsql
{
[DbContext(typeof(PgsqlContext))]
[Migration("20250516180953_Split-UpdateChaptersDownloadedJob-Into-UpdateSingleChapterDownloadedJob")]
partial class SplitUpdateChaptersDownloadedJobIntoUpdateSingleChapterDownloadedJob
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "9.0.3")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("API.Schema.Author", b =>
{
b.Property<string>("AuthorId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("AuthorName")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("character varying(128)");
b.HasKey("AuthorId");
b.ToTable("Authors");
});
modelBuilder.Entity("API.Schema.Chapter", b =>
{
b.Property<string>("ChapterId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
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()
.HasColumnType("character varying(64)");
b.Property<string>("Title")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("Url")
.IsRequired()
.HasMaxLength(2048)
.HasColumnType("character varying(2048)");
b.Property<int?>("VolumeNumber")
.HasColumnType("integer");
b.HasKey("ChapterId");
b.HasIndex("ParentMangaId");
b.ToTable("Chapters");
});
modelBuilder.Entity("API.Schema.Jobs.Job", b =>
{
b.Property<string>("JobId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<bool>("Enabled")
.HasColumnType("boolean");
b.Property<byte>("JobType")
.HasColumnType("smallint");
b.Property<DateTime>("LastExecution")
.HasColumnType("timestamp with time zone");
b.Property<string>("ParentJobId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<decimal>("RecurrenceMs")
.HasColumnType("numeric(20,0)");
b.Property<byte>("state")
.HasColumnType("smallint");
b.HasKey("JobId");
b.HasIndex("ParentJobId");
b.ToTable("Jobs");
b.HasDiscriminator<byte>("JobType");
b.UseTphMappingStrategy();
});
modelBuilder.Entity("API.Schema.LocalLibrary", b =>
{
b.Property<string>("LocalLibraryId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("BasePath")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("LibraryName")
.IsRequired()
.HasMaxLength(512)
.HasColumnType("character varying(512)");
b.HasKey("LocalLibraryId");
b.ToTable("LocalLibraries");
});
modelBuilder.Entity("API.Schema.Manga", b =>
{
b.Property<string>("MangaId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
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<string>("IdOnConnectorSite")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<float>("IgnoreChaptersBefore")
.HasColumnType("real");
b.Property<string>("LibraryId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("MangaConnectorName")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("character varying(32)");
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<string>("WebsiteUrl")
.IsRequired()
.HasMaxLength(512)
.HasColumnType("character varying(512)");
b.Property<long?>("Year")
.HasColumnType("bigint");
b.HasKey("MangaId");
b.HasIndex("LibraryId");
b.HasIndex("MangaConnectorName");
b.ToTable("Mangas");
});
modelBuilder.Entity("API.Schema.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.MangaTag", b =>
{
b.Property<string>("Tag")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.HasKey("Tag");
b.ToTable("Tags");
});
modelBuilder.Entity("AuthorToManga", b =>
{
b.Property<string>("AuthorIds")
.HasColumnType("character varying(64)");
b.Property<string>("MangaIds")
.HasColumnType("character varying(64)");
b.HasKey("AuthorIds", "MangaIds");
b.HasIndex("MangaIds");
b.ToTable("AuthorToManga");
});
modelBuilder.Entity("JobJob", b =>
{
b.Property<string>("DependsOnJobsJobId")
.HasColumnType("character varying(64)");
b.Property<string>("JobId")
.HasColumnType("character varying(64)");
b.HasKey("DependsOnJobsJobId", "JobId");
b.HasIndex("JobId");
b.ToTable("JobJob");
});
modelBuilder.Entity("MangaTagToManga", b =>
{
b.Property<string>("MangaTagIds")
.HasColumnType("character varying(64)");
b.Property<string>("MangaIds")
.HasColumnType("character varying(64)");
b.HasKey("MangaTagIds", "MangaIds");
b.HasIndex("MangaIds");
b.ToTable("MangaTagToManga");
});
modelBuilder.Entity("API.Schema.Jobs.DownloadAvailableChaptersJob", b =>
{
b.HasBaseType("API.Schema.Jobs.Job");
b.Property<string>("MangaId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.HasIndex("MangaId");
b.ToTable("Jobs", t =>
{
t.Property("MangaId")
.HasColumnName("DownloadAvailableChaptersJob_MangaId");
});
b.HasDiscriminator().HasValue((byte)1);
});
modelBuilder.Entity("API.Schema.Jobs.DownloadMangaCoverJob", b =>
{
b.HasBaseType("API.Schema.Jobs.Job");
b.Property<string>("MangaId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.HasIndex("MangaId");
b.HasDiscriminator().HasValue((byte)4);
});
modelBuilder.Entity("API.Schema.Jobs.DownloadSingleChapterJob", b =>
{
b.HasBaseType("API.Schema.Jobs.Job");
b.Property<string>("ChapterId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.HasIndex("ChapterId");
b.HasDiscriminator().HasValue((byte)0);
});
modelBuilder.Entity("API.Schema.Jobs.MoveFileOrFolderJob", b =>
{
b.HasBaseType("API.Schema.Jobs.Job");
b.Property<string>("FromLocation")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("ToLocation")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.HasDiscriminator().HasValue((byte)3);
});
modelBuilder.Entity("API.Schema.Jobs.MoveMangaLibraryJob", b =>
{
b.HasBaseType("API.Schema.Jobs.Job");
b.Property<string>("MangaId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("ToLibraryId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.HasIndex("MangaId");
b.HasIndex("ToLibraryId");
b.ToTable("Jobs", t =>
{
t.Property("MangaId")
.HasColumnName("MoveMangaLibraryJob_MangaId");
});
b.HasDiscriminator().HasValue((byte)7);
});
modelBuilder.Entity("API.Schema.Jobs.RetrieveChaptersJob", b =>
{
b.HasBaseType("API.Schema.Jobs.Job");
b.Property<string>("Language")
.IsRequired()
.HasMaxLength(8)
.HasColumnType("character varying(8)");
b.Property<string>("MangaId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.HasIndex("MangaId");
b.ToTable("Jobs", t =>
{
t.Property("MangaId")
.HasColumnName("RetrieveChaptersJob_MangaId");
});
b.HasDiscriminator().HasValue((byte)5);
});
modelBuilder.Entity("API.Schema.Jobs.UpdateChaptersDownloadedJob", b =>
{
b.HasBaseType("API.Schema.Jobs.Job");
b.Property<string>("MangaId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.HasIndex("MangaId");
b.ToTable("Jobs", t =>
{
t.Property("MangaId")
.HasColumnName("UpdateChaptersDownloadedJob_MangaId");
});
b.HasDiscriminator().HasValue((byte)6);
});
modelBuilder.Entity("API.Schema.Jobs.UpdateSingleChapterDownloadedJob", b =>
{
b.HasBaseType("API.Schema.Jobs.Job");
b.Property<string>("ChapterId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.HasIndex("ChapterId");
b.ToTable("Jobs", t =>
{
t.Property("ChapterId")
.HasColumnName("UpdateSingleChapterDownloadedJob_ChapterId");
});
b.HasDiscriminator().HasValue((byte)8);
});
modelBuilder.Entity("API.Schema.MangaConnectors.ComickIo", b =>
{
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
b.HasDiscriminator().HasValue("ComickIo");
});
modelBuilder.Entity("API.Schema.MangaConnectors.Global", b =>
{
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
b.HasDiscriminator().HasValue("Global");
});
modelBuilder.Entity("API.Schema.MangaConnectors.MangaDex", b =>
{
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
b.HasDiscriminator().HasValue("MangaDex");
});
modelBuilder.Entity("API.Schema.Chapter", b =>
{
b.HasOne("API.Schema.Manga", "ParentManga")
.WithMany("Chapters")
.HasForeignKey("ParentMangaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("ParentManga");
});
modelBuilder.Entity("API.Schema.Jobs.Job", b =>
{
b.HasOne("API.Schema.Jobs.Job", "ParentJob")
.WithMany()
.HasForeignKey("ParentJobId")
.OnDelete(DeleteBehavior.Cascade);
b.Navigation("ParentJob");
});
modelBuilder.Entity("API.Schema.Manga", b =>
{
b.HasOne("API.Schema.LocalLibrary", "Library")
.WithMany()
.HasForeignKey("LibraryId")
.OnDelete(DeleteBehavior.SetNull);
b.HasOne("API.Schema.MangaConnectors.MangaConnector", "MangaConnector")
.WithMany()
.HasForeignKey("MangaConnectorName")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.OwnsMany("API.Schema.Link", "Links", b1 =>
{
b1.Property<string>("LinkId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
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>("MangaId")
.IsRequired()
.HasColumnType("character varying(64)");
b1.HasKey("LinkId");
b1.HasIndex("MangaId");
b1.ToTable("Link");
b1.WithOwner()
.HasForeignKey("MangaId");
});
b.OwnsMany("API.Schema.MangaAltTitle", "AltTitles", b1 =>
{
b1.Property<string>("AltTitleId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b1.Property<string>("Language")
.IsRequired()
.HasMaxLength(8)
.HasColumnType("character varying(8)");
b1.Property<string>("MangaId")
.IsRequired()
.HasColumnType("character varying(64)");
b1.Property<string>("Title")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b1.HasKey("AltTitleId");
b1.HasIndex("MangaId");
b1.ToTable("MangaAltTitle");
b1.WithOwner()
.HasForeignKey("MangaId");
});
b.Navigation("AltTitles");
b.Navigation("Library");
b.Navigation("Links");
b.Navigation("MangaConnector");
});
modelBuilder.Entity("AuthorToManga", b =>
{
b.HasOne("API.Schema.Author", null)
.WithMany()
.HasForeignKey("AuthorIds")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("API.Schema.Manga", null)
.WithMany()
.HasForeignKey("MangaIds")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("JobJob", b =>
{
b.HasOne("API.Schema.Jobs.Job", null)
.WithMany()
.HasForeignKey("DependsOnJobsJobId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("API.Schema.Jobs.Job", null)
.WithMany()
.HasForeignKey("JobId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("MangaTagToManga", b =>
{
b.HasOne("API.Schema.Manga", null)
.WithMany()
.HasForeignKey("MangaIds")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("API.Schema.MangaTag", null)
.WithMany()
.HasForeignKey("MangaTagIds")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("API.Schema.Jobs.DownloadAvailableChaptersJob", b =>
{
b.HasOne("API.Schema.Manga", "Manga")
.WithMany()
.HasForeignKey("MangaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Manga");
});
modelBuilder.Entity("API.Schema.Jobs.DownloadMangaCoverJob", b =>
{
b.HasOne("API.Schema.Manga", "Manga")
.WithMany()
.HasForeignKey("MangaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Manga");
});
modelBuilder.Entity("API.Schema.Jobs.DownloadSingleChapterJob", b =>
{
b.HasOne("API.Schema.Chapter", "Chapter")
.WithMany()
.HasForeignKey("ChapterId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Chapter");
});
modelBuilder.Entity("API.Schema.Jobs.MoveMangaLibraryJob", b =>
{
b.HasOne("API.Schema.Manga", "Manga")
.WithMany()
.HasForeignKey("MangaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("API.Schema.LocalLibrary", "ToLibrary")
.WithMany()
.HasForeignKey("ToLibraryId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Manga");
b.Navigation("ToLibrary");
});
modelBuilder.Entity("API.Schema.Jobs.RetrieveChaptersJob", b =>
{
b.HasOne("API.Schema.Manga", "Manga")
.WithMany()
.HasForeignKey("MangaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Manga");
});
modelBuilder.Entity("API.Schema.Jobs.UpdateChaptersDownloadedJob", b =>
{
b.HasOne("API.Schema.Manga", "Manga")
.WithMany()
.HasForeignKey("MangaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Manga");
});
modelBuilder.Entity("API.Schema.Jobs.UpdateSingleChapterDownloadedJob", b =>
{
b.HasOne("API.Schema.Chapter", "Chapter")
.WithMany()
.HasForeignKey("ChapterId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Chapter");
});
modelBuilder.Entity("API.Schema.Manga", b =>
{
b.Navigation("Chapters");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -1,94 +0,0 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace API.Migrations.pgsql
{
/// <inheritdoc />
public partial class SplitUpdateChaptersDownloadedJobIntoUpdateSingleChapterDownloadedJob : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_Jobs_Mangas_UpdateFilesDownloadedJob_MangaId",
table: "Jobs");
migrationBuilder.RenameColumn(
name: "UpdateFilesDownloadedJob_MangaId",
table: "Jobs",
newName: "UpdateChaptersDownloadedJob_MangaId");
migrationBuilder.RenameIndex(
name: "IX_Jobs_UpdateFilesDownloadedJob_MangaId",
table: "Jobs",
newName: "IX_Jobs_UpdateChaptersDownloadedJob_MangaId");
migrationBuilder.AddColumn<string>(
name: "UpdateSingleChapterDownloadedJob_ChapterId",
table: "Jobs",
type: "character varying(64)",
maxLength: 64,
nullable: true);
migrationBuilder.CreateIndex(
name: "IX_Jobs_UpdateSingleChapterDownloadedJob_ChapterId",
table: "Jobs",
column: "UpdateSingleChapterDownloadedJob_ChapterId");
migrationBuilder.AddForeignKey(
name: "FK_Jobs_Chapters_UpdateSingleChapterDownloadedJob_ChapterId",
table: "Jobs",
column: "UpdateSingleChapterDownloadedJob_ChapterId",
principalTable: "Chapters",
principalColumn: "ChapterId",
onDelete: ReferentialAction.Cascade);
migrationBuilder.AddForeignKey(
name: "FK_Jobs_Mangas_UpdateChaptersDownloadedJob_MangaId",
table: "Jobs",
column: "UpdateChaptersDownloadedJob_MangaId",
principalTable: "Mangas",
principalColumn: "MangaId",
onDelete: ReferentialAction.Cascade);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_Jobs_Chapters_UpdateSingleChapterDownloadedJob_ChapterId",
table: "Jobs");
migrationBuilder.DropForeignKey(
name: "FK_Jobs_Mangas_UpdateChaptersDownloadedJob_MangaId",
table: "Jobs");
migrationBuilder.DropIndex(
name: "IX_Jobs_UpdateSingleChapterDownloadedJob_ChapterId",
table: "Jobs");
migrationBuilder.DropColumn(
name: "UpdateSingleChapterDownloadedJob_ChapterId",
table: "Jobs");
migrationBuilder.RenameColumn(
name: "UpdateChaptersDownloadedJob_MangaId",
table: "Jobs",
newName: "UpdateFilesDownloadedJob_MangaId");
migrationBuilder.RenameIndex(
name: "IX_Jobs_UpdateChaptersDownloadedJob_MangaId",
table: "Jobs",
newName: "IX_Jobs_UpdateFilesDownloadedJob_MangaId");
migrationBuilder.AddForeignKey(
name: "FK_Jobs_Mangas_UpdateFilesDownloadedJob_MangaId",
table: "Jobs",
column: "UpdateFilesDownloadedJob_MangaId",
principalTable: "Mangas",
principalColumn: "MangaId",
onDelete: ReferentialAction.Cascade);
}
}
}

View File

@ -1,724 +0,0 @@
// <auto-generated />
using System;
using API.Schema.Contexts;
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.pgsql
{
[DbContext(typeof(PgsqlContext))]
[Migration("20250518142903_Chapter-IdOnConnectorSite")]
partial class ChapterIdOnConnectorSite
{
/// <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.Author", b =>
{
b.Property<string>("AuthorId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("AuthorName")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("character varying(128)");
b.HasKey("AuthorId");
b.ToTable("Authors");
});
modelBuilder.Entity("API.Schema.Chapter", b =>
{
b.Property<string>("ChapterId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
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>("IdOnConnectorSite")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("ParentMangaId")
.IsRequired()
.HasColumnType("character varying(64)");
b.Property<string>("Title")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("Url")
.IsRequired()
.HasMaxLength(2048)
.HasColumnType("character varying(2048)");
b.Property<int?>("VolumeNumber")
.HasColumnType("integer");
b.HasKey("ChapterId");
b.HasIndex("ParentMangaId");
b.ToTable("Chapters");
});
modelBuilder.Entity("API.Schema.Jobs.Job", b =>
{
b.Property<string>("JobId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<bool>("Enabled")
.HasColumnType("boolean");
b.Property<byte>("JobType")
.HasColumnType("smallint");
b.Property<DateTime>("LastExecution")
.HasColumnType("timestamp with time zone");
b.Property<string>("ParentJobId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<decimal>("RecurrenceMs")
.HasColumnType("numeric(20,0)");
b.Property<byte>("state")
.HasColumnType("smallint");
b.HasKey("JobId");
b.HasIndex("ParentJobId");
b.ToTable("Jobs");
b.HasDiscriminator<byte>("JobType");
b.UseTphMappingStrategy();
});
modelBuilder.Entity("API.Schema.LocalLibrary", b =>
{
b.Property<string>("LocalLibraryId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("BasePath")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("LibraryName")
.IsRequired()
.HasMaxLength(512)
.HasColumnType("character varying(512)");
b.HasKey("LocalLibraryId");
b.ToTable("LocalLibraries");
});
modelBuilder.Entity("API.Schema.Manga", b =>
{
b.Property<string>("MangaId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
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<string>("IdOnConnectorSite")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<float>("IgnoreChaptersBefore")
.HasColumnType("real");
b.Property<string>("LibraryId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("MangaConnectorName")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("character varying(32)");
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<string>("WebsiteUrl")
.IsRequired()
.HasMaxLength(512)
.HasColumnType("character varying(512)");
b.Property<long?>("Year")
.HasColumnType("bigint");
b.HasKey("MangaId");
b.HasIndex("LibraryId");
b.HasIndex("MangaConnectorName");
b.ToTable("Mangas");
});
modelBuilder.Entity("API.Schema.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.MangaTag", b =>
{
b.Property<string>("Tag")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.HasKey("Tag");
b.ToTable("Tags");
});
modelBuilder.Entity("AuthorToManga", b =>
{
b.Property<string>("AuthorIds")
.HasColumnType("character varying(64)");
b.Property<string>("MangaIds")
.HasColumnType("character varying(64)");
b.HasKey("AuthorIds", "MangaIds");
b.HasIndex("MangaIds");
b.ToTable("AuthorToManga");
});
modelBuilder.Entity("JobJob", b =>
{
b.Property<string>("DependsOnJobsJobId")
.HasColumnType("character varying(64)");
b.Property<string>("JobId")
.HasColumnType("character varying(64)");
b.HasKey("DependsOnJobsJobId", "JobId");
b.HasIndex("JobId");
b.ToTable("JobJob");
});
modelBuilder.Entity("MangaTagToManga", b =>
{
b.Property<string>("MangaTagIds")
.HasColumnType("character varying(64)");
b.Property<string>("MangaIds")
.HasColumnType("character varying(64)");
b.HasKey("MangaTagIds", "MangaIds");
b.HasIndex("MangaIds");
b.ToTable("MangaTagToManga");
});
modelBuilder.Entity("API.Schema.Jobs.DownloadAvailableChaptersJob", b =>
{
b.HasBaseType("API.Schema.Jobs.Job");
b.Property<string>("MangaId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.HasIndex("MangaId");
b.ToTable("Jobs", t =>
{
t.Property("MangaId")
.HasColumnName("DownloadAvailableChaptersJob_MangaId");
});
b.HasDiscriminator().HasValue((byte)1);
});
modelBuilder.Entity("API.Schema.Jobs.DownloadMangaCoverJob", b =>
{
b.HasBaseType("API.Schema.Jobs.Job");
b.Property<string>("MangaId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.HasIndex("MangaId");
b.HasDiscriminator().HasValue((byte)4);
});
modelBuilder.Entity("API.Schema.Jobs.DownloadSingleChapterJob", b =>
{
b.HasBaseType("API.Schema.Jobs.Job");
b.Property<string>("ChapterId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.HasIndex("ChapterId");
b.HasDiscriminator().HasValue((byte)0);
});
modelBuilder.Entity("API.Schema.Jobs.MoveFileOrFolderJob", b =>
{
b.HasBaseType("API.Schema.Jobs.Job");
b.Property<string>("FromLocation")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("ToLocation")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.HasDiscriminator().HasValue((byte)3);
});
modelBuilder.Entity("API.Schema.Jobs.MoveMangaLibraryJob", b =>
{
b.HasBaseType("API.Schema.Jobs.Job");
b.Property<string>("MangaId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("ToLibraryId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.HasIndex("MangaId");
b.HasIndex("ToLibraryId");
b.ToTable("Jobs", t =>
{
t.Property("MangaId")
.HasColumnName("MoveMangaLibraryJob_MangaId");
});
b.HasDiscriminator().HasValue((byte)7);
});
modelBuilder.Entity("API.Schema.Jobs.RetrieveChaptersJob", b =>
{
b.HasBaseType("API.Schema.Jobs.Job");
b.Property<string>("Language")
.IsRequired()
.HasMaxLength(8)
.HasColumnType("character varying(8)");
b.Property<string>("MangaId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.HasIndex("MangaId");
b.ToTable("Jobs", t =>
{
t.Property("MangaId")
.HasColumnName("RetrieveChaptersJob_MangaId");
});
b.HasDiscriminator().HasValue((byte)5);
});
modelBuilder.Entity("API.Schema.Jobs.UpdateChaptersDownloadedJob", b =>
{
b.HasBaseType("API.Schema.Jobs.Job");
b.Property<string>("MangaId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.HasIndex("MangaId");
b.ToTable("Jobs", t =>
{
t.Property("MangaId")
.HasColumnName("UpdateChaptersDownloadedJob_MangaId");
});
b.HasDiscriminator().HasValue((byte)6);
});
modelBuilder.Entity("API.Schema.Jobs.UpdateSingleChapterDownloadedJob", b =>
{
b.HasBaseType("API.Schema.Jobs.Job");
b.Property<string>("ChapterId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.HasIndex("ChapterId");
b.ToTable("Jobs", t =>
{
t.Property("ChapterId")
.HasColumnName("UpdateSingleChapterDownloadedJob_ChapterId");
});
b.HasDiscriminator().HasValue((byte)8);
});
modelBuilder.Entity("API.Schema.MangaConnectors.ComickIo", b =>
{
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
b.HasDiscriminator().HasValue("ComickIo");
});
modelBuilder.Entity("API.Schema.MangaConnectors.Global", b =>
{
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
b.HasDiscriminator().HasValue("Global");
});
modelBuilder.Entity("API.Schema.MangaConnectors.MangaDex", b =>
{
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
b.HasDiscriminator().HasValue("MangaDex");
});
modelBuilder.Entity("API.Schema.Chapter", b =>
{
b.HasOne("API.Schema.Manga", "ParentManga")
.WithMany("Chapters")
.HasForeignKey("ParentMangaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("ParentManga");
});
modelBuilder.Entity("API.Schema.Jobs.Job", b =>
{
b.HasOne("API.Schema.Jobs.Job", "ParentJob")
.WithMany()
.HasForeignKey("ParentJobId")
.OnDelete(DeleteBehavior.Cascade);
b.Navigation("ParentJob");
});
modelBuilder.Entity("API.Schema.Manga", b =>
{
b.HasOne("API.Schema.LocalLibrary", "Library")
.WithMany()
.HasForeignKey("LibraryId")
.OnDelete(DeleteBehavior.SetNull);
b.HasOne("API.Schema.MangaConnectors.MangaConnector", "MangaConnector")
.WithMany()
.HasForeignKey("MangaConnectorName")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.OwnsMany("API.Schema.Link", "Links", b1 =>
{
b1.Property<string>("LinkId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
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>("MangaId")
.IsRequired()
.HasColumnType("character varying(64)");
b1.HasKey("LinkId");
b1.HasIndex("MangaId");
b1.ToTable("Link");
b1.WithOwner()
.HasForeignKey("MangaId");
});
b.OwnsMany("API.Schema.MangaAltTitle", "AltTitles", b1 =>
{
b1.Property<string>("AltTitleId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b1.Property<string>("Language")
.IsRequired()
.HasMaxLength(8)
.HasColumnType("character varying(8)");
b1.Property<string>("MangaId")
.IsRequired()
.HasColumnType("character varying(64)");
b1.Property<string>("Title")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b1.HasKey("AltTitleId");
b1.HasIndex("MangaId");
b1.ToTable("MangaAltTitle");
b1.WithOwner()
.HasForeignKey("MangaId");
});
b.Navigation("AltTitles");
b.Navigation("Library");
b.Navigation("Links");
b.Navigation("MangaConnector");
});
modelBuilder.Entity("AuthorToManga", b =>
{
b.HasOne("API.Schema.Author", null)
.WithMany()
.HasForeignKey("AuthorIds")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("API.Schema.Manga", null)
.WithMany()
.HasForeignKey("MangaIds")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("JobJob", b =>
{
b.HasOne("API.Schema.Jobs.Job", null)
.WithMany()
.HasForeignKey("DependsOnJobsJobId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("API.Schema.Jobs.Job", null)
.WithMany()
.HasForeignKey("JobId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("MangaTagToManga", b =>
{
b.HasOne("API.Schema.Manga", null)
.WithMany()
.HasForeignKey("MangaIds")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("API.Schema.MangaTag", null)
.WithMany()
.HasForeignKey("MangaTagIds")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("API.Schema.Jobs.DownloadAvailableChaptersJob", b =>
{
b.HasOne("API.Schema.Manga", "Manga")
.WithMany()
.HasForeignKey("MangaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Manga");
});
modelBuilder.Entity("API.Schema.Jobs.DownloadMangaCoverJob", b =>
{
b.HasOne("API.Schema.Manga", "Manga")
.WithMany()
.HasForeignKey("MangaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Manga");
});
modelBuilder.Entity("API.Schema.Jobs.DownloadSingleChapterJob", b =>
{
b.HasOne("API.Schema.Chapter", "Chapter")
.WithMany()
.HasForeignKey("ChapterId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Chapter");
});
modelBuilder.Entity("API.Schema.Jobs.MoveMangaLibraryJob", b =>
{
b.HasOne("API.Schema.Manga", "Manga")
.WithMany()
.HasForeignKey("MangaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("API.Schema.LocalLibrary", "ToLibrary")
.WithMany()
.HasForeignKey("ToLibraryId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Manga");
b.Navigation("ToLibrary");
});
modelBuilder.Entity("API.Schema.Jobs.RetrieveChaptersJob", b =>
{
b.HasOne("API.Schema.Manga", "Manga")
.WithMany()
.HasForeignKey("MangaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Manga");
});
modelBuilder.Entity("API.Schema.Jobs.UpdateChaptersDownloadedJob", b =>
{
b.HasOne("API.Schema.Manga", "Manga")
.WithMany()
.HasForeignKey("MangaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Manga");
});
modelBuilder.Entity("API.Schema.Jobs.UpdateSingleChapterDownloadedJob", b =>
{
b.HasOne("API.Schema.Chapter", "Chapter")
.WithMany()
.HasForeignKey("ChapterId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Chapter");
});
modelBuilder.Entity("API.Schema.Manga", b =>
{
b.Navigation("Chapters");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -1,29 +0,0 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace API.Migrations.pgsql
{
/// <inheritdoc />
public partial class ChapterIdOnConnectorSite : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "IdOnConnectorSite",
table: "Chapters",
type: "character varying(256)",
maxLength: 256,
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "IdOnConnectorSite",
table: "Chapters");
}
}
}

View File

@ -1,755 +0,0 @@
// <auto-generated />
using System;
using API.Schema.Contexts;
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.pgsql
{
[DbContext(typeof(PgsqlContext))]
[Migration("20250518161710_UpdateCoverJob")]
partial class UpdateCoverJob
{
/// <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.Author", b =>
{
b.Property<string>("AuthorId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("AuthorName")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("character varying(128)");
b.HasKey("AuthorId");
b.ToTable("Authors");
});
modelBuilder.Entity("API.Schema.Chapter", b =>
{
b.Property<string>("ChapterId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
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>("IdOnConnectorSite")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("ParentMangaId")
.IsRequired()
.HasColumnType("character varying(64)");
b.Property<string>("Title")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("Url")
.IsRequired()
.HasMaxLength(2048)
.HasColumnType("character varying(2048)");
b.Property<int?>("VolumeNumber")
.HasColumnType("integer");
b.HasKey("ChapterId");
b.HasIndex("ParentMangaId");
b.ToTable("Chapters");
});
modelBuilder.Entity("API.Schema.Jobs.Job", b =>
{
b.Property<string>("JobId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<bool>("Enabled")
.HasColumnType("boolean");
b.Property<byte>("JobType")
.HasColumnType("smallint");
b.Property<DateTime>("LastExecution")
.HasColumnType("timestamp with time zone");
b.Property<string>("ParentJobId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<decimal>("RecurrenceMs")
.HasColumnType("numeric(20,0)");
b.Property<byte>("state")
.HasColumnType("smallint");
b.HasKey("JobId");
b.HasIndex("ParentJobId");
b.ToTable("Jobs");
b.HasDiscriminator<byte>("JobType");
b.UseTphMappingStrategy();
});
modelBuilder.Entity("API.Schema.LocalLibrary", b =>
{
b.Property<string>("LocalLibraryId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("BasePath")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("LibraryName")
.IsRequired()
.HasMaxLength(512)
.HasColumnType("character varying(512)");
b.HasKey("LocalLibraryId");
b.ToTable("LocalLibraries");
});
modelBuilder.Entity("API.Schema.Manga", b =>
{
b.Property<string>("MangaId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
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<string>("IdOnConnectorSite")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<float>("IgnoreChaptersBefore")
.HasColumnType("real");
b.Property<string>("LibraryId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("MangaConnectorName")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("character varying(32)");
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<string>("WebsiteUrl")
.IsRequired()
.HasMaxLength(512)
.HasColumnType("character varying(512)");
b.Property<long?>("Year")
.HasColumnType("bigint");
b.HasKey("MangaId");
b.HasIndex("LibraryId");
b.HasIndex("MangaConnectorName");
b.ToTable("Mangas");
});
modelBuilder.Entity("API.Schema.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.MangaTag", b =>
{
b.Property<string>("Tag")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.HasKey("Tag");
b.ToTable("Tags");
});
modelBuilder.Entity("AuthorToManga", b =>
{
b.Property<string>("AuthorIds")
.HasColumnType("character varying(64)");
b.Property<string>("MangaIds")
.HasColumnType("character varying(64)");
b.HasKey("AuthorIds", "MangaIds");
b.HasIndex("MangaIds");
b.ToTable("AuthorToManga");
});
modelBuilder.Entity("JobJob", b =>
{
b.Property<string>("DependsOnJobsJobId")
.HasColumnType("character varying(64)");
b.Property<string>("JobId")
.HasColumnType("character varying(64)");
b.HasKey("DependsOnJobsJobId", "JobId");
b.HasIndex("JobId");
b.ToTable("JobJob");
});
modelBuilder.Entity("MangaTagToManga", b =>
{
b.Property<string>("MangaTagIds")
.HasColumnType("character varying(64)");
b.Property<string>("MangaIds")
.HasColumnType("character varying(64)");
b.HasKey("MangaTagIds", "MangaIds");
b.HasIndex("MangaIds");
b.ToTable("MangaTagToManga");
});
modelBuilder.Entity("API.Schema.Jobs.DownloadAvailableChaptersJob", b =>
{
b.HasBaseType("API.Schema.Jobs.Job");
b.Property<string>("MangaId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.HasIndex("MangaId");
b.ToTable("Jobs", t =>
{
t.Property("MangaId")
.HasColumnName("DownloadAvailableChaptersJob_MangaId");
});
b.HasDiscriminator().HasValue((byte)1);
});
modelBuilder.Entity("API.Schema.Jobs.DownloadMangaCoverJob", b =>
{
b.HasBaseType("API.Schema.Jobs.Job");
b.Property<string>("MangaId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.HasIndex("MangaId");
b.HasDiscriminator().HasValue((byte)4);
});
modelBuilder.Entity("API.Schema.Jobs.DownloadSingleChapterJob", b =>
{
b.HasBaseType("API.Schema.Jobs.Job");
b.Property<string>("ChapterId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.HasIndex("ChapterId");
b.HasDiscriminator().HasValue((byte)0);
});
modelBuilder.Entity("API.Schema.Jobs.MoveFileOrFolderJob", b =>
{
b.HasBaseType("API.Schema.Jobs.Job");
b.Property<string>("FromLocation")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("ToLocation")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.HasDiscriminator().HasValue((byte)3);
});
modelBuilder.Entity("API.Schema.Jobs.MoveMangaLibraryJob", b =>
{
b.HasBaseType("API.Schema.Jobs.Job");
b.Property<string>("MangaId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("ToLibraryId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.HasIndex("MangaId");
b.HasIndex("ToLibraryId");
b.ToTable("Jobs", t =>
{
t.Property("MangaId")
.HasColumnName("MoveMangaLibraryJob_MangaId");
});
b.HasDiscriminator().HasValue((byte)7);
});
modelBuilder.Entity("API.Schema.Jobs.RetrieveChaptersJob", b =>
{
b.HasBaseType("API.Schema.Jobs.Job");
b.Property<string>("Language")
.IsRequired()
.HasMaxLength(8)
.HasColumnType("character varying(8)");
b.Property<string>("MangaId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.HasIndex("MangaId");
b.ToTable("Jobs", t =>
{
t.Property("MangaId")
.HasColumnName("RetrieveChaptersJob_MangaId");
});
b.HasDiscriminator().HasValue((byte)5);
});
modelBuilder.Entity("API.Schema.Jobs.UpdateChaptersDownloadedJob", b =>
{
b.HasBaseType("API.Schema.Jobs.Job");
b.Property<string>("MangaId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.HasIndex("MangaId");
b.ToTable("Jobs", t =>
{
t.Property("MangaId")
.HasColumnName("UpdateChaptersDownloadedJob_MangaId");
});
b.HasDiscriminator().HasValue((byte)6);
});
modelBuilder.Entity("API.Schema.Jobs.UpdateCoverJob", b =>
{
b.HasBaseType("API.Schema.Jobs.Job");
b.Property<string>("MangaId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.HasIndex("MangaId");
b.ToTable("Jobs", t =>
{
t.Property("MangaId")
.HasColumnName("UpdateCoverJob_MangaId");
});
b.HasDiscriminator().HasValue((byte)9);
});
modelBuilder.Entity("API.Schema.Jobs.UpdateSingleChapterDownloadedJob", b =>
{
b.HasBaseType("API.Schema.Jobs.Job");
b.Property<string>("ChapterId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.HasIndex("ChapterId");
b.ToTable("Jobs", t =>
{
t.Property("ChapterId")
.HasColumnName("UpdateSingleChapterDownloadedJob_ChapterId");
});
b.HasDiscriminator().HasValue((byte)8);
});
modelBuilder.Entity("API.Schema.MangaConnectors.ComickIo", b =>
{
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
b.HasDiscriminator().HasValue("ComickIo");
});
modelBuilder.Entity("API.Schema.MangaConnectors.Global", b =>
{
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
b.HasDiscriminator().HasValue("Global");
});
modelBuilder.Entity("API.Schema.MangaConnectors.MangaDex", b =>
{
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
b.HasDiscriminator().HasValue("MangaDex");
});
modelBuilder.Entity("API.Schema.Chapter", b =>
{
b.HasOne("API.Schema.Manga", "ParentManga")
.WithMany("Chapters")
.HasForeignKey("ParentMangaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("ParentManga");
});
modelBuilder.Entity("API.Schema.Jobs.Job", b =>
{
b.HasOne("API.Schema.Jobs.Job", "ParentJob")
.WithMany()
.HasForeignKey("ParentJobId")
.OnDelete(DeleteBehavior.Cascade);
b.Navigation("ParentJob");
});
modelBuilder.Entity("API.Schema.Manga", b =>
{
b.HasOne("API.Schema.LocalLibrary", "Library")
.WithMany()
.HasForeignKey("LibraryId")
.OnDelete(DeleteBehavior.SetNull);
b.HasOne("API.Schema.MangaConnectors.MangaConnector", "MangaConnector")
.WithMany()
.HasForeignKey("MangaConnectorName")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.OwnsMany("API.Schema.Link", "Links", b1 =>
{
b1.Property<string>("LinkId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
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>("MangaId")
.IsRequired()
.HasColumnType("character varying(64)");
b1.HasKey("LinkId");
b1.HasIndex("MangaId");
b1.ToTable("Link");
b1.WithOwner()
.HasForeignKey("MangaId");
});
b.OwnsMany("API.Schema.MangaAltTitle", "AltTitles", b1 =>
{
b1.Property<string>("AltTitleId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b1.Property<string>("Language")
.IsRequired()
.HasMaxLength(8)
.HasColumnType("character varying(8)");
b1.Property<string>("MangaId")
.IsRequired()
.HasColumnType("character varying(64)");
b1.Property<string>("Title")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b1.HasKey("AltTitleId");
b1.HasIndex("MangaId");
b1.ToTable("MangaAltTitle");
b1.WithOwner()
.HasForeignKey("MangaId");
});
b.Navigation("AltTitles");
b.Navigation("Library");
b.Navigation("Links");
b.Navigation("MangaConnector");
});
modelBuilder.Entity("AuthorToManga", b =>
{
b.HasOne("API.Schema.Author", null)
.WithMany()
.HasForeignKey("AuthorIds")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("API.Schema.Manga", null)
.WithMany()
.HasForeignKey("MangaIds")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("JobJob", b =>
{
b.HasOne("API.Schema.Jobs.Job", null)
.WithMany()
.HasForeignKey("DependsOnJobsJobId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("API.Schema.Jobs.Job", null)
.WithMany()
.HasForeignKey("JobId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("MangaTagToManga", b =>
{
b.HasOne("API.Schema.Manga", null)
.WithMany()
.HasForeignKey("MangaIds")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("API.Schema.MangaTag", null)
.WithMany()
.HasForeignKey("MangaTagIds")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("API.Schema.Jobs.DownloadAvailableChaptersJob", b =>
{
b.HasOne("API.Schema.Manga", "Manga")
.WithMany()
.HasForeignKey("MangaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Manga");
});
modelBuilder.Entity("API.Schema.Jobs.DownloadMangaCoverJob", b =>
{
b.HasOne("API.Schema.Manga", "Manga")
.WithMany()
.HasForeignKey("MangaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Manga");
});
modelBuilder.Entity("API.Schema.Jobs.DownloadSingleChapterJob", b =>
{
b.HasOne("API.Schema.Chapter", "Chapter")
.WithMany()
.HasForeignKey("ChapterId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Chapter");
});
modelBuilder.Entity("API.Schema.Jobs.MoveMangaLibraryJob", b =>
{
b.HasOne("API.Schema.Manga", "Manga")
.WithMany()
.HasForeignKey("MangaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("API.Schema.LocalLibrary", "ToLibrary")
.WithMany()
.HasForeignKey("ToLibraryId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Manga");
b.Navigation("ToLibrary");
});
modelBuilder.Entity("API.Schema.Jobs.RetrieveChaptersJob", b =>
{
b.HasOne("API.Schema.Manga", "Manga")
.WithMany()
.HasForeignKey("MangaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Manga");
});
modelBuilder.Entity("API.Schema.Jobs.UpdateChaptersDownloadedJob", b =>
{
b.HasOne("API.Schema.Manga", "Manga")
.WithMany()
.HasForeignKey("MangaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Manga");
});
modelBuilder.Entity("API.Schema.Jobs.UpdateCoverJob", b =>
{
b.HasOne("API.Schema.Manga", "Manga")
.WithMany()
.HasForeignKey("MangaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Manga");
});
modelBuilder.Entity("API.Schema.Jobs.UpdateSingleChapterDownloadedJob", b =>
{
b.HasOne("API.Schema.Chapter", "Chapter")
.WithMany()
.HasForeignKey("ChapterId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Chapter");
});
modelBuilder.Entity("API.Schema.Manga", b =>
{
b.Navigation("Chapters");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -1,50 +0,0 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace API.Migrations.pgsql
{
/// <inheritdoc />
public partial class UpdateCoverJob : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "UpdateCoverJob_MangaId",
table: "Jobs",
type: "character varying(64)",
maxLength: 64,
nullable: true);
migrationBuilder.CreateIndex(
name: "IX_Jobs_UpdateCoverJob_MangaId",
table: "Jobs",
column: "UpdateCoverJob_MangaId");
migrationBuilder.AddForeignKey(
name: "FK_Jobs_Mangas_UpdateCoverJob_MangaId",
table: "Jobs",
column: "UpdateCoverJob_MangaId",
principalTable: "Mangas",
principalColumn: "MangaId",
onDelete: ReferentialAction.Cascade);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_Jobs_Mangas_UpdateCoverJob_MangaId",
table: "Jobs");
migrationBuilder.DropIndex(
name: "IX_Jobs_UpdateCoverJob_MangaId",
table: "Jobs");
migrationBuilder.DropColumn(
name: "UpdateCoverJob_MangaId",
table: "Jobs");
}
}
}

View File

@ -1,724 +0,0 @@
// <auto-generated />
using System;
using API.Schema.Contexts;
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.pgsql
{
[DbContext(typeof(PgsqlContext))]
[Migration("20250518183729_Remove-UpdateSingleChapterDownloaded-Job")]
partial class RemoveUpdateSingleChapterDownloadedJob
{
/// <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.Author", b =>
{
b.Property<string>("AuthorId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("AuthorName")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("character varying(128)");
b.HasKey("AuthorId");
b.ToTable("Authors");
});
modelBuilder.Entity("API.Schema.Chapter", b =>
{
b.Property<string>("ChapterId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
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>("IdOnConnectorSite")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("ParentMangaId")
.IsRequired()
.HasColumnType("character varying(64)");
b.Property<string>("Title")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("Url")
.IsRequired()
.HasMaxLength(2048)
.HasColumnType("character varying(2048)");
b.Property<int?>("VolumeNumber")
.HasColumnType("integer");
b.HasKey("ChapterId");
b.HasIndex("ParentMangaId");
b.ToTable("Chapters");
});
modelBuilder.Entity("API.Schema.Jobs.Job", b =>
{
b.Property<string>("JobId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<bool>("Enabled")
.HasColumnType("boolean");
b.Property<byte>("JobType")
.HasColumnType("smallint");
b.Property<DateTime>("LastExecution")
.HasColumnType("timestamp with time zone");
b.Property<string>("ParentJobId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<decimal>("RecurrenceMs")
.HasColumnType("numeric(20,0)");
b.Property<byte>("state")
.HasColumnType("smallint");
b.HasKey("JobId");
b.HasIndex("ParentJobId");
b.ToTable("Jobs");
b.HasDiscriminator<byte>("JobType");
b.UseTphMappingStrategy();
});
modelBuilder.Entity("API.Schema.LocalLibrary", b =>
{
b.Property<string>("LocalLibraryId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("BasePath")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("LibraryName")
.IsRequired()
.HasMaxLength(512)
.HasColumnType("character varying(512)");
b.HasKey("LocalLibraryId");
b.ToTable("LocalLibraries");
});
modelBuilder.Entity("API.Schema.Manga", b =>
{
b.Property<string>("MangaId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
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<string>("IdOnConnectorSite")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<float>("IgnoreChaptersBefore")
.HasColumnType("real");
b.Property<string>("LibraryId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("MangaConnectorName")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("character varying(32)");
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<string>("WebsiteUrl")
.IsRequired()
.HasMaxLength(512)
.HasColumnType("character varying(512)");
b.Property<long?>("Year")
.HasColumnType("bigint");
b.HasKey("MangaId");
b.HasIndex("LibraryId");
b.HasIndex("MangaConnectorName");
b.ToTable("Mangas");
});
modelBuilder.Entity("API.Schema.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.MangaTag", b =>
{
b.Property<string>("Tag")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.HasKey("Tag");
b.ToTable("Tags");
});
modelBuilder.Entity("AuthorToManga", b =>
{
b.Property<string>("AuthorIds")
.HasColumnType("character varying(64)");
b.Property<string>("MangaIds")
.HasColumnType("character varying(64)");
b.HasKey("AuthorIds", "MangaIds");
b.HasIndex("MangaIds");
b.ToTable("AuthorToManga");
});
modelBuilder.Entity("JobJob", b =>
{
b.Property<string>("DependsOnJobsJobId")
.HasColumnType("character varying(64)");
b.Property<string>("JobId")
.HasColumnType("character varying(64)");
b.HasKey("DependsOnJobsJobId", "JobId");
b.HasIndex("JobId");
b.ToTable("JobJob");
});
modelBuilder.Entity("MangaTagToManga", b =>
{
b.Property<string>("MangaTagIds")
.HasColumnType("character varying(64)");
b.Property<string>("MangaIds")
.HasColumnType("character varying(64)");
b.HasKey("MangaTagIds", "MangaIds");
b.HasIndex("MangaIds");
b.ToTable("MangaTagToManga");
});
modelBuilder.Entity("API.Schema.Jobs.DownloadAvailableChaptersJob", b =>
{
b.HasBaseType("API.Schema.Jobs.Job");
b.Property<string>("MangaId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.HasIndex("MangaId");
b.ToTable("Jobs", t =>
{
t.Property("MangaId")
.HasColumnName("DownloadAvailableChaptersJob_MangaId");
});
b.HasDiscriminator().HasValue((byte)1);
});
modelBuilder.Entity("API.Schema.Jobs.DownloadMangaCoverJob", b =>
{
b.HasBaseType("API.Schema.Jobs.Job");
b.Property<string>("MangaId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.HasIndex("MangaId");
b.HasDiscriminator().HasValue((byte)4);
});
modelBuilder.Entity("API.Schema.Jobs.DownloadSingleChapterJob", b =>
{
b.HasBaseType("API.Schema.Jobs.Job");
b.Property<string>("ChapterId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.HasIndex("ChapterId");
b.HasDiscriminator().HasValue((byte)0);
});
modelBuilder.Entity("API.Schema.Jobs.MoveFileOrFolderJob", b =>
{
b.HasBaseType("API.Schema.Jobs.Job");
b.Property<string>("FromLocation")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("ToLocation")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.HasDiscriminator().HasValue((byte)3);
});
modelBuilder.Entity("API.Schema.Jobs.MoveMangaLibraryJob", b =>
{
b.HasBaseType("API.Schema.Jobs.Job");
b.Property<string>("MangaId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("ToLibraryId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.HasIndex("MangaId");
b.HasIndex("ToLibraryId");
b.ToTable("Jobs", t =>
{
t.Property("MangaId")
.HasColumnName("MoveMangaLibraryJob_MangaId");
});
b.HasDiscriminator().HasValue((byte)7);
});
modelBuilder.Entity("API.Schema.Jobs.RetrieveChaptersJob", b =>
{
b.HasBaseType("API.Schema.Jobs.Job");
b.Property<string>("Language")
.IsRequired()
.HasMaxLength(8)
.HasColumnType("character varying(8)");
b.Property<string>("MangaId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.HasIndex("MangaId");
b.ToTable("Jobs", t =>
{
t.Property("MangaId")
.HasColumnName("RetrieveChaptersJob_MangaId");
});
b.HasDiscriminator().HasValue((byte)5);
});
modelBuilder.Entity("API.Schema.Jobs.UpdateChaptersDownloadedJob", b =>
{
b.HasBaseType("API.Schema.Jobs.Job");
b.Property<string>("MangaId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.HasIndex("MangaId");
b.ToTable("Jobs", t =>
{
t.Property("MangaId")
.HasColumnName("UpdateChaptersDownloadedJob_MangaId");
});
b.HasDiscriminator().HasValue((byte)6);
});
modelBuilder.Entity("API.Schema.Jobs.UpdateCoverJob", b =>
{
b.HasBaseType("API.Schema.Jobs.Job");
b.Property<string>("MangaId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.HasIndex("MangaId");
b.ToTable("Jobs", t =>
{
t.Property("MangaId")
.HasColumnName("UpdateCoverJob_MangaId");
});
b.HasDiscriminator().HasValue((byte)9);
});
modelBuilder.Entity("API.Schema.MangaConnectors.ComickIo", b =>
{
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
b.HasDiscriminator().HasValue("ComickIo");
});
modelBuilder.Entity("API.Schema.MangaConnectors.Global", b =>
{
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
b.HasDiscriminator().HasValue("Global");
});
modelBuilder.Entity("API.Schema.MangaConnectors.MangaDex", b =>
{
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
b.HasDiscriminator().HasValue("MangaDex");
});
modelBuilder.Entity("API.Schema.Chapter", b =>
{
b.HasOne("API.Schema.Manga", "ParentManga")
.WithMany("Chapters")
.HasForeignKey("ParentMangaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("ParentManga");
});
modelBuilder.Entity("API.Schema.Jobs.Job", b =>
{
b.HasOne("API.Schema.Jobs.Job", "ParentJob")
.WithMany()
.HasForeignKey("ParentJobId")
.OnDelete(DeleteBehavior.Cascade);
b.Navigation("ParentJob");
});
modelBuilder.Entity("API.Schema.Manga", b =>
{
b.HasOne("API.Schema.LocalLibrary", "Library")
.WithMany()
.HasForeignKey("LibraryId")
.OnDelete(DeleteBehavior.SetNull);
b.HasOne("API.Schema.MangaConnectors.MangaConnector", "MangaConnector")
.WithMany()
.HasForeignKey("MangaConnectorName")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.OwnsMany("API.Schema.Link", "Links", b1 =>
{
b1.Property<string>("LinkId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
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>("MangaId")
.IsRequired()
.HasColumnType("character varying(64)");
b1.HasKey("LinkId");
b1.HasIndex("MangaId");
b1.ToTable("Link");
b1.WithOwner()
.HasForeignKey("MangaId");
});
b.OwnsMany("API.Schema.MangaAltTitle", "AltTitles", b1 =>
{
b1.Property<string>("AltTitleId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b1.Property<string>("Language")
.IsRequired()
.HasMaxLength(8)
.HasColumnType("character varying(8)");
b1.Property<string>("MangaId")
.IsRequired()
.HasColumnType("character varying(64)");
b1.Property<string>("Title")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b1.HasKey("AltTitleId");
b1.HasIndex("MangaId");
b1.ToTable("MangaAltTitle");
b1.WithOwner()
.HasForeignKey("MangaId");
});
b.Navigation("AltTitles");
b.Navigation("Library");
b.Navigation("Links");
b.Navigation("MangaConnector");
});
modelBuilder.Entity("AuthorToManga", b =>
{
b.HasOne("API.Schema.Author", null)
.WithMany()
.HasForeignKey("AuthorIds")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("API.Schema.Manga", null)
.WithMany()
.HasForeignKey("MangaIds")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("JobJob", b =>
{
b.HasOne("API.Schema.Jobs.Job", null)
.WithMany()
.HasForeignKey("DependsOnJobsJobId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("API.Schema.Jobs.Job", null)
.WithMany()
.HasForeignKey("JobId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("MangaTagToManga", b =>
{
b.HasOne("API.Schema.Manga", null)
.WithMany()
.HasForeignKey("MangaIds")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("API.Schema.MangaTag", null)
.WithMany()
.HasForeignKey("MangaTagIds")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("API.Schema.Jobs.DownloadAvailableChaptersJob", b =>
{
b.HasOne("API.Schema.Manga", "Manga")
.WithMany()
.HasForeignKey("MangaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Manga");
});
modelBuilder.Entity("API.Schema.Jobs.DownloadMangaCoverJob", b =>
{
b.HasOne("API.Schema.Manga", "Manga")
.WithMany()
.HasForeignKey("MangaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Manga");
});
modelBuilder.Entity("API.Schema.Jobs.DownloadSingleChapterJob", b =>
{
b.HasOne("API.Schema.Chapter", "Chapter")
.WithMany()
.HasForeignKey("ChapterId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Chapter");
});
modelBuilder.Entity("API.Schema.Jobs.MoveMangaLibraryJob", b =>
{
b.HasOne("API.Schema.Manga", "Manga")
.WithMany()
.HasForeignKey("MangaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("API.Schema.LocalLibrary", "ToLibrary")
.WithMany()
.HasForeignKey("ToLibraryId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Manga");
b.Navigation("ToLibrary");
});
modelBuilder.Entity("API.Schema.Jobs.RetrieveChaptersJob", b =>
{
b.HasOne("API.Schema.Manga", "Manga")
.WithMany()
.HasForeignKey("MangaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Manga");
});
modelBuilder.Entity("API.Schema.Jobs.UpdateChaptersDownloadedJob", b =>
{
b.HasOne("API.Schema.Manga", "Manga")
.WithMany()
.HasForeignKey("MangaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Manga");
});
modelBuilder.Entity("API.Schema.Jobs.UpdateCoverJob", b =>
{
b.HasOne("API.Schema.Manga", "Manga")
.WithMany()
.HasForeignKey("MangaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Manga");
});
modelBuilder.Entity("API.Schema.Manga", b =>
{
b.Navigation("Chapters");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -1,50 +0,0 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace API.Migrations.pgsql
{
/// <inheritdoc />
public partial class RemoveUpdateSingleChapterDownloadedJob : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_Jobs_Chapters_UpdateSingleChapterDownloadedJob_ChapterId",
table: "Jobs");
migrationBuilder.DropIndex(
name: "IX_Jobs_UpdateSingleChapterDownloadedJob_ChapterId",
table: "Jobs");
migrationBuilder.DropColumn(
name: "UpdateSingleChapterDownloadedJob_ChapterId",
table: "Jobs");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "UpdateSingleChapterDownloadedJob_ChapterId",
table: "Jobs",
type: "character varying(64)",
maxLength: 64,
nullable: true);
migrationBuilder.CreateIndex(
name: "IX_Jobs_UpdateSingleChapterDownloadedJob_ChapterId",
table: "Jobs",
column: "UpdateSingleChapterDownloadedJob_ChapterId");
migrationBuilder.AddForeignKey(
name: "FK_Jobs_Chapters_UpdateSingleChapterDownloadedJob_ChapterId",
table: "Jobs",
column: "UpdateSingleChapterDownloadedJob_ChapterId",
principalTable: "Chapters",
principalColumn: "ChapterId",
onDelete: ReferentialAction.Cascade);
}
}
}

View File

@ -1,788 +0,0 @@
// <auto-generated />
using System;
using API.Schema.Contexts;
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.pgsql
{
[DbContext(typeof(PgsqlContext))]
[Migration("20250628204956_AddMAL")]
partial class AddMAL
{
/// <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.Author", b =>
{
b.Property<string>("AuthorId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("AuthorName")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("character varying(128)");
b.HasKey("AuthorId");
b.ToTable("Authors");
});
modelBuilder.Entity("API.Schema.Chapter", b =>
{
b.Property<string>("ChapterId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
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>("IdOnConnectorSite")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("ParentMangaId")
.IsRequired()
.HasColumnType("character varying(64)");
b.Property<string>("Title")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("Url")
.IsRequired()
.HasMaxLength(2048)
.HasColumnType("character varying(2048)");
b.Property<int?>("VolumeNumber")
.HasColumnType("integer");
b.HasKey("ChapterId");
b.HasIndex("ParentMangaId");
b.ToTable("Chapters");
});
modelBuilder.Entity("API.Schema.Jobs.Job", b =>
{
b.Property<string>("JobId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<bool>("Enabled")
.HasColumnType("boolean");
b.Property<byte>("JobType")
.HasColumnType("smallint");
b.Property<DateTime>("LastExecution")
.HasColumnType("timestamp with time zone");
b.Property<string>("ParentJobId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<decimal>("RecurrenceMs")
.HasColumnType("numeric(20,0)");
b.Property<byte>("state")
.HasColumnType("smallint");
b.HasKey("JobId");
b.HasIndex("ParentJobId");
b.ToTable("Jobs");
b.HasDiscriminator<byte>("JobType");
b.UseTphMappingStrategy();
});
modelBuilder.Entity("API.Schema.LocalLibrary", b =>
{
b.Property<string>("LocalLibraryId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("BasePath")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("LibraryName")
.IsRequired()
.HasMaxLength(512)
.HasColumnType("character varying(512)");
b.HasKey("LocalLibraryId");
b.ToTable("LocalLibraries");
});
modelBuilder.Entity("API.Schema.Manga", b =>
{
b.Property<string>("MangaId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
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<string>("IdOnConnectorSite")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<float>("IgnoreChaptersBefore")
.HasColumnType("real");
b.Property<string>("LibraryId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("MangaConnectorName")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("character varying(32)");
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<string>("WebsiteUrl")
.IsRequired()
.HasMaxLength(512)
.HasColumnType("character varying(512)");
b.Property<long?>("Year")
.HasColumnType("bigint");
b.HasKey("MangaId");
b.HasIndex("LibraryId");
b.HasIndex("MangaConnectorName");
b.ToTable("Mangas");
});
modelBuilder.Entity("API.Schema.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.MangaTag", b =>
{
b.Property<string>("Tag")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.HasKey("Tag");
b.ToTable("Tags");
});
modelBuilder.Entity("API.Schema.MetadataFetchers.MetadataEntry", b =>
{
b.Property<string>("MangaId")
.HasColumnType("character varying(64)");
b.Property<string>("MetadataFetcherName")
.HasColumnType("text");
b.Property<string>("Identifier")
.IsRequired()
.HasColumnType("text");
b.HasKey("MangaId", "MetadataFetcherName");
b.HasIndex("MetadataFetcherName");
b.ToTable("MetadataEntries");
});
modelBuilder.Entity("API.Schema.MetadataFetchers.MetadataFetcher", b =>
{
b.Property<string>("MetadataFetcherName")
.HasColumnType("text");
b.Property<string>("MetadataEntry")
.IsRequired()
.HasMaxLength(21)
.HasColumnType("character varying(21)");
b.HasKey("MetadataFetcherName");
b.ToTable("MetadataFetcher");
b.HasDiscriminator<string>("MetadataEntry").HasValue("MetadataFetcher");
b.UseTphMappingStrategy();
});
modelBuilder.Entity("AuthorToManga", b =>
{
b.Property<string>("AuthorIds")
.HasColumnType("character varying(64)");
b.Property<string>("MangaIds")
.HasColumnType("character varying(64)");
b.HasKey("AuthorIds", "MangaIds");
b.HasIndex("MangaIds");
b.ToTable("AuthorToManga");
});
modelBuilder.Entity("JobJob", b =>
{
b.Property<string>("DependsOnJobsJobId")
.HasColumnType("character varying(64)");
b.Property<string>("JobId")
.HasColumnType("character varying(64)");
b.HasKey("DependsOnJobsJobId", "JobId");
b.HasIndex("JobId");
b.ToTable("JobJob");
});
modelBuilder.Entity("MangaTagToManga", b =>
{
b.Property<string>("MangaTagIds")
.HasColumnType("character varying(64)");
b.Property<string>("MangaIds")
.HasColumnType("character varying(64)");
b.HasKey("MangaTagIds", "MangaIds");
b.HasIndex("MangaIds");
b.ToTable("MangaTagToManga");
});
modelBuilder.Entity("API.Schema.Jobs.DownloadAvailableChaptersJob", b =>
{
b.HasBaseType("API.Schema.Jobs.Job");
b.Property<string>("MangaId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.HasIndex("MangaId");
b.ToTable("Jobs", t =>
{
t.Property("MangaId")
.HasColumnName("DownloadAvailableChaptersJob_MangaId");
});
b.HasDiscriminator().HasValue((byte)1);
});
modelBuilder.Entity("API.Schema.Jobs.DownloadMangaCoverJob", b =>
{
b.HasBaseType("API.Schema.Jobs.Job");
b.Property<string>("MangaId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.HasIndex("MangaId");
b.HasDiscriminator().HasValue((byte)4);
});
modelBuilder.Entity("API.Schema.Jobs.DownloadSingleChapterJob", b =>
{
b.HasBaseType("API.Schema.Jobs.Job");
b.Property<string>("ChapterId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.HasIndex("ChapterId");
b.HasDiscriminator().HasValue((byte)0);
});
modelBuilder.Entity("API.Schema.Jobs.MoveFileOrFolderJob", b =>
{
b.HasBaseType("API.Schema.Jobs.Job");
b.Property<string>("FromLocation")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("ToLocation")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.HasDiscriminator().HasValue((byte)3);
});
modelBuilder.Entity("API.Schema.Jobs.MoveMangaLibraryJob", b =>
{
b.HasBaseType("API.Schema.Jobs.Job");
b.Property<string>("MangaId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("ToLibraryId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.HasIndex("MangaId");
b.HasIndex("ToLibraryId");
b.ToTable("Jobs", t =>
{
t.Property("MangaId")
.HasColumnName("MoveMangaLibraryJob_MangaId");
});
b.HasDiscriminator().HasValue((byte)7);
});
modelBuilder.Entity("API.Schema.Jobs.RetrieveChaptersJob", b =>
{
b.HasBaseType("API.Schema.Jobs.Job");
b.Property<string>("Language")
.IsRequired()
.HasMaxLength(8)
.HasColumnType("character varying(8)");
b.Property<string>("MangaId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.HasIndex("MangaId");
b.ToTable("Jobs", t =>
{
t.Property("MangaId")
.HasColumnName("RetrieveChaptersJob_MangaId");
});
b.HasDiscriminator().HasValue((byte)5);
});
modelBuilder.Entity("API.Schema.Jobs.UpdateChaptersDownloadedJob", b =>
{
b.HasBaseType("API.Schema.Jobs.Job");
b.Property<string>("MangaId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.HasIndex("MangaId");
b.ToTable("Jobs", t =>
{
t.Property("MangaId")
.HasColumnName("UpdateChaptersDownloadedJob_MangaId");
});
b.HasDiscriminator().HasValue((byte)6);
});
modelBuilder.Entity("API.Schema.Jobs.UpdateCoverJob", b =>
{
b.HasBaseType("API.Schema.Jobs.Job");
b.Property<string>("MangaId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.HasIndex("MangaId");
b.ToTable("Jobs", t =>
{
t.Property("MangaId")
.HasColumnName("UpdateCoverJob_MangaId");
});
b.HasDiscriminator().HasValue((byte)9);
});
modelBuilder.Entity("API.Schema.MangaConnectors.ComickIo", b =>
{
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
b.HasDiscriminator().HasValue("ComickIo");
});
modelBuilder.Entity("API.Schema.MangaConnectors.Global", b =>
{
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
b.HasDiscriminator().HasValue("Global");
});
modelBuilder.Entity("API.Schema.MangaConnectors.MangaDex", b =>
{
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
b.HasDiscriminator().HasValue("MangaDex");
});
modelBuilder.Entity("API.Schema.MetadataFetchers.MyAnimeList", b =>
{
b.HasBaseType("API.Schema.MetadataFetchers.MetadataFetcher");
b.HasDiscriminator().HasValue("MyAnimeList");
});
modelBuilder.Entity("API.Schema.Chapter", b =>
{
b.HasOne("API.Schema.Manga", "ParentManga")
.WithMany("Chapters")
.HasForeignKey("ParentMangaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("ParentManga");
});
modelBuilder.Entity("API.Schema.Jobs.Job", b =>
{
b.HasOne("API.Schema.Jobs.Job", "ParentJob")
.WithMany()
.HasForeignKey("ParentJobId")
.OnDelete(DeleteBehavior.Cascade);
b.Navigation("ParentJob");
});
modelBuilder.Entity("API.Schema.Manga", b =>
{
b.HasOne("API.Schema.LocalLibrary", "Library")
.WithMany()
.HasForeignKey("LibraryId")
.OnDelete(DeleteBehavior.SetNull);
b.HasOne("API.Schema.MangaConnectors.MangaConnector", "MangaConnector")
.WithMany()
.HasForeignKey("MangaConnectorName")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.OwnsMany("API.Schema.Link", "Links", b1 =>
{
b1.Property<string>("LinkId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
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>("MangaId")
.IsRequired()
.HasColumnType("character varying(64)");
b1.HasKey("LinkId");
b1.HasIndex("MangaId");
b1.ToTable("Link");
b1.WithOwner()
.HasForeignKey("MangaId");
});
b.OwnsMany("API.Schema.MangaAltTitle", "AltTitles", b1 =>
{
b1.Property<string>("AltTitleId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b1.Property<string>("Language")
.IsRequired()
.HasMaxLength(8)
.HasColumnType("character varying(8)");
b1.Property<string>("MangaId")
.IsRequired()
.HasColumnType("character varying(64)");
b1.Property<string>("Title")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b1.HasKey("AltTitleId");
b1.HasIndex("MangaId");
b1.ToTable("MangaAltTitle");
b1.WithOwner()
.HasForeignKey("MangaId");
});
b.Navigation("AltTitles");
b.Navigation("Library");
b.Navigation("Links");
b.Navigation("MangaConnector");
});
modelBuilder.Entity("API.Schema.MetadataFetchers.MetadataEntry", b =>
{
b.HasOne("API.Schema.Manga", "Manga")
.WithMany()
.HasForeignKey("MangaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("API.Schema.MetadataFetchers.MetadataFetcher", "MetadataFetcher")
.WithMany()
.HasForeignKey("MetadataFetcherName")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Manga");
b.Navigation("MetadataFetcher");
});
modelBuilder.Entity("AuthorToManga", b =>
{
b.HasOne("API.Schema.Author", null)
.WithMany()
.HasForeignKey("AuthorIds")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("API.Schema.Manga", null)
.WithMany()
.HasForeignKey("MangaIds")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("JobJob", b =>
{
b.HasOne("API.Schema.Jobs.Job", null)
.WithMany()
.HasForeignKey("DependsOnJobsJobId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("API.Schema.Jobs.Job", null)
.WithMany()
.HasForeignKey("JobId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("MangaTagToManga", b =>
{
b.HasOne("API.Schema.Manga", null)
.WithMany()
.HasForeignKey("MangaIds")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("API.Schema.MangaTag", null)
.WithMany()
.HasForeignKey("MangaTagIds")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("API.Schema.Jobs.DownloadAvailableChaptersJob", b =>
{
b.HasOne("API.Schema.Manga", "Manga")
.WithMany()
.HasForeignKey("MangaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Manga");
});
modelBuilder.Entity("API.Schema.Jobs.DownloadMangaCoverJob", b =>
{
b.HasOne("API.Schema.Manga", "Manga")
.WithMany()
.HasForeignKey("MangaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Manga");
});
modelBuilder.Entity("API.Schema.Jobs.DownloadSingleChapterJob", b =>
{
b.HasOne("API.Schema.Chapter", "Chapter")
.WithMany()
.HasForeignKey("ChapterId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Chapter");
});
modelBuilder.Entity("API.Schema.Jobs.MoveMangaLibraryJob", b =>
{
b.HasOne("API.Schema.Manga", "Manga")
.WithMany()
.HasForeignKey("MangaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("API.Schema.LocalLibrary", "ToLibrary")
.WithMany()
.HasForeignKey("ToLibraryId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Manga");
b.Navigation("ToLibrary");
});
modelBuilder.Entity("API.Schema.Jobs.RetrieveChaptersJob", b =>
{
b.HasOne("API.Schema.Manga", "Manga")
.WithMany()
.HasForeignKey("MangaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Manga");
});
modelBuilder.Entity("API.Schema.Jobs.UpdateChaptersDownloadedJob", b =>
{
b.HasOne("API.Schema.Manga", "Manga")
.WithMany()
.HasForeignKey("MangaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Manga");
});
modelBuilder.Entity("API.Schema.Jobs.UpdateCoverJob", b =>
{
b.HasOne("API.Schema.Manga", "Manga")
.WithMany()
.HasForeignKey("MangaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Manga");
});
modelBuilder.Entity("API.Schema.Manga", b =>
{
b.Navigation("Chapters");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -1,66 +0,0 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace API.Migrations.pgsql
{
/// <inheritdoc />
public partial class AddMAL : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "MetadataFetcher",
columns: table => new
{
MetadataFetcherName = 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.MetadataFetcherName);
});
migrationBuilder.CreateTable(
name: "MetadataEntries",
columns: table => new
{
MangaId = table.Column<string>(type: "character varying(64)", nullable: false),
MetadataFetcherName = table.Column<string>(type: "text", nullable: false),
Identifier = table.Column<string>(type: "text", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_MetadataEntries", x => new { x.MangaId, x.MetadataFetcherName });
table.ForeignKey(
name: "FK_MetadataEntries_Mangas_MangaId",
column: x => x.MangaId,
principalTable: "Mangas",
principalColumn: "MangaId",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_MetadataEntries_MetadataFetcher_MetadataFetcherName",
column: x => x.MetadataFetcherName,
principalTable: "MetadataFetcher",
principalColumn: "MetadataFetcherName",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_MetadataEntries_MetadataFetcherName",
table: "MetadataEntries",
column: "MetadataFetcherName");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "MetadataEntries");
migrationBuilder.DropTable(
name: "MetadataFetcher");
}
}
}

View File

@ -1,788 +0,0 @@
// <auto-generated />
using System;
using API.Schema.Contexts;
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.pgsql
{
[DbContext(typeof(PgsqlContext))]
[Migration("20250629184056_MetadataEntry-PrimaryKeyChange")]
partial class MetadataEntryPrimaryKeyChange
{
/// <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.Author", b =>
{
b.Property<string>("AuthorId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("AuthorName")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("character varying(128)");
b.HasKey("AuthorId");
b.ToTable("Authors");
});
modelBuilder.Entity("API.Schema.Chapter", b =>
{
b.Property<string>("ChapterId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
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>("IdOnConnectorSite")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("ParentMangaId")
.IsRequired()
.HasColumnType("character varying(64)");
b.Property<string>("Title")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("Url")
.IsRequired()
.HasMaxLength(2048)
.HasColumnType("character varying(2048)");
b.Property<int?>("VolumeNumber")
.HasColumnType("integer");
b.HasKey("ChapterId");
b.HasIndex("ParentMangaId");
b.ToTable("Chapters");
});
modelBuilder.Entity("API.Schema.Jobs.Job", b =>
{
b.Property<string>("JobId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<bool>("Enabled")
.HasColumnType("boolean");
b.Property<byte>("JobType")
.HasColumnType("smallint");
b.Property<DateTime>("LastExecution")
.HasColumnType("timestamp with time zone");
b.Property<string>("ParentJobId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<decimal>("RecurrenceMs")
.HasColumnType("numeric(20,0)");
b.Property<byte>("state")
.HasColumnType("smallint");
b.HasKey("JobId");
b.HasIndex("ParentJobId");
b.ToTable("Jobs");
b.HasDiscriminator<byte>("JobType");
b.UseTphMappingStrategy();
});
modelBuilder.Entity("API.Schema.LocalLibrary", b =>
{
b.Property<string>("LocalLibraryId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("BasePath")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("LibraryName")
.IsRequired()
.HasMaxLength(512)
.HasColumnType("character varying(512)");
b.HasKey("LocalLibraryId");
b.ToTable("LocalLibraries");
});
modelBuilder.Entity("API.Schema.Manga", b =>
{
b.Property<string>("MangaId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
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<string>("IdOnConnectorSite")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<float>("IgnoreChaptersBefore")
.HasColumnType("real");
b.Property<string>("LibraryId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("MangaConnectorName")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("character varying(32)");
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<string>("WebsiteUrl")
.IsRequired()
.HasMaxLength(512)
.HasColumnType("character varying(512)");
b.Property<long?>("Year")
.HasColumnType("bigint");
b.HasKey("MangaId");
b.HasIndex("LibraryId");
b.HasIndex("MangaConnectorName");
b.ToTable("Mangas");
});
modelBuilder.Entity("API.Schema.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.MangaTag", b =>
{
b.Property<string>("Tag")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.HasKey("Tag");
b.ToTable("Tags");
});
modelBuilder.Entity("API.Schema.MetadataFetchers.MetadataEntry", b =>
{
b.Property<string>("MetadataFetcherName")
.HasColumnType("text");
b.Property<string>("Identifier")
.HasColumnType("text");
b.Property<string>("MangaId")
.IsRequired()
.HasColumnType("character varying(64)");
b.HasKey("MetadataFetcherName", "Identifier");
b.HasIndex("MangaId");
b.ToTable("MetadataEntries");
});
modelBuilder.Entity("API.Schema.MetadataFetchers.MetadataFetcher", b =>
{
b.Property<string>("MetadataFetcherName")
.HasColumnType("text");
b.Property<string>("MetadataEntry")
.IsRequired()
.HasMaxLength(21)
.HasColumnType("character varying(21)");
b.HasKey("MetadataFetcherName");
b.ToTable("MetadataFetcher");
b.HasDiscriminator<string>("MetadataEntry").HasValue("MetadataFetcher");
b.UseTphMappingStrategy();
});
modelBuilder.Entity("AuthorToManga", b =>
{
b.Property<string>("AuthorIds")
.HasColumnType("character varying(64)");
b.Property<string>("MangaIds")
.HasColumnType("character varying(64)");
b.HasKey("AuthorIds", "MangaIds");
b.HasIndex("MangaIds");
b.ToTable("AuthorToManga");
});
modelBuilder.Entity("JobJob", b =>
{
b.Property<string>("DependsOnJobsJobId")
.HasColumnType("character varying(64)");
b.Property<string>("JobId")
.HasColumnType("character varying(64)");
b.HasKey("DependsOnJobsJobId", "JobId");
b.HasIndex("JobId");
b.ToTable("JobJob");
});
modelBuilder.Entity("MangaTagToManga", b =>
{
b.Property<string>("MangaTagIds")
.HasColumnType("character varying(64)");
b.Property<string>("MangaIds")
.HasColumnType("character varying(64)");
b.HasKey("MangaTagIds", "MangaIds");
b.HasIndex("MangaIds");
b.ToTable("MangaTagToManga");
});
modelBuilder.Entity("API.Schema.Jobs.DownloadAvailableChaptersJob", b =>
{
b.HasBaseType("API.Schema.Jobs.Job");
b.Property<string>("MangaId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.HasIndex("MangaId");
b.ToTable("Jobs", t =>
{
t.Property("MangaId")
.HasColumnName("DownloadAvailableChaptersJob_MangaId");
});
b.HasDiscriminator().HasValue((byte)1);
});
modelBuilder.Entity("API.Schema.Jobs.DownloadMangaCoverJob", b =>
{
b.HasBaseType("API.Schema.Jobs.Job");
b.Property<string>("MangaId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.HasIndex("MangaId");
b.HasDiscriminator().HasValue((byte)4);
});
modelBuilder.Entity("API.Schema.Jobs.DownloadSingleChapterJob", b =>
{
b.HasBaseType("API.Schema.Jobs.Job");
b.Property<string>("ChapterId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.HasIndex("ChapterId");
b.HasDiscriminator().HasValue((byte)0);
});
modelBuilder.Entity("API.Schema.Jobs.MoveFileOrFolderJob", b =>
{
b.HasBaseType("API.Schema.Jobs.Job");
b.Property<string>("FromLocation")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("ToLocation")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.HasDiscriminator().HasValue((byte)3);
});
modelBuilder.Entity("API.Schema.Jobs.MoveMangaLibraryJob", b =>
{
b.HasBaseType("API.Schema.Jobs.Job");
b.Property<string>("MangaId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("ToLibraryId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.HasIndex("MangaId");
b.HasIndex("ToLibraryId");
b.ToTable("Jobs", t =>
{
t.Property("MangaId")
.HasColumnName("MoveMangaLibraryJob_MangaId");
});
b.HasDiscriminator().HasValue((byte)7);
});
modelBuilder.Entity("API.Schema.Jobs.RetrieveChaptersJob", b =>
{
b.HasBaseType("API.Schema.Jobs.Job");
b.Property<string>("Language")
.IsRequired()
.HasMaxLength(8)
.HasColumnType("character varying(8)");
b.Property<string>("MangaId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.HasIndex("MangaId");
b.ToTable("Jobs", t =>
{
t.Property("MangaId")
.HasColumnName("RetrieveChaptersJob_MangaId");
});
b.HasDiscriminator().HasValue((byte)5);
});
modelBuilder.Entity("API.Schema.Jobs.UpdateChaptersDownloadedJob", b =>
{
b.HasBaseType("API.Schema.Jobs.Job");
b.Property<string>("MangaId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.HasIndex("MangaId");
b.ToTable("Jobs", t =>
{
t.Property("MangaId")
.HasColumnName("UpdateChaptersDownloadedJob_MangaId");
});
b.HasDiscriminator().HasValue((byte)6);
});
modelBuilder.Entity("API.Schema.Jobs.UpdateCoverJob", b =>
{
b.HasBaseType("API.Schema.Jobs.Job");
b.Property<string>("MangaId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.HasIndex("MangaId");
b.ToTable("Jobs", t =>
{
t.Property("MangaId")
.HasColumnName("UpdateCoverJob_MangaId");
});
b.HasDiscriminator().HasValue((byte)9);
});
modelBuilder.Entity("API.Schema.MangaConnectors.ComickIo", b =>
{
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
b.HasDiscriminator().HasValue("ComickIo");
});
modelBuilder.Entity("API.Schema.MangaConnectors.Global", b =>
{
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
b.HasDiscriminator().HasValue("Global");
});
modelBuilder.Entity("API.Schema.MangaConnectors.MangaDex", b =>
{
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
b.HasDiscriminator().HasValue("MangaDex");
});
modelBuilder.Entity("API.Schema.MetadataFetchers.MyAnimeList", b =>
{
b.HasBaseType("API.Schema.MetadataFetchers.MetadataFetcher");
b.HasDiscriminator().HasValue("MyAnimeList");
});
modelBuilder.Entity("API.Schema.Chapter", b =>
{
b.HasOne("API.Schema.Manga", "ParentManga")
.WithMany("Chapters")
.HasForeignKey("ParentMangaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("ParentManga");
});
modelBuilder.Entity("API.Schema.Jobs.Job", b =>
{
b.HasOne("API.Schema.Jobs.Job", "ParentJob")
.WithMany()
.HasForeignKey("ParentJobId")
.OnDelete(DeleteBehavior.Cascade);
b.Navigation("ParentJob");
});
modelBuilder.Entity("API.Schema.Manga", b =>
{
b.HasOne("API.Schema.LocalLibrary", "Library")
.WithMany()
.HasForeignKey("LibraryId")
.OnDelete(DeleteBehavior.SetNull);
b.HasOne("API.Schema.MangaConnectors.MangaConnector", "MangaConnector")
.WithMany()
.HasForeignKey("MangaConnectorName")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.OwnsMany("API.Schema.Link", "Links", b1 =>
{
b1.Property<string>("LinkId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
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>("MangaId")
.IsRequired()
.HasColumnType("character varying(64)");
b1.HasKey("LinkId");
b1.HasIndex("MangaId");
b1.ToTable("Link");
b1.WithOwner()
.HasForeignKey("MangaId");
});
b.OwnsMany("API.Schema.MangaAltTitle", "AltTitles", b1 =>
{
b1.Property<string>("AltTitleId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b1.Property<string>("Language")
.IsRequired()
.HasMaxLength(8)
.HasColumnType("character varying(8)");
b1.Property<string>("MangaId")
.IsRequired()
.HasColumnType("character varying(64)");
b1.Property<string>("Title")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b1.HasKey("AltTitleId");
b1.HasIndex("MangaId");
b1.ToTable("MangaAltTitle");
b1.WithOwner()
.HasForeignKey("MangaId");
});
b.Navigation("AltTitles");
b.Navigation("Library");
b.Navigation("Links");
b.Navigation("MangaConnector");
});
modelBuilder.Entity("API.Schema.MetadataFetchers.MetadataEntry", b =>
{
b.HasOne("API.Schema.Manga", "Manga")
.WithMany()
.HasForeignKey("MangaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("API.Schema.MetadataFetchers.MetadataFetcher", "MetadataFetcher")
.WithMany()
.HasForeignKey("MetadataFetcherName")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Manga");
b.Navigation("MetadataFetcher");
});
modelBuilder.Entity("AuthorToManga", b =>
{
b.HasOne("API.Schema.Author", null)
.WithMany()
.HasForeignKey("AuthorIds")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("API.Schema.Manga", null)
.WithMany()
.HasForeignKey("MangaIds")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("JobJob", b =>
{
b.HasOne("API.Schema.Jobs.Job", null)
.WithMany()
.HasForeignKey("DependsOnJobsJobId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("API.Schema.Jobs.Job", null)
.WithMany()
.HasForeignKey("JobId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("MangaTagToManga", b =>
{
b.HasOne("API.Schema.Manga", null)
.WithMany()
.HasForeignKey("MangaIds")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("API.Schema.MangaTag", null)
.WithMany()
.HasForeignKey("MangaTagIds")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("API.Schema.Jobs.DownloadAvailableChaptersJob", b =>
{
b.HasOne("API.Schema.Manga", "Manga")
.WithMany()
.HasForeignKey("MangaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Manga");
});
modelBuilder.Entity("API.Schema.Jobs.DownloadMangaCoverJob", b =>
{
b.HasOne("API.Schema.Manga", "Manga")
.WithMany()
.HasForeignKey("MangaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Manga");
});
modelBuilder.Entity("API.Schema.Jobs.DownloadSingleChapterJob", b =>
{
b.HasOne("API.Schema.Chapter", "Chapter")
.WithMany()
.HasForeignKey("ChapterId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Chapter");
});
modelBuilder.Entity("API.Schema.Jobs.MoveMangaLibraryJob", b =>
{
b.HasOne("API.Schema.Manga", "Manga")
.WithMany()
.HasForeignKey("MangaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("API.Schema.LocalLibrary", "ToLibrary")
.WithMany()
.HasForeignKey("ToLibraryId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Manga");
b.Navigation("ToLibrary");
});
modelBuilder.Entity("API.Schema.Jobs.RetrieveChaptersJob", b =>
{
b.HasOne("API.Schema.Manga", "Manga")
.WithMany()
.HasForeignKey("MangaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Manga");
});
modelBuilder.Entity("API.Schema.Jobs.UpdateChaptersDownloadedJob", b =>
{
b.HasOne("API.Schema.Manga", "Manga")
.WithMany()
.HasForeignKey("MangaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Manga");
});
modelBuilder.Entity("API.Schema.Jobs.UpdateCoverJob", b =>
{
b.HasOne("API.Schema.Manga", "Manga")
.WithMany()
.HasForeignKey("MangaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Manga");
});
modelBuilder.Entity("API.Schema.Manga", b =>
{
b.Navigation("Chapters");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -1,54 +0,0 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace API.Migrations.pgsql
{
/// <inheritdoc />
public partial class MetadataEntryPrimaryKeyChange : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropPrimaryKey(
name: "PK_MetadataEntries",
table: "MetadataEntries");
migrationBuilder.DropIndex(
name: "IX_MetadataEntries_MetadataFetcherName",
table: "MetadataEntries");
migrationBuilder.AddPrimaryKey(
name: "PK_MetadataEntries",
table: "MetadataEntries",
columns: new[] { "MetadataFetcherName", "Identifier" });
migrationBuilder.CreateIndex(
name: "IX_MetadataEntries_MangaId",
table: "MetadataEntries",
column: "MangaId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropPrimaryKey(
name: "PK_MetadataEntries",
table: "MetadataEntries");
migrationBuilder.DropIndex(
name: "IX_MetadataEntries_MangaId",
table: "MetadataEntries");
migrationBuilder.AddPrimaryKey(
name: "PK_MetadataEntries",
table: "MetadataEntries",
columns: new[] { "MangaId", "MetadataFetcherName" });
migrationBuilder.CreateIndex(
name: "IX_MetadataEntries_MetadataFetcherName",
table: "MetadataEntries",
column: "MetadataFetcherName");
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,10 +1,9 @@
using System.Reflection; using System.Reflection;
using API; using API;
using API.Controllers; using API.Schema.LibraryContext;
using API.Schema; using API.Schema.MangaContext;
using API.Schema.Contexts; using API.Schema.MangaContext.MangaConnectors;
using API.Schema.Jobs; using API.Schema.NotificationsContext;
using API.Schema.MangaConnectors;
using Asp.Versioning; using Asp.Versioning;
using Asp.Versioning.Builder; using Asp.Versioning.Builder;
using Asp.Versioning.Conventions; using Asp.Versioning.Conventions;
@ -56,17 +55,17 @@ builder.Services.AddSwaggerGen(opt =>
}); });
builder.Services.ConfigureOptions<NamedSwaggerGenOptions>(); builder.Services.ConfigureOptions<NamedSwaggerGenOptions>();
string ConnectionString = $"Host={Environment.GetEnvironmentVariable("POSTGRES_HOST") ?? "localhost:5432"}; " + string connectionString = $"Host={Environment.GetEnvironmentVariable("POSTGRES_HOST") ?? "localhost:5432"}; " +
$"Database={Environment.GetEnvironmentVariable("POSTGRES_DB") ?? "postgres"}; " + $"Database={Environment.GetEnvironmentVariable("POSTGRES_DB") ?? "postgres"}; " +
$"Username={Environment.GetEnvironmentVariable("POSTGRES_USER") ?? "postgres"}; " + $"Username={Environment.GetEnvironmentVariable("POSTGRES_USER") ?? "postgres"}; " +
$"Password={Environment.GetEnvironmentVariable("POSTGRES_PASSWORD") ?? "postgres"}"; $"Password={Environment.GetEnvironmentVariable("POSTGRES_PASSWORD") ?? "postgres"}";
builder.Services.AddDbContext<PgsqlContext>(options => builder.Services.AddDbContext<MangaContext>(options =>
options.UseNpgsql(ConnectionString)); options.UseNpgsql(connectionString));
builder.Services.AddDbContext<NotificationsContext>(options => builder.Services.AddDbContext<NotificationsContext>(options =>
options.UseNpgsql(ConnectionString)); options.UseNpgsql(connectionString));
builder.Services.AddDbContext<LibraryContext>(options => builder.Services.AddDbContext<LibraryContext>(options =>
options.UseNpgsql(ConnectionString)); options.UseNpgsql(connectionString));
builder.Services.AddControllers(options => builder.Services.AddControllers(options =>
{ {
@ -98,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();
@ -108,76 +106,46 @@ app.UseMiddleware<RequestTimeMiddleware>();
using (IServiceScope scope = app.Services.CreateScope()) using (IServiceScope scope = app.Services.CreateScope())
{ {
PgsqlContext context = scope.ServiceProvider.GetRequiredService<PgsqlContext>(); MangaContext context = scope.ServiceProvider.GetRequiredService<MangaContext>();
//TODO Remove after migrations complete context.Database.Migrate();
if (context.Database.GetMigrations().Contains("20250630182650_OofV2.1") == false)
{
IQueryable<(string, string)> mangas = context.Database.SqlQuery<(string, string)>($"SELECT MangaConnectorName, IdOnConnectorSite as ID FROM Mangas");
context.Database.Migrate();
foreach ((string mangaConnectorName, string idOnConnectorSite) manga in mangas)
{
if(context.MangaConnectors.Find(manga.mangaConnectorName) is not { } mangaConnector)
continue;
if(mangaConnector.GetMangaFromId(manga.idOnConnectorSite) is not { } result)
continue;
if (SearchController.AddMangaToContext(result.Item1, result.Item2, context) is { } added)
{
RetrieveChaptersJob retrieveChaptersJob = new (added, "en", 0);
UpdateChaptersDownloadedJob update = new(added, 0, null, [retrieveChaptersJob]);
context.Jobs.AddRange([retrieveChaptersJob, update]);
}
}
} else
context.Database.Migrate();
MangaConnector[] connectors = MangaConnector[] connectors =
[ [
new MangaDex(), new MangaDex(),
new ComickIo(), new ComickIo(),
new Global(scope.ServiceProvider.GetService<PgsqlContext>()!) new Global(scope.ServiceProvider.GetService<MangaContext>()!)
]; ];
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.Jobs.AddRange(context.Jobs.Where(j => j.JobType == JobType.DownloadAvailableChaptersJob)
.Include(downloadAvailableChaptersJob => ((DownloadAvailableChaptersJob)downloadAvailableChaptersJob).Manga)
.ToList()
.Select(dacj => new UpdateChaptersDownloadedJob(((DownloadAvailableChaptersJob)dacj).Manga, 0, dacj)));
context.Jobs.RemoveRange(context.Jobs.Where(j => j.state == JobState.Completed && j.RecurrenceMs < 1));
foreach (Job job in context.Jobs.Where(j => j.state == JobState.Running))
{
job.state = JobState.FirstExecution;
job.LastExecution = DateTime.UnixEpoch;
}
context.SaveChanges(); context.Sync();
} }
using (IServiceScope scope = app.Services.CreateScope()) 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));
context.SaveChanges(); context.Sync();
} }
TrangaSettings.Load();
Tranga.StartLogger();
using (IServiceScope scope = app.Services.CreateScope()) using (IServiceScope scope = app.Services.CreateScope())
{ {
PgsqlContext context = scope.ServiceProvider.GetRequiredService<PgsqlContext>(); LibraryContext context = scope.ServiceProvider.GetRequiredService<LibraryContext>();
Tranga.RemoveStaleFiles(context); context.Database.Migrate();
context.Sync();
} }
Tranga.JobStarterThread.Start(app.Services);
//Tranga.NotificationSenderThread.Start(app.Services); //TODO RE-ENABLE Tranga.StartLogger();
Tranga.PeriodicWorkerStarterThread.Start(app.Services);
app.UseCors("AllowAll"); app.UseCors("AllowAll");

View File

@ -1,32 +0,0 @@
using API.Schema.LibraryConnectors;
using log4net;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Diagnostics;
namespace API.Schema.Contexts;
public class LibraryContext(DbContextOptions<LibraryContext> options) : DbContext(options)
{
public DbSet<LibraryConnector> LibraryConnectors { get; set; }
private ILog Log => LogManager.GetLogger(GetType());
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
base.OnConfiguring(optionsBuilder);
optionsBuilder.EnableSensitiveDataLogging();
optionsBuilder.LogTo(s =>
{
Log.Debug(s);
}, [DbLoggerCategory.Query.Name], LogLevel.Trace, DbContextLoggerOptions.Level | DbContextLoggerOptions.Category);
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
//LibraryConnector Types
modelBuilder.Entity<LibraryConnector>()
.HasDiscriminator(l => l.LibraryType)
.HasValue<Komga>(LibraryType.Komga)
.HasValue<Kavita>(LibraryType.Kavita);
}
}

View File

@ -1,23 +0,0 @@
using API.Schema.NotificationConnectors;
using log4net;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Diagnostics;
namespace API.Schema.Contexts;
public class NotificationsContext(DbContextOptions<NotificationsContext> options) : DbContext(options)
{
public DbSet<NotificationConnector> NotificationConnectors { get; set; }
public DbSet<Notification> Notifications { get; set; }
private ILog Log => LogManager.GetLogger(GetType());
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
base.OnConfiguring(optionsBuilder);
optionsBuilder.EnableSensitiveDataLogging();
optionsBuilder.LogTo(s =>
{
Log.Debug(s);
}, [DbLoggerCategory.Query.Name], LogLevel.Trace, DbContextLoggerOptions.Level | DbContextLoggerOptions.Category);
}
}

View File

@ -3,9 +3,19 @@ using Microsoft.EntityFrameworkCore;
namespace API.Schema; namespace API.Schema;
[PrimaryKey("Key")] [PrimaryKey("Key")]
public abstract class Identifiable(string key) public abstract class Identifiable
{ {
public string Key { get; init; } = key; public Identifiable()
{
this.Key = TokenGen.CreateToken(this.GetType());
}
public Identifiable(string key)
{
this.Key = key;
}
public string Key { get; init; }
public override string ToString() => Key; public override string ToString() => Key;
} }

View File

@ -1,50 +0,0 @@
using System.ComponentModel.DataAnnotations;
using API.Schema.Contexts;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Newtonsoft.Json;
namespace API.Schema.Jobs;
public class DownloadAvailableChaptersJob : JobWithDownloading
{
[StringLength(64)] [Required] public string MangaId { get; init; } = null!;
private Manga? _manga;
[JsonIgnore]
public Manga Manga
{
get => LazyLoader.Load(this, ref _manga) ?? throw new InvalidOperationException();
init
{
MangaId = value.Key;
_manga = value;
}
}
public DownloadAvailableChaptersJob(Manga manga, ulong recurrenceMs, Job? parentJob = null, ICollection<Job>? dependsOnJobs = null)
: base(TokenGen.CreateToken(typeof(DownloadAvailableChaptersJob)), JobType.DownloadAvailableChaptersJob, recurrenceMs, parentJob, dependsOnJobs)
{
this.Manga = manga;
}
/// <summary>
/// EF ONLY!!!
/// </summary>
internal DownloadAvailableChaptersJob(ILazyLoader lazyLoader, string key, string mangaId, ulong recurrenceMs, string? parentJobId)
: base(lazyLoader, key, JobType.DownloadAvailableChaptersJob, recurrenceMs, parentJobId)
{
this.MangaId = mangaId;
}
protected override IEnumerable<Job> RunInternal(PgsqlContext context)
{
// Chapters that aren't downloaded and for which no downloading-Job exists
IEnumerable<Chapter> newChapters = Manga.Chapters
.Where(c =>
c.Downloaded == false &&
context.Jobs.Any(j =>
j.JobType == JobType.DownloadSingleChapterJob &&
((DownloadSingleChapterJob)j).Chapter.ParentMangaId == MangaId) == false);
return newChapters.Select(c => new DownloadSingleChapterJob(c, this));
}
}

View File

@ -1,55 +0,0 @@
using System.ComponentModel.DataAnnotations;
using API.Schema.Contexts;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Newtonsoft.Json;
namespace API.Schema.Jobs;
public class DownloadMangaCoverJob : JobWithDownloading
{
[StringLength(64)] [Required] public string MangaId { get; init; } = null!;
private Manga? _manga;
[JsonIgnore]
public Manga Manga
{
get => LazyLoader.Load(this, ref _manga) ?? throw new InvalidOperationException();
init
{
MangaId = value.Key;
_manga = value;
}
}
public DownloadMangaCoverJob(Manga manga, Job? parentJob = null, ICollection<Job>? dependsOnJobs = null)
: base(TokenGen.CreateToken(typeof(DownloadMangaCoverJob)), JobType.DownloadMangaCoverJob, 0, parentJob, dependsOnJobs)
{
this.Manga = manga;
}
/// <summary>
/// EF ONLY!!!
/// </summary>
internal DownloadMangaCoverJob(ILazyLoader lazyLoader, string key, string mangaId, ulong recurrenceMs, string? parentJobId)
: base(lazyLoader, key, JobType.DownloadMangaCoverJob, recurrenceMs, parentJobId)
{
this.MangaId = mangaId;
}
protected override IEnumerable<Job> RunInternal(PgsqlContext context)
{
//TODO MangaConnector Selection
MangaConnectorId<Manga> mcId = Manga.MangaConnectorIds.First();
try
{
Manga.CoverFileNameInCache = mcId.MangaConnector.SaveCoverImageToCache(mcId);
context.SaveChanges();
}
catch (DbUpdateException e)
{
Log.Error(e);
}
return [];
}
}

View File

@ -1,149 +0,0 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using API.Schema.Contexts;
using log4net;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Newtonsoft.Json;
namespace API.Schema.Jobs;
[PrimaryKey("Key")]
public abstract class Job : Identifiable, IComparable<Job>
{
[StringLength(64)] public string? ParentJobId { get; private set; }
[JsonIgnore] public Job? ParentJob { get; internal set; }
private ICollection<Job>? _dependsOnJobs;
[JsonIgnore] public ICollection<Job> DependsOnJobs
{
get => LazyLoader.Load(this, ref _dependsOnJobs) ?? throw new InvalidOperationException();
init => _dependsOnJobs = value;
}
[Required] public JobType JobType { get; init; }
[Required] public ulong RecurrenceMs { get; set; }
[Required] public DateTime LastExecution { get; internal set; } = DateTime.UnixEpoch;
[NotMapped] [Required] public DateTime NextExecution => LastExecution.AddMilliseconds(RecurrenceMs);
[Required] public JobState state { get; internal set; } = JobState.FirstExecution;
[Required] public bool Enabled { get; internal set; } = true;
[JsonIgnore] [NotMapped] internal bool IsCompleted => state is >= (JobState)128 and < (JobState)192;
[NotMapped] [JsonIgnore] protected ILog Log { get; init; }
[NotMapped] [JsonIgnore] protected ILazyLoader LazyLoader { get; init; } = null!;
protected Job(string key, JobType jobType, ulong recurrenceMs, Job? parentJob = null, ICollection<Job>? dependsOnJobs = null)
: base(key)
{
this.JobType = jobType;
this.RecurrenceMs = recurrenceMs;
this.ParentJobId = parentJob?.Key;
this.ParentJob = parentJob;
this.DependsOnJobs = dependsOnJobs ?? [];
this.Log = LogManager.GetLogger(this.GetType());
}
/// <summary>
/// EF ONLY!!!
/// </summary>
protected internal Job(ILazyLoader lazyLoader, string key, JobType jobType, ulong recurrenceMs, string? parentJobId)
: base(key)
{
this.LazyLoader = lazyLoader;
this.JobType = jobType;
this.RecurrenceMs = recurrenceMs;
this.ParentJobId = parentJobId;
this.DependsOnJobs = [];
this.Log = LogManager.GetLogger(this.GetType());
}
public IEnumerable<Job> Run(PgsqlContext context, ref bool running)
{
Log.Info($"Running job {this}");
DateTime jobStart = DateTime.UtcNow;
Job[]? ret = null;
try
{
this.state = JobState.Running;
context.SaveChanges();
running = true;
ret = RunInternal(context).ToArray();
Log.Info($"Job {this} completed. Generated {ret.Length} new jobs.");
this.state = this.RecurrenceMs > 0 ? JobState.CompletedWaiting : JobState.Completed;
this.LastExecution = DateTime.UtcNow;
context.SaveChanges();
}
catch (Exception e)
{
if (e is not DbUpdateException)
{
Log.Error($"Failed to run job {this}", e);
this.state = JobState.Failed;
this.Enabled = false;
this.LastExecution = DateTime.UtcNow;
context.SaveChanges();
}
else
{
Log.Error($"Failed to update Database {this}", e);
}
}
try
{
if (ret != null)
{
context.Jobs.AddRange(ret);
context.SaveChanges();
}
}
catch (DbUpdateException e)
{
Log.Error($"Failed to update Database {this}", e);
}
Log.Info($"Finished Job {this}! (took {DateTime.UtcNow.Subtract(jobStart).TotalMilliseconds}ms)");
return ret ?? [];
}
protected abstract IEnumerable<Job> RunInternal(PgsqlContext context);
public List<Job> GetDependenciesAndSelf()
{
List<Job> ret = GetDependencies();
ret.Add(this);
return ret;
}
public List<Job> GetDependencies()
{
List<Job> ret = new ();
foreach (Job job in DependsOnJobs)
{
ret.AddRange(job.GetDependenciesAndSelf());
}
return ret;
}
public int CompareTo(Job? other)
{
if (other is null)
return -1;
// Sort by missing dependencies
if (this.GetDependencies().Count(job => !job.IsCompleted) <
other.GetDependencies().Count(job => !job.IsCompleted))
return -1;
// Sort by NextExecution-time
if (this.NextExecution < other.NextExecution)
return -1;
return 1;
}
public override string ToString() => base.ToString();
}

View File

@ -1,14 +0,0 @@
namespace API.Schema.Jobs;
public enum JobState : byte
{
//Values 0-63 Preparation Stages
FirstExecution = 0,
//64-127 Running Stages
Running = 64,
//128-191 Completion Stages
Completed = 128,
CompletedWaiting = 159,
//192-255 Error stages
Failed = 192
}

View File

@ -1,14 +0,0 @@
namespace API.Schema.Jobs;
public enum JobType : byte
{
DownloadSingleChapterJob = 0,
DownloadAvailableChaptersJob = 1,
MoveFileOrFolderJob = 3,
DownloadMangaCoverJob = 4,
RetrieveChaptersJob = 5,
UpdateChaptersDownloadedJob = 6,
MoveMangaLibraryJob = 7,
UpdateCoverJob = 9,
}

View File

@ -1,18 +0,0 @@
using Microsoft.EntityFrameworkCore.Infrastructure;
namespace API.Schema.Jobs;
public abstract class JobWithDownloading : Job
{
public JobWithDownloading(string key, JobType jobType, ulong recurrenceMs, Job? parentJob = null, ICollection<Job>? dependsOnJobs = null)
: base(key, jobType, recurrenceMs, parentJob, dependsOnJobs)
{
}
public JobWithDownloading(ILazyLoader lazyLoader, string key, JobType jobType, ulong recurrenceMs, string? parentJobId)
: base(lazyLoader, key, jobType, recurrenceMs, parentJobId)
{
}
}

View File

@ -1,71 +0,0 @@
using System.ComponentModel.DataAnnotations;
using API.Schema.Contexts;
using Microsoft.EntityFrameworkCore.Infrastructure;
namespace API.Schema.Jobs;
public class MoveFileOrFolderJob : Job
{
[StringLength(256)]
[Required]
public string FromLocation { get; init; }
[StringLength(256)]
[Required]
public string ToLocation { get; init; }
public MoveFileOrFolderJob(string fromLocation, string toLocation, Job? parentJob = null, ICollection<Job>? dependsOnJobs = null)
: base(TokenGen.CreateToken(typeof(MoveFileOrFolderJob)), JobType.MoveFileOrFolderJob, 0, parentJob, dependsOnJobs)
{
this.FromLocation = fromLocation;
this.ToLocation = toLocation;
}
/// <summary>
/// EF ONLY!!!
/// </summary>
internal MoveFileOrFolderJob(ILazyLoader lazyLoader, string key, ulong recurrenceMs, string fromLocation, string toLocation, string? parentJobId)
: base(lazyLoader, key, JobType.MoveFileOrFolderJob, recurrenceMs, parentJobId)
{
this.FromLocation = fromLocation;
this.ToLocation = toLocation;
}
protected override IEnumerable<Job> RunInternal(PgsqlContext context)
{
try
{
FileInfo fi = new (FromLocation);
if (!fi.Exists)
{
Log.Error($"File does not exist at {FromLocation}");
return [];
}
if (File.Exists(ToLocation))//Do not override existing
{
Log.Error($"File already exists at {ToLocation}");
return [];
}
if(fi.Attributes.HasFlag(FileAttributes.Directory))
MoveDirectory(fi, ToLocation);
else
MoveFile(fi, ToLocation);
}
catch (Exception e)
{
Log.Error(e);
}
return [];
}
private void MoveDirectory(FileInfo from, string toLocation)
{
Directory.Move(from.FullName, toLocation);
}
private void MoveFile(FileInfo from, string toLocation)
{
File.Move(from.FullName, toLocation);
}
}

View File

@ -1,72 +0,0 @@
using System.ComponentModel.DataAnnotations;
using API.Schema.Contexts;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Newtonsoft.Json;
namespace API.Schema.Jobs;
public class MoveMangaLibraryJob : Job
{
[StringLength(64)] [Required] public string MangaId { get; init; } = null!;
private Manga? _manga;
[JsonIgnore]
public Manga Manga
{
get => LazyLoader.Load(this, ref _manga) ?? throw new InvalidOperationException();
init
{
MangaId = value.Key;
_manga = value;
}
}
[StringLength(64)] [Required] public string ToLibraryId { get; private set; } = null!;
private FileLibrary? _toFileLibrary;
[JsonIgnore]
public FileLibrary ToFileLibrary
{
get => LazyLoader.Load(this, ref _toFileLibrary) ?? throw new InvalidOperationException();
init
{
ToLibraryId = value.Key;
_toFileLibrary = value;
}
}
public MoveMangaLibraryJob(Manga manga, FileLibrary toFileLibrary, Job? parentJob = null, ICollection<Job>? dependsOnJobs = null)
: base(TokenGen.CreateToken(typeof(MoveMangaLibraryJob)), JobType.MoveMangaLibraryJob, 0, parentJob, dependsOnJobs)
{
this.Manga = manga;
this.ToFileLibrary = toFileLibrary;
}
/// <summary>
/// EF ONLY!!!
/// </summary>
internal MoveMangaLibraryJob(ILazyLoader lazyLoader, string key, ulong recurrenceMs, string mangaId, string toLibraryId, string? parentJobId)
: base(lazyLoader, key, JobType.MoveMangaLibraryJob, recurrenceMs, parentJobId)
{
this.MangaId = mangaId;
this.ToLibraryId = toLibraryId;
}
protected override IEnumerable<Job> RunInternal(PgsqlContext context)
{
context.Entry(Manga).Reference<FileLibrary>(m => m.Library).Load();
Dictionary<Chapter, string> oldPath = Manga.Chapters.ToDictionary(c => c, c => c.FullArchiveFilePath);
Manga.Library = ToFileLibrary;
try
{
context.SaveChanges();
}
catch (DbUpdateException e)
{
Log.Error(e);
return [];
}
return Manga.Chapters.Select(c => new MoveFileOrFolderJob(oldPath[c], c.FullArchiveFilePath));
}
}

View File

@ -1,70 +0,0 @@
using System.ComponentModel.DataAnnotations;
using API.Schema.Contexts;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Newtonsoft.Json;
namespace API.Schema.Jobs;
public class RetrieveChaptersJob : JobWithDownloading
{
[StringLength(64)] [Required] public string MangaId { get; init; } = null!;
private Manga? _manga;
[JsonIgnore]
public Manga Manga
{
get => LazyLoader.Load(this, ref _manga) ?? throw new InvalidOperationException();
init
{
MangaId = value.Key;
_manga = value;
}
}
[StringLength(8)] [Required] public string Language { get; private set; }
public RetrieveChaptersJob(Manga manga, string language, ulong recurrenceMs, Job? parentJob = null, ICollection<Job>? dependsOnJobs = null)
: base(TokenGen.CreateToken(typeof(RetrieveChaptersJob)), JobType.RetrieveChaptersJob, recurrenceMs, parentJob, dependsOnJobs)
{
this.Manga = manga;
this.Language = language;
}
/// <summary>
/// EF ONLY!!!
/// </summary>
internal RetrieveChaptersJob(ILazyLoader lazyLoader, string key, string mangaId, ulong recurrenceMs, string language, string? parentJobId)
: base(lazyLoader, key, JobType.RetrieveChaptersJob, recurrenceMs, parentJobId)
{
this.MangaId = mangaId;
this.Language = language;
}
protected override IEnumerable<Job> RunInternal(PgsqlContext context)
{
//TODO MangaConnector Selection
MangaConnectorId<Manga> mcId = Manga.MangaConnectorIds.First();
// This gets all chapters that are not downloaded
(Chapter, MangaConnectorId<Chapter>)[] allChapters = mcId.MangaConnector.GetChapters(mcId, 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
{
foreach ((Chapter chapter, MangaConnectorId<Chapter> mcId) newChapter in newChapters)
{
Manga.Chapters.Add(newChapter.chapter);
context.MangaConnectorToChapter.Add(newChapter.mcId);
}
context.SaveChanges();
}
catch (DbUpdateException e)
{
Log.Error(e);
}
return [];
}
}

View File

@ -1,58 +0,0 @@
using System.ComponentModel.DataAnnotations;
using API.Schema.Contexts;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Newtonsoft.Json;
namespace API.Schema.Jobs;
public class UpdateChaptersDownloadedJob : Job
{
[StringLength(64)] [Required] public string MangaId { get; init; } = null!;
private Manga? _manga;
[JsonIgnore]
public Manga Manga
{
get => LazyLoader.Load(this, ref _manga) ?? throw new InvalidOperationException();
init
{
MangaId = value.Key;
_manga = value;
}
}
public UpdateChaptersDownloadedJob(Manga manga, ulong recurrenceMs, Job? parentJob = null, ICollection<Job>? dependsOnJobs = null)
: base(TokenGen.CreateToken(typeof(UpdateChaptersDownloadedJob)), JobType.UpdateChaptersDownloadedJob, recurrenceMs, parentJob, dependsOnJobs)
{
this.Manga = manga;
}
/// <summary>
/// EF ONLY!!!
/// </summary>
internal UpdateChaptersDownloadedJob(ILazyLoader lazyLoader, string key, ulong recurrenceMs, string mangaId, string? parentJobId)
: base(lazyLoader, key, JobType.UpdateChaptersDownloadedJob, recurrenceMs, parentJobId)
{
this.MangaId = mangaId;
}
protected override IEnumerable<Job> RunInternal(PgsqlContext context)
{
context.Entry(Manga).Reference<FileLibrary>(m => m.Library).Load();
foreach (Chapter mangaChapter in Manga.Chapters)
{
mangaChapter.Downloaded = mangaChapter.CheckDownloaded();
}
try
{
context.SaveChanges();
}
catch (DbUpdateException e)
{
Log.Error(e);
}
return [];
}
}

View File

@ -1,67 +0,0 @@
using System.ComponentModel.DataAnnotations;
using API.Schema.Contexts;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Newtonsoft.Json;
namespace API.Schema.Jobs;
public class UpdateCoverJob : Job
{
[StringLength(64)] [Required] public string MangaId { get; init; } = null!;
private Manga? _manga;
[JsonIgnore]
public Manga Manga
{
get => LazyLoader.Load(this, ref _manga) ?? throw new InvalidOperationException();
init
{
MangaId = value.Key;
_manga = value;
}
}
public UpdateCoverJob(Manga manga, ulong recurrenceMs, Job? parentJob = null, ICollection<Job>? dependsOnJobs = null)
: base(TokenGen.CreateToken(typeof(UpdateCoverJob)), JobType.UpdateCoverJob, recurrenceMs, parentJob, dependsOnJobs)
{
this.Manga = manga;
}
/// <summary>
/// EF ONLY!!!
/// </summary>
internal UpdateCoverJob(ILazyLoader lazyLoader, string key, string mangaId, ulong recurrenceMs, string? parentJobId)
: base(lazyLoader, key, JobType.UpdateCoverJob, recurrenceMs, parentJobId)
{
this.MangaId = mangaId;
}
protected override IEnumerable<Job> RunInternal(PgsqlContext context)
{
bool keepCover = context.Jobs
.Any(job => job.JobType == JobType.DownloadAvailableChaptersJob
&& ((DownloadAvailableChaptersJob)job).MangaId == MangaId);
if (!keepCover)
{
if(File.Exists(Manga.CoverFileNameInCache))
File.Delete(Manga.CoverFileNameInCache);
try
{
Manga.CoverFileNameInCache = null;
context.Jobs.Remove(this);
context.SaveChanges();
}
catch (DbUpdateException e)
{
Log.Error(e);
}
}
else
{
return [new DownloadMangaCoverJob(Manga, this)];
}
return [];
}
}

View File

@ -1,32 +0,0 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using log4net;
using Microsoft.EntityFrameworkCore;
using Newtonsoft.Json;
namespace API.Schema.LibraryConnectors;
[PrimaryKey("LibraryConnectorId")]
public abstract class LibraryConnector(string libraryConnectorId, LibraryType libraryType, string baseUrl, string auth)
{
[StringLength(64)]
[Required]
public string LibraryConnectorId { get; } = libraryConnectorId;
[Required]
public LibraryType LibraryType { get; init; } = libraryType;
[StringLength(256)]
[Required]
[Url]
public string BaseUrl { get; init; } = baseUrl;
[StringLength(256)]
[Required]
public string Auth { get; init; } = auth;
[JsonIgnore]
[NotMapped]
protected ILog Log { get; init; } = LogManager.GetLogger($"{libraryType.ToString()} {baseUrl}");
protected abstract void UpdateLibraryInternal();
internal abstract bool Test();
}

View File

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

View File

@ -1,12 +1,12 @@
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Nodes; using System.Text.Json.Nodes;
namespace API.Schema.LibraryConnectors; 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

@ -1,12 +1,11 @@
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Nodes; using System.Text.Json.Nodes;
namespace API.Schema.LibraryConnectors; 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

@ -0,0 +1,57 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using log4net;
using Microsoft.EntityFrameworkCore;
using Newtonsoft.Json;
namespace API.Schema.LibraryContext.LibraryConnectors;
[PrimaryKey("Key")]
public abstract class LibraryConnector : Identifiable
{
[Required]
public LibraryType LibraryType { get; init; }
[StringLength(256)]
[Required]
[Url]
public string BaseUrl { get; init; }
[StringLength(256)]
[Required]
public string Auth { get; init; }
[JsonIgnore]
[NotMapped]
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();
internal abstract bool Test();
}
public enum LibraryType : byte
{
Komga = 0,
Kavita = 1
}

View File

@ -2,7 +2,7 @@
using System.Net.Http.Headers; using System.Net.Http.Headers;
using log4net; using log4net;
namespace API.Schema.LibraryConnectors; namespace API.Schema.LibraryContext.LibraryConnectors;
public class NetClient public class NetClient
{ {

View File

@ -0,0 +1,18 @@
using API.Schema.LibraryContext.LibraryConnectors;
using Microsoft.EntityFrameworkCore;
namespace API.Schema.LibraryContext;
public class LibraryContext(DbContextOptions<LibraryContext> options) : TrangaBaseContext<LibraryContext>(options)
{
public DbSet<LibraryConnector> LibraryConnectors { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
//LibraryConnector Types
modelBuilder.Entity<LibraryConnector>()
.HasDiscriminator(l => l.LibraryType)
.HasValue<Komga>(LibraryType.Komga)
.HasValue<Kavita>(LibraryType.Kavita);
}
}

View File

@ -1,7 +1,7 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
namespace API.Schema; namespace API.Schema.MangaContext;
[PrimaryKey("Key")] [PrimaryKey("Key")]
public class AltTitle(string language, string title) : Identifiable(TokenGen.CreateToken("AltTitle")) public class AltTitle(string language, string title) : Identifiable(TokenGen.CreateToken("AltTitle"))

View File

@ -1,7 +1,7 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
namespace API.Schema; namespace API.Schema.MangaContext;
[PrimaryKey("Key")] [PrimaryKey("Key")]
public class Author(string authorName) : Identifiable(TokenGen.CreateToken(typeof(Author), authorName)) public class Author(string authorName) : Identifiable(TokenGen.CreateToken(typeof(Author), authorName))

View File

@ -7,7 +7,7 @@ using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Infrastructure;
using Newtonsoft.Json; using Newtonsoft.Json;
namespace API.Schema; namespace API.Schema.MangaContext;
[PrimaryKey("Key")] [PrimaryKey("Key")]
public class Chapter : Identifiable, IComparable<Chapter> public class Chapter : Identifiable, IComparable<Chapter>
@ -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

@ -1,7 +1,7 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
namespace API.Schema; namespace API.Schema.MangaContext;
[PrimaryKey("Key")] [PrimaryKey("Key")]
public class FileLibrary(string basePath, string libraryName) public class FileLibrary(string basePath, string libraryName)

View File

@ -1,7 +1,7 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
namespace API.Schema; namespace API.Schema.MangaContext;
[PrimaryKey("Key")] [PrimaryKey("Key")]
public class Link(string linkProvider, string linkUrl) : Identifiable(TokenGen.CreateToken(typeof(Link), linkProvider, linkUrl)) public class Link(string linkProvider, string linkUrl) : Identifiable(TokenGen.CreateToken(typeof(Link), linkProvider, linkUrl))

View File

@ -2,14 +2,13 @@ using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema; using System.ComponentModel.DataAnnotations.Schema;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Text; using System.Text;
using API.Schema.Contexts; using API.Workers;
using API.Schema.Jobs;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Infrastructure;
using Newtonsoft.Json; using Newtonsoft.Json;
using static System.IO.UnixFileMode; using static System.IO.UnixFileMode;
namespace API.Schema; namespace API.Schema.MangaContext;
[PrimaryKey("Key")] [PrimaryKey("Key")]
public class Manga : Identifiable public class Manga : Identifiable
@ -157,42 +156,31 @@ public class Manga : Identifiable
} }
/// <summary> /// <summary>
/// /// Merges another Manga (MangaConnectorIds and Chapters)
/// </summary> /// </summary>
/// <param name="other"></param> /// <param name="other">The other <see cref="Manga" /> to merge</param>
/// <param name="context"></param> /// <param name="context"><see cref="MangaContext"/> to use for Database operations</param>
/// <exception cref="DbUpdateException"></exception> /// <returns>An array of <see cref="MoveFileOrFolderWorker"/> for moving <see cref="Chapter"/> to new Directory</returns>
public void MergeFrom(Manga other, PgsqlContext context) public BaseWorker[] MergeFrom(Manga other, MangaContext context)
{ {
try context.Mangas.Remove(other);
List<BaseWorker> newJobs = new();
this.MangaConnectorIds = this.MangaConnectorIds
.UnionBy(other.MangaConnectorIds, id => id.MangaConnectorName)
.ToList();
foreach (Chapter otherChapter in other.Chapters)
{ {
context.Mangas.Remove(other); string oldPath = otherChapter.FullArchiveFilePath;
List<Job> newJobs = new(); Chapter newChapter = new(this, otherChapter.ChapterNumber, otherChapter.VolumeNumber,
otherChapter.Title);
this.MangaConnectorIds = this.MangaConnectorIds this.Chapters.Add(newChapter);
.UnionBy(other.MangaConnectorIds, id => id.MangaConnectorName) string newPath = newChapter.FullArchiveFilePath;
.ToList(); newJobs.Add(new MoveFileOrFolderWorker(newPath, oldPath));
foreach (Chapter otherChapter in other.Chapters)
{
string oldPath = otherChapter.FullArchiveFilePath;
Chapter newChapter = new(this, otherChapter.ChapterNumber, otherChapter.VolumeNumber,
otherChapter.Title);
this.Chapters.Add(newChapter);
string newPath = newChapter.FullArchiveFilePath;
newJobs.Add(new MoveFileOrFolderJob(oldPath, newPath));
}
if (other.Chapters.Count > 0)
newJobs.Add(new UpdateChaptersDownloadedJob(this, 0, null, newJobs));
context.Jobs.AddRange(newJobs);
context.SaveChanges();
}
catch (DbUpdateException e)
{
throw new DbUpdateException(e.Message, e.InnerException, e.Entries);
} }
return newJobs.ToArray();
} }
public override string ToString() => $"{base.ToString()} {Name}"; public override string ToString() => $"{base.ToString()} {Name}";

View File

@ -1,10 +1,10 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using API.Schema.MangaConnectors; using API.Schema.MangaContext.MangaConnectors;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Infrastructure;
using Newtonsoft.Json; using Newtonsoft.Json;
namespace API.Schema; namespace API.Schema.MangaContext;
[PrimaryKey("Key")] [PrimaryKey("Key")]
public class MangaConnectorId<T> : Identifiable where T : Identifiable public class MangaConnectorId<T> : Identifiable where T : Identifiable
@ -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

@ -2,7 +2,7 @@
using API.MangaDownloadClients; using API.MangaDownloadClients;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
namespace API.Schema.MangaConnectors; namespace API.Schema.MangaContext.MangaConnectors;
public class ComickIo : MangaConnector public class ComickIo : MangaConnector
{ {

View File

@ -1,11 +1,9 @@
using API.Schema.Contexts; namespace API.Schema.MangaContext.MangaConnectors;
namespace API.Schema.MangaConnectors;
public class Global : MangaConnector public class Global : MangaConnector
{ {
private PgsqlContext context { get; init; } private MangaContext context { get; init; }
public Global(PgsqlContext context) : base("Global", ["all"], [""], "") public Global(MangaContext context) : base("Global", ["all"], [""], "")
{ {
this.context = context; this.context = context;
} }

View File

@ -6,7 +6,7 @@ using log4net;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Newtonsoft.Json; using Newtonsoft.Json;
namespace API.Schema.MangaConnectors; namespace API.Schema.MangaContext.MangaConnectors;
[PrimaryKey("Name")] [PrimaryKey("Name")]
public abstract class MangaConnector(string name, string[] supportedLanguages, string[] baseUris, string iconUrl) public abstract class MangaConnector(string name, string[] supportedLanguages, string[] baseUris, string iconUrl)

View File

@ -2,7 +2,7 @@
using API.MangaDownloadClients; using API.MangaDownloadClients;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
namespace API.Schema.MangaConnectors; namespace API.Schema.MangaContext.MangaConnectors;
public class MangaDex : MangaConnector public class MangaDex : MangaConnector
{ {

View File

@ -1,124 +1,23 @@
using API.Schema.Jobs; using API.Schema.MangaContext.MangaConnectors;
using API.Schema.MangaConnectors; using API.Schema.MangaContext.MetadataFetchers;
using API.Schema.MetadataFetchers;
using log4net;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Diagnostics;
namespace API.Schema.Contexts; namespace API.Schema.MangaContext;
public class PgsqlContext(DbContextOptions<PgsqlContext> options) : DbContext(options) public class MangaContext(DbContextOptions<MangaContext> options) : TrangaBaseContext<MangaContext>(options)
{ {
public DbSet<Job> Jobs { get; set; }
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; }
public DbSet<MangaConnectorId<Manga>> MangaConnectorToManga { get; set; } public DbSet<MangaConnectorId<Manga>> MangaConnectorToManga { get; set; }
public DbSet<MangaConnectorId<Chapter>> MangaConnectorToChapter { get; set; } public DbSet<MangaConnectorId<Chapter>> MangaConnectorToChapter { get; set; }
public DbSet<MetadataEntry> MetadataEntries { get; set; } public DbSet<MetadataEntry> MetadataEntries { get; set; }
private ILog Log => LogManager.GetLogger(GetType());
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
base.OnConfiguring(optionsBuilder);
optionsBuilder.EnableSensitiveDataLogging();
optionsBuilder.LogTo(s =>
{
Log.Debug(s);
}, [DbLoggerCategory.Query.Name], LogLevel.Trace, DbContextLoggerOptions.Level | DbContextLoggerOptions.Category);
}
protected override void OnModelCreating(ModelBuilder modelBuilder) protected override void OnModelCreating(ModelBuilder modelBuilder)
{ {
//Job Types
modelBuilder.Entity<Job>()
.HasDiscriminator(j => j.JobType)
.HasValue<MoveFileOrFolderJob>(JobType.MoveFileOrFolderJob)
.HasValue<MoveMangaLibraryJob>(JobType.MoveMangaLibraryJob)
.HasValue<DownloadAvailableChaptersJob>(JobType.DownloadAvailableChaptersJob)
.HasValue<DownloadSingleChapterJob>(JobType.DownloadSingleChapterJob)
.HasValue<DownloadMangaCoverJob>(JobType.DownloadMangaCoverJob)
.HasValue<RetrieveChaptersJob>(JobType.RetrieveChaptersJob)
.HasValue<UpdateCoverJob>(JobType.UpdateCoverJob)
.HasValue<UpdateChaptersDownloadedJob>(JobType.UpdateChaptersDownloadedJob);
modelBuilder.Entity<DownloadAvailableChaptersJob>()
.HasOne<Manga>(j => j.Manga)
.WithMany()
.HasForeignKey(j => j.MangaId)
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<DownloadAvailableChaptersJob>()
.Navigation(j => j.Manga)
.EnableLazyLoading();
modelBuilder.Entity<DownloadMangaCoverJob>()
.HasOne<Manga>(j => j.Manga)
.WithMany()
.HasForeignKey(j => j.MangaId)
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<DownloadMangaCoverJob>()
.Navigation(j => j.Manga)
.EnableLazyLoading();
modelBuilder.Entity<DownloadSingleChapterJob>()
.HasOne<Chapter>(j => j.Chapter)
.WithMany()
.HasForeignKey(j => j.ChapterId)
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<DownloadSingleChapterJob>()
.Navigation(j => j.Chapter)
.EnableLazyLoading();
modelBuilder.Entity<MoveMangaLibraryJob>()
.HasOne<Manga>(j => j.Manga)
.WithMany()
.HasForeignKey(j => j.MangaId)
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<MoveMangaLibraryJob>()
.Navigation(j => j.Manga)
.EnableLazyLoading();
modelBuilder.Entity<MoveMangaLibraryJob>()
.HasOne<FileLibrary>(j => j.ToFileLibrary)
.WithMany()
.HasForeignKey(j => j.ToLibraryId)
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<MoveMangaLibraryJob>()
.Navigation(j => j.ToFileLibrary)
.EnableLazyLoading();
modelBuilder.Entity<RetrieveChaptersJob>()
.HasOne<Manga>(j => j.Manga)
.WithMany()
.HasForeignKey(j => j.MangaId)
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<RetrieveChaptersJob>()
.Navigation(j => j.Manga)
.EnableLazyLoading();
modelBuilder.Entity<UpdateChaptersDownloadedJob>()
.HasOne<Manga>(j => j.Manga)
.WithMany()
.HasForeignKey(j => j.MangaId)
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<UpdateChaptersDownloadedJob>()
.Navigation(j => j.Manga)
.EnableLazyLoading();
//Job has possible ParentJob
modelBuilder.Entity<Job>()
.HasOne<Job>(childJob => childJob.ParentJob)
.WithMany()
.HasForeignKey(childJob => childJob.ParentJobId)
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<Job>()
.Navigation(childJob => childJob.ParentJob)
.EnableLazyLoading();
//Job might be dependent on other Jobs
modelBuilder.Entity<Job>()
.HasMany<Job>(root => root.DependsOnJobs)
.WithMany();
modelBuilder.Entity<Job>()
.Navigation(j => j.DependsOnJobs)
.EnableLazyLoading();
//MangaConnector Types //MangaConnector Types
modelBuilder.Entity<MangaConnector>() modelBuilder.Entity<MangaConnector>()
.HasDiscriminator(c => c.Name) .HasDiscriminator(c => c.Name)

View File

@ -1,7 +1,7 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
namespace API.Schema; namespace API.Schema.MangaContext;
[PrimaryKey("Tag")] [PrimaryKey("Tag")]
public class MangaTag(string tag) public class MangaTag(string tag)
@ -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

@ -1,7 +1,7 @@
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Newtonsoft.Json; using Newtonsoft.Json;
namespace API.Schema.MetadataFetchers; namespace API.Schema.MangaContext.MetadataFetchers;
[PrimaryKey("MetadataFetcherName", "Identifier")] [PrimaryKey("MetadataFetcherName", "Identifier")]
public class MetadataEntry public class MetadataEntry
@ -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

@ -1,26 +1,24 @@
using System.Diagnostics.CodeAnalysis;
using API.Schema.Contexts;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
namespace API.Schema.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) =>
@ -33,5 +31,5 @@ public abstract class MetadataFetcher
/// <summary> /// <summary>
/// Updates the Manga linked in the MetadataEntry /// Updates the Manga linked in the MetadataEntry
/// </summary> /// </summary>
public abstract void UpdateMetadata(MetadataEntry metadataEntry, PgsqlContext dbContext); public abstract void UpdateMetadata(MetadataEntry metadataEntry, MangaContext dbContext);
} }

View File

@ -1,3 +1,3 @@
namespace API.Schema.MetadataFetchers; namespace API.Schema.MangaContext.MetadataFetchers;
public record MetadataSearchResult(string Identifier, string Name, string Url, string? Description = null, string? CoverUrl = null); public record MetadataSearchResult(string Identifier, string Name, string Url, string? Description = null, string? CoverUrl = null);

View File

@ -1,9 +1,8 @@
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using API.Schema.Contexts;
using JikanDotNet; using JikanDotNet;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
namespace API.Schema.MetadataFetchers; namespace API.Schema.MangaContext.MetadataFetchers;
public class MyAnimeList : MetadataFetcher public class MyAnimeList : MetadataFetcher
{ {
@ -45,7 +44,7 @@ public class MyAnimeList : MetadataFetcher
/// <param name="dbContext"></param> /// <param name="dbContext"></param>
/// <exception cref="FormatException"></exception> /// <exception cref="FormatException"></exception>
/// <exception cref="DbUpdateException"></exception> /// <exception cref="DbUpdateException"></exception>
public override void UpdateMetadata(MetadataEntry metadataEntry, PgsqlContext dbContext) public override void UpdateMetadata(MetadataEntry metadataEntry, MangaContext dbContext)
{ {
Manga dbManga = dbContext.Mangas.Find(metadataEntry.MangaId)!; Manga dbManga = dbContext.Mangas.Find(metadataEntry.MangaId)!;
MangaFull resultData; MangaFull resultData;
@ -68,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

@ -1,8 +0,0 @@
namespace API.Schema;
public enum NotificationUrgency : byte
{
Low = 1,
Normal = 3,
High = 5
}

View File

@ -1,15 +1,11 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
namespace API.Schema; namespace API.Schema.NotificationsContext;
[PrimaryKey("NotificationId")] [PrimaryKey(nameof(Key))]
public class Notification public class Notification : Identifiable
{ {
[StringLength(64)]
[Required]
public string NotificationId { get; init; }
[Required] [Required]
public NotificationUrgency Urgency { get; init; } public NotificationUrgency Urgency { get; init; }
@ -23,30 +19,38 @@ public class Notification
[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"))
{ {
this.NotificationId = TokenGen.CreateToken("Notification");
this.Title = title; this.Title = title;
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 notificationId, string title, string message, NotificationUrgency urgency, DateTime date) public Notification(string key, string title, string message, NotificationUrgency urgency, DateTime date, bool isSent)
: base(key)
{ {
this.NotificationId = notificationId;
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() public override string ToString() => $"{base.ToString()} {Urgency} {Title} {Message}";
{ }
return $"{NotificationId} {Urgency} {Title}";
} public enum NotificationUrgency : byte
{
Low = 1,
Normal = 3,
High = 5
} }

View File

@ -5,7 +5,7 @@ using log4net;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Newtonsoft.Json; using Newtonsoft.Json;
namespace API.Schema.NotificationConnectors; namespace API.Schema.NotificationsContext.NotificationConnectors;
[PrimaryKey("Name")] [PrimaryKey("Name")]
public class NotificationConnector(string name, string url, Dictionary<string, string> headers, string httpMethod, string body) public class NotificationConnector(string name, string url, Dictionary<string, string> headers, string httpMethod, string body)
@ -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

@ -0,0 +1,10 @@
using API.Schema.NotificationsContext.NotificationConnectors;
using Microsoft.EntityFrameworkCore;
namespace API.Schema.NotificationsContext;
public class NotificationsContext(DbContextOptions<NotificationsContext> options) : TrangaBaseContext<NotificationsContext>(options)
{
public DbSet<NotificationConnector> NotificationConnectors { get; set; }
public DbSet<Notification> Notifications { get; set; }
}

View File

@ -0,0 +1,40 @@
using log4net;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Diagnostics;
namespace API.Schema;
public abstract class TrangaBaseContext<T> : DbContext where T : DbContext
{
private ILog Log { get; init; }
protected TrangaBaseContext(DbContextOptions<T> options) : base(options)
{
this.Log = LogManager.GetLogger(GetType());
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
base.OnConfiguring(optionsBuilder);
optionsBuilder.LogTo(s =>
{
Log.Debug(s);
}, Array.Empty<string>(), LogLevel.Warning, DbContextLoggerOptions.Level | DbContextLoggerOptions.Category | DbContextLoggerOptions.UtcTime);
}
internal (bool success, string? exceptionMessage) Sync()
{
try
{
this.SaveChanges();
return (true, null);
}
catch (Exception e)
{
Log.Error(null, e);
return (false, e.Message);
}
}
public override string ToString() => $"{GetType().Name} {typeof(T).Name}";
}

View File

@ -1,12 +1,12 @@
using API.Schema; using System.Diagnostics.CodeAnalysis;
using API.Schema.Contexts; using API.Schema.LibraryContext;
using API.Schema.Jobs; using API.Schema.MangaContext;
using API.Schema.MangaConnectors; using API.Schema.MangaContext.MetadataFetchers;
using API.Schema.MetadataFetchers; using API.Schema.NotificationsContext;
using API.Schema.NotificationConnectors; using API.Workers;
using API.Workers.MaintenanceWorkers;
using log4net; using log4net;
using log4net.Config; using log4net.Config;
using Microsoft.EntityFrameworkCore;
namespace API; namespace API;
@ -22,99 +22,61 @@ public static class Tranga
" |___| |__| |___._||__|__||___ ||___._|\n" + " |___| |__| |___._||__|__||___ ||___._|\n" +
" |_____| \n\n"; " |_____| \n\n";
public static Thread NotificationSenderThread { get; } = new (NotificationSender); public static Thread PeriodicWorkerStarterThread { get; } = new (WorkerStarter);
public static Thread JobStarterThread { get; } = new (JobStarter);
private static readonly ILog Log = LogManager.GetLogger(typeof(Tranga)); private static readonly ILog Log = LogManager.GetLogger(typeof(Tranga));
internal static 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 void RemoveStaleFiles(PgsqlContext context) internal static HashSet<BaseWorker> AllWorkers { get; private set; } = new ();
public static void AddWorker(BaseWorker worker) => AllWorkers.Add(worker);
public static void AddWorkers(IEnumerable<BaseWorker> workers)
{ {
Log.Info("Removing stale files..."); foreach (BaseWorker baseWorker in workers)
if (!Directory.Exists(TrangaSettings.coverImageCache))
return;
string[] usedFiles = context.Mangas.Select(m => m.CoverFileNameInCache).Where(s => s != null).ToArray()!;
string[] extraneousFiles = new DirectoryInfo(TrangaSettings.coverImageCache).GetFiles()
.Where(f => usedFiles.Contains(f.FullName) == false)
.Select(f => f.FullName)
.ToArray();
foreach (string path in extraneousFiles)
{ {
Log.Info($"Deleting {path}"); AddWorker(baseWorker);
File.Delete(path);
} }
} }
private static void NotificationSender(object? serviceProviderObj) public static void RemoveWorker(BaseWorker removeWorker)
{ {
if (serviceProviderObj is null) IEnumerable<BaseWorker> baseWorkers = AllWorkers.Where(w => w.DependenciesAndSelf.Any(worker => worker == removeWorker));
{
Log.Error("serviceProviderObj is null");
return;
}
IServiceProvider serviceProvider = (IServiceProvider)serviceProviderObj!;
using IServiceScope scope = serviceProvider.CreateScope();
NotificationsContext context = scope.ServiceProvider.GetRequiredService<NotificationsContext>();
try
{
//Removing Notifications from previous runs
IQueryable<Notification> staleNotifications =
context.Notifications.Where(n => n.Urgency < NotificationUrgency.Normal);
context.Notifications.RemoveRange(staleNotifications);
context.SaveChanges();
}
catch (DbUpdateException e)
{
Log.Error("Error removing stale notifications.", e);
}
while (true) foreach (BaseWorker worker in baseWorkers)
{ {
SendNotifications(serviceProvider, NotificationUrgency.High); StopWorker(worker);
SendNotifications(serviceProvider, NotificationUrgency.Normal); AllWorkers.Remove(worker);
SendNotifications(serviceProvider, NotificationUrgency.Low);
Thread.Sleep(2000);
} }
} }
private static void SendNotifications(IServiceProvider serviceProvider, NotificationUrgency urgency) private static readonly Dictionary<BaseWorker, Task<BaseWorker[]>> RunningWorkers = new();
public static BaseWorker[] GetRunningWorkers() => RunningWorkers.Keys.ToArray();
private static readonly HashSet<BaseWorker> StartWorkers = new();
private static void WorkerStarter(object? serviceProviderObj)
{ {
Log.Debug($"Sending notifications for {urgency}"); Log.Info("WorkerStarter Thread running.");
using IServiceScope scope = serviceProvider.CreateScope();
NotificationsContext context = scope.ServiceProvider.GetRequiredService<NotificationsContext>();
List<Notification> notifications = context.Notifications.Where(n => n.Urgency == urgency).ToList();
if (!notifications.Any())
return;
try
{
foreach (NotificationConnector notificationConnector in context.NotificationConnectors)
{
foreach (Notification notification in notifications)
notificationConnector.SendNotification(notification.Title, notification.Message);
}
context.Notifications.RemoveRange(notifications);
context.SaveChangesAsync();
}
catch (DbUpdateException e)
{
Log.Error("Error sending notifications.", e);
}
}
private static readonly Dictionary<Thread, Job> RunningJobs = new();
private static void JobStarter(object? serviceProviderObj)
{
Log.Info("JobStarter Thread running.");
if (serviceProviderObj is null) if (serviceProviderObj is null)
{ {
Log.Error("serviceProviderObj is null"); Log.Error("serviceProviderObj is null");
@ -123,171 +85,118 @@ public static class Tranga
IServiceProvider serviceProvider = (IServiceProvider)serviceProviderObj; IServiceProvider serviceProvider = (IServiceProvider)serviceProviderObj;
while (true) while (true)
{
Log.Debug("Starting Job-Cycle...");
DateTime cycleStart = DateTime.UtcNow;
using IServiceScope scope = serviceProvider.CreateScope();
PgsqlContext cycleContext = scope.ServiceProvider.GetRequiredService<PgsqlContext>();
//Get Running Jobs
List<Job> runningJobs = cycleContext.Jobs.GetRunningJobs();
DateTime filterStart = DateTime.UtcNow;
Log.Debug("Filtering Jobs...");
List<Job> waitingJobs = cycleContext.Jobs.GetWaitingJobs();
List<Job> dueJobs = waitingJobs.FilterDueJobs();
List<Job> jobsWithoutDependencies = dueJobs.FilterJobDependencies();
List<Job> startJobs = dueJobs;
Log.Debug($"Jobs Filtered! (took {DateTime.UtcNow.Subtract(filterStart).TotalMilliseconds}ms)");
//Start Jobs that are allowed to run (preconditions match)
foreach (Job job in startJobs)
{
bool running = false;
Thread t = new(() =>
{
using IServiceScope jobScope = serviceProvider.CreateScope();
PgsqlContext jobContext = jobScope.ServiceProvider.GetRequiredService<PgsqlContext>();
if (jobContext.Jobs.Find(job.Key) is not { } inContext)
return;
inContext.Run(jobContext, ref running); //FIND the job IN THE NEW CONTEXT!!!!!!! SO WE DON'T GET TRACKING PROBLEMS AND AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
});
RunningJobs.Add(t, job);
t.Start();
while(!running)
Thread.Sleep(10);
}
Log.Debug($"Running: {runningJobs.Count}\n" +
$"{string.Join("\n", runningJobs.Select(s => "\t- " + s))}\n" +
$"Waiting: {waitingJobs.Count} Due: {dueJobs.Count}\n" +
$"{string.Join("\n", dueJobs.Select(s => "\t- " + s))}\n" +
$"of which {jobsWithoutDependencies.Count} without missing dependencies, of which\n" +
$"{startJobs.Count} were started:\n" +
$"{string.Join("\n", startJobs.Select(s => "\t- " + s))}");
if (Log.IsDebugEnabled && dueJobs.Count < 1)
if(waitingJobs.MinBy(j => j.NextExecution) is { } nextJob)
Log.Debug($"Next job in {nextJob.NextExecution.Subtract(DateTime.UtcNow)} (at {nextJob.NextExecution}): {nextJob.Key}");
(Thread, Job)[] removeFromThreadsList = RunningJobs.Where(t => !t.Key.IsAlive)
.Select(t => (t.Key, t.Value)).ToArray();
Log.Debug($"Remove from Threads List: {removeFromThreadsList.Length}");
foreach ((Thread thread, Job job) thread in removeFromThreadsList)
{
RunningJobs.Remove(thread.thread);
}
try
{
cycleContext.SaveChanges();
}
catch (DbUpdateException e)
{
Log.Error("Failed saving Job changes.", e);
}
Log.Debug($"Job-Cycle over! (took {DateTime.UtcNow.Subtract(cycleStart).TotalMilliseconds}ms");
Thread.Sleep(TrangaSettings.startNewJobTimeoutMs);
}
}
private static List<Job> GetRunningJobs(this IQueryable<Job> jobs)
{
DateTime start = DateTime.UtcNow;
List<Job> ret = jobs.Where(j => j.state == JobState.Running).ToList();
DateTime end = DateTime.UtcNow;
Log.Debug($"Getting running Jobs took {end.Subtract(start).TotalMilliseconds}ms");
return ret;
}
private static List<Job> GetWaitingJobs(this IQueryable<Job> jobs)
{
DateTime start = DateTime.UtcNow;
List<Job> ret = jobs.Where(j => j.state == JobState.CompletedWaiting || j.state == JobState.FirstExecution).ToList();
DateTime end = DateTime.UtcNow;
Log.Debug($"Getting waiting Jobs took {end.Subtract(start).TotalMilliseconds}ms");
return ret;
}
private static List<Job> FilterDueJobs(this List<Job> jobs)
{
DateTime start = DateTime.UtcNow;
List<Job> ret = jobs.Where(j => j.NextExecution < DateTime.UtcNow).ToList();
DateTime end = DateTime.UtcNow;
Log.Debug($"Filtering Due Jobs took {end.Subtract(start).TotalMilliseconds}ms");
return ret;
}
private static List<Job> FilterJobDependencies(this List<Job> jobs)
{
DateTime start = DateTime.UtcNow;
List<Job> ret = jobs.Where(job => job.DependsOnJobs.All(j => j.IsCompleted)).ToList();
DateTime end = DateTime.UtcNow;
Log.Debug($"Filtering Dependencies took {end.Subtract(start).TotalMilliseconds}ms");
return ret;
}
private static List<Job> FilterJobsWithoutDownloading(this List<Job> jobs)
{
JobType[] types = [JobType.MoveFileOrFolderJob, JobType.MoveMangaLibraryJob, JobType.UpdateChaptersDownloadedJob];
DateTime start = DateTime.UtcNow;
List<Job> ret = jobs.Where(j => types.Contains(j.JobType)).ToList();
DateTime end = DateTime.UtcNow;
Log.Debug($"Filtering Jobs without Download took {end.Subtract(start).TotalMilliseconds}ms");
return ret;
}
private static List<Job> MatchJobsRunningAndWaiting(Dictionary<string, Dictionary<JobType, List<Job>>> running,
Dictionary<string, Dictionary<JobType, List<Job>>> waiting)
{
Log.Debug($"Matching {running.Count} running Jobs to {waiting.Count} waiting Jobs. Busy Connectors: {string.Join(", ", running.Select(r => r.Key))}");
DateTime start = DateTime.UtcNow;
List<Job> ret = new();
//Foreach MangaConnector
foreach ((string connector, Dictionary<JobType, List<Job>> jobTypeJobsWaiting) in waiting)
{ {
//Check if MangaConnector has a Job running CheckRunningWorkers();
if (running.TryGetValue(connector, out Dictionary<JobType, List<Job>>? jobTypeJobsRunning))
foreach (BaseWorker baseWorker in AllWorkers.DueWorkers())
StartWorkers.Add(baseWorker);
foreach (BaseWorker worker in StartWorkers.ToArray())
{ {
//MangaConnector has running Jobs if(RunningWorkers.ContainsKey(worker))
//Match per JobType (MangaConnector can have 1 Job per Type running at the same time) continue;
foreach ((JobType jobType, List<Job> jobsWaiting) in jobTypeJobsWaiting) if (worker is BaseWorkerWithContext<MangaContext> mangaContextWorker)
{ {
if(jobTypeJobsRunning.ContainsKey(jobType)) mangaContextWorker.SetScope(serviceProvider.CreateScope());
//Already a job of Type running on MangaConnector RunningWorkers.Add(mangaContextWorker, mangaContextWorker.DoWork());
continue; }else if (worker is BaseWorkerWithContext<NotificationsContext> notificationContextWorker)
if (jobType is not JobType.DownloadSingleChapterJob)
//If it is not a DownloadSingleChapterJob, just add the first
ret.Add(jobsWaiting.First());
else
//Add the Job with the lowest Chapternumber
ret.Add(jobsWaiting.OrderBy(j => ((DownloadSingleChapterJob)j).Chapter).First());
}
}
else
{
//MangaConnector has no running Jobs
foreach ((JobType jobType, List<Job> jobsWaiting) in jobTypeJobsWaiting)
{ {
if(ret.Any(j => j.JobType == jobType)) notificationContextWorker.SetScope(serviceProvider.CreateScope());
//Already a job of type to be started RunningWorkers.Add(notificationContextWorker, notificationContextWorker.DoWork());
continue; }else if (worker is BaseWorkerWithContext<LibraryContext> libraryContextWorker)
if (jobType is not JobType.DownloadSingleChapterJob) {
//If it is not a DownloadSingleChapterJob, just add the first libraryContextWorker.SetScope(serviceProvider.CreateScope());
ret.Add(jobsWaiting.First()); RunningWorkers.Add(libraryContextWorker, libraryContextWorker.DoWork());
else }else
//Add the Job with the lowest Chapternumber RunningWorkers.Add(worker, worker.DoWork());
ret.Add(jobsWaiting.OrderBy(j => ((DownloadSingleChapterJob)j).Chapter).First());
} StartWorkers.Remove(worker);
} }
Thread.Sleep(Settings.WorkCycleTimeoutMs);
} }
DateTime end = DateTime.UtcNow; }
Log.Debug($"Getting eligible jobs (not held back by Connector) took {end.Subtract(start).TotalMilliseconds}ms");
return ret; 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;
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 startNewJobTimeoutMs { 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("startNewJobTimeoutMs", JToken.FromObject(startNewJobTimeoutMs));
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("startNewJobTimeoutMs", out JToken? snjt))
startNewJobTimeoutMs = 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>()!;
} }
} }

120
API/Workers/BaseWorker.cs Normal file
View File

@ -0,0 +1,120 @@
using API.Schema;
using log4net;
namespace API.Workers;
public abstract class BaseWorker : Identifiable
{
/// <summary>
/// Workers this Worker depends on being completed before running.
/// </summary>
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);
/// <summary>
/// <see cref="AllDependencies"/> and Self.
/// </summary>
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 bool AllDependenciesFulfilled => DependsOn.All(d => d.State >= WorkerExecutionState.Completed);
internal WorkerExecutionState State { get; private set; }
private static readonly CancellationTokenSource CancellationTokenSource = new(TimeSpan.FromMinutes(10));
protected ILog Log { get; init; }
/// <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)
{
this.DependsOn = dependsOn?.ToArray() ?? [];
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()
{
Log.Debug($"Checking {this}");
this.State = WorkerExecutionState.Waiting;
BaseWorker[] missingDependenciesThatNeedStarting = MissingDependencies.Where(d => d.State < WorkerExecutionState.Waiting).ToArray();
if(missingDependenciesThatNeedStarting.Any())
return new Task<BaseWorker[]>(() => missingDependenciesThatNeedStarting);
if (MissingDependencies.Any())
return new Task<BaseWorker[]>(WaitForDependencies);
Log.Info($"Running {this}");
DateTime startTime = DateTime.UtcNow;
Task<BaseWorker[]> task = new (DoWorkInternal, CancellationTokenSource.Token);
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();
this.State = WorkerExecutionState.Running;
return task;
}
protected abstract BaseWorker[] DoWorkInternal();
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())
{
Thread.Sleep(Tranga.Settings.WorkCycleTimeoutMs);
}
return [this];
}
}
public enum WorkerExecutionState
{
Failed = 0,
Cancelled = 32,
Created = 64,
Waiting = 96,
Running = 128,
Completed = 192
}

View File

@ -0,0 +1,24 @@
using System.Configuration;
using Microsoft.EntityFrameworkCore;
namespace API.Workers;
public abstract class BaseWorkerWithContext<T>(IEnumerable<BaseWorker>? dependsOn = null) : BaseWorker(dependsOn) where T : DbContext
{
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();
}
}

9
API/Workers/IPeriodic.cs Normal file
View File

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

View File

@ -1,67 +1,39 @@
using System.ComponentModel.DataAnnotations;
using System.IO.Compression; using System.IO.Compression;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using API.MangaDownloadClients; using API.MangaDownloadClients;
using API.Schema.Contexts; using API.Schema.MangaContext;
using Microsoft.EntityFrameworkCore.Infrastructure; using API.Schema.MangaContext.MangaConnectors;
using Newtonsoft.Json;
using SixLabors.ImageSharp; using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Formats.Jpeg; using SixLabors.ImageSharp.Formats.Jpeg;
using SixLabors.ImageSharp.Processing; using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp.Processing.Processors.Binarization; using SixLabors.ImageSharp.Processing.Processors.Binarization;
using static System.IO.UnixFileMode; using static System.IO.UnixFileMode;
namespace API.Schema.Jobs; namespace API.Workers;
public class DownloadSingleChapterJob : JobWithDownloading public class DownloadChapterFromMangaconnectorWorker(MangaConnectorId<Chapter> chId, IEnumerable<BaseWorker>? dependsOn = null)
: BaseWorkerWithContext<MangaContext>(dependsOn)
{ {
[StringLength(64)] [Required] public string ChapterId { get; init; } = null!; internal readonly string MangaConnectorIdId = chId.Key;
private Chapter? _chapter; protected override BaseWorker[] DoWorkInternal()
[JsonIgnore]
public Chapter Chapter
{ {
get => LazyLoader.Load(this, ref _chapter) ?? throw new InvalidOperationException(); if (DbContext.MangaConnectorToChapter.Find(MangaConnectorIdId) is not { } MangaConnectorId)
init return []; //TODO Exception?
{ MangaConnector mangaConnector = MangaConnectorId.MangaConnector;
ChapterId = value.Key; Chapter chapter = MangaConnectorId.Obj;
_chapter = value; if (chapter.Downloaded)
}
}
public DownloadSingleChapterJob(Chapter chapter, Job? parentJob = null, ICollection<Job>? dependsOnJobs = null)
: base(TokenGen.CreateToken(typeof(DownloadSingleChapterJob)), JobType.DownloadSingleChapterJob, 0, parentJob, dependsOnJobs)
{
this.Chapter = chapter;
}
/// <summary>
/// EF ONLY!!!
/// </summary>
internal DownloadSingleChapterJob(ILazyLoader lazyLoader, string key, string chapterId, ulong recurrenceMs, string? parentJobId)
: base(lazyLoader, key, JobType.DownloadSingleChapterJob, recurrenceMs, parentJobId)
{
this.ChapterId = chapterId;
}
protected override IEnumerable<Job> RunInternal(PgsqlContext context)
{
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}");
return []; return [];
} }
string saveArchiveFilePath = Chapter.FullArchiveFilePath; string saveArchiveFilePath = chapter.FullArchiveFilePath;
Log.Debug($"Chapter path: {saveArchiveFilePath}"); Log.Debug($"Chapter path: {saveArchiveFilePath}");
//Check if Publication Directory already exists //Check if Publication Directory already exists
@ -69,7 +41,7 @@ public class DownloadSingleChapterJob : JobWithDownloading
if (directoryPath is null) if (directoryPath is null)
{ {
Log.Error($"Directory path could not be found: {saveArchiveFilePath}"); Log.Error($"Directory path could not be found: {saveArchiveFilePath}");
this.state = JobState.Failed; this.Fail();
return []; return [];
} }
if (!Directory.Exists(directoryPath)) if (!Directory.Exists(directoryPath))
@ -92,7 +64,7 @@ public class DownloadSingleChapterJob : JobWithDownloading
string tempFolder = Directory.CreateTempSubdirectory("trangatemp").FullName; string tempFolder = Directory.CreateTempSubdirectory("trangatemp").FullName;
Log.Debug($"Created temp folder: {tempFolder}"); Log.Debug($"Created temp folder: {tempFolder}");
Log.Info($"Downloading images: {Chapter}"); Log.Info($"Downloading images: {chapter}");
int chapterNum = 0; int chapterNum = 0;
//Download all Images to temporary Folder //Download all Images to temporary Folder
foreach (string imageUrl in imageUrls) foreach (string imageUrl in imageUrls)
@ -107,36 +79,27 @@ public class DownloadSingleChapterJob : JobWithDownloading
} }
} }
CopyCoverFromCacheToDownloadLocation(Chapter.ParentManga); CopyCoverFromCacheToDownloadLocation(chapter.ParentManga);
Log.Debug($"Creating ComicInfo.xml {Chapter}"); Log.Debug($"Creating ComicInfo.xml {chapter}");
File.WriteAllText(Path.Join(tempFolder, "ComicInfo.xml"), Chapter.GetComicInfoXmlString()); File.WriteAllText(Path.Join(tempFolder, "ComicInfo.xml"), chapter.GetComicInfoXmlString());
Log.Debug($"Packaging images to archive {Chapter}"); Log.Debug($"Packaging images to archive {chapter}");
//ZIP-it and ship-it //ZIP-it and ship-it
ZipFile.CreateFromDirectory(tempFolder, saveArchiveFilePath); ZipFile.CreateFromDirectory(tempFolder, saveArchiveFilePath);
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
File.SetUnixFileMode(saveArchiveFilePath, UserRead | UserWrite | UserExecute | GroupRead | GroupWrite | GroupExecute | OtherRead | OtherExecute); File.SetUnixFileMode(saveArchiveFilePath, UserRead | UserWrite | UserExecute | GroupRead | GroupWrite | GroupExecute | OtherRead | OtherExecute);
Directory.Delete(tempFolder, true); //Cleanup Directory.Delete(tempFolder, true); //Cleanup
Chapter.Downloaded = true; chapter.Downloaded = true;
context.SaveChanges(); DbContext.Sync();
if (context.Jobs.ToList().Any(j => return [];
{
if (j.JobType != JobType.UpdateChaptersDownloadedJob)
return false;
UpdateChaptersDownloadedJob job = (UpdateChaptersDownloadedJob)j;
return job.MangaId == Chapter.ParentMangaId;
}))
return [];
return [new UpdateChaptersDownloadedJob(Chapter.ParentManga, 0, this.ParentJob)];
} }
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;
@ -147,12 +110,12 @@ public class DownloadSingleChapterJob : JobWithDownloading
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)
@ -200,21 +163,23 @@ public class DownloadSingleChapterJob : JobWithDownloading
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}";
} }

Some files were not shown because too many files have changed in this diff Show More