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;
public record GotifyRecord(string endpoint, string appToken, int priority)
public record GotifyRecord(string Name, string Endpoint, string AppToken, int Priority)
{
public bool Validate()
{
if (endpoint == string.Empty)
if (Endpoint == string.Empty)
return false;
if (appToken == string.Empty)
if (AppToken == string.Empty)
return false;
if (priority < 0 || priority > 10)
if (Priority < 0 || Priority > 10)
return false;
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;
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()
{
if (endpoint == string.Empty)
if (Endpoint == string.Empty)
return false;
if (username == string.Empty)
if (Username == string.Empty)
return false;
if (password == string.Empty)
if (Password == string.Empty)
return false;
if (priority < 1 || priority > 5)
if (Priority < 1 || Priority > 5)
return false;
return true;
}

View File

@ -1,12 +1,12 @@
namespace API.APIEndpointRecords;
public record PushoverRecord(string apptoken, string user)
public record PushoverRecord(string Name, string AppToken, string User)
{
public bool Validate()
{
if (apptoken == string.Empty)
if (AppToken == string.Empty)
return false;
if (user == string.Empty)
if (User == string.Empty)
return false;
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.LibraryConnectors;
using API.Schema.LibraryContext;
using API.Schema.LibraryContext.LibraryConnectors;
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{v:apiVersion}/[controller]")]
public class LibraryConnectorController(LibraryContext context, ILog Log) : Controller
public class LibraryConnectorController(LibraryContext context) : Controller
{
/// <summary>
/// Gets all configured ToFileLibrary-Connectors
/// Gets all configured <see cref="LibraryConnector"/>
/// </summary>
/// <response code="200"></response>
[HttpGet]
@ -21,32 +21,31 @@ public class LibraryConnectorController(LibraryContext context, ILog Log) : Cont
public IActionResult GetAllConnectors()
{
LibraryConnector[] connectors = context.LibraryConnectors.ToArray();
return Ok(connectors);
}
/// <summary>
/// Returns ToFileLibrary-Connector with requested ID
/// Returns <see cref="LibraryConnector"/> with <paramref name="LibraryConnectorId"/>
/// </summary>
/// <param name="LibraryControllerId">ToFileLibrary-Connector-ID</param>
/// <param name="LibraryConnectorId"><see cref="LibraryConnector"/>.Key</param>
/// <response code="200"></response>
/// <response code="404">Connector with ID not found.</response>
[HttpGet("{LibraryControllerId}")]
/// <response code="404"><see cref="LibraryConnector"/> with <paramref name="LibraryConnectorId"/> not found.</response>
[HttpGet("{LibraryConnectorId}")]
[ProducesResponseType<LibraryConnector>(Status200OK, "application/json")]
[ProducesResponseType(Status404NotFound)]
public IActionResult GetConnector(string LibraryControllerId)
public IActionResult GetConnector(string LibraryConnectorId)
{
LibraryConnector? ret = context.LibraryConnectors.Find(LibraryControllerId);
return (ret is not null) switch
{
true => Ok(ret),
false => NotFound()
};
if (context.LibraryConnectors.Find(LibraryConnectorId) is not { } connector)
return NotFound();
return Ok(connector);
}
/// <summary>
/// Creates a new ToFileLibrary-Connector
/// Creates a new <see cref="LibraryConnector"/>
/// </summary>
/// <param name="libraryConnector">ToFileLibrary-Connector</param>
/// <param name="libraryConnector"></param>
/// <response code="201"></response>
/// <response code="500">Error during Database Operation</response>
[HttpPut]
@ -54,46 +53,34 @@ public class LibraryConnectorController(LibraryContext context, ILog Log) : Cont
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public IActionResult CreateConnector([FromBody]LibraryConnector libraryConnector)
{
try
{
context.LibraryConnectors.Add(libraryConnector);
context.SaveChanges();
if(context.Sync() is { success: false } result)
return StatusCode(Status500InternalServerError, result.exceptionMessage);
return Created();
}
catch (Exception e)
{
Log.Error(e);
return StatusCode(500, e.Message);
}
}
/// <summary>
/// Deletes the ToFileLibrary-Connector with the requested ID
/// Deletes <see cref="LibraryConnector"/> with <paramref name="LibraryConnectorId"/>
/// </summary>
/// <param name="LibraryControllerId">ToFileLibrary-Connector-ID</param>
/// <param name="LibraryConnectorId">ToFileLibrary-Connector-ID</param>
/// <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>
[HttpDelete("{LibraryControllerId}")]
[HttpDelete("{LibraryConnectorId}")]
[ProducesResponseType(Status200OK)]
[ProducesResponseType(Status404NotFound)]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public IActionResult DeleteConnector(string LibraryControllerId)
public IActionResult DeleteConnector(string LibraryConnectorId)
{
try
{
LibraryConnector? ret = context.LibraryConnectors.Find(LibraryControllerId);
if (ret is null)
if (context.LibraryConnectors.Find(LibraryConnectorId) is not { } connector)
return NotFound();
context.Remove(ret);
context.SaveChanges();
context.LibraryConnectors.Remove(connector);
if(context.Sync() is { success: false } result)
return StatusCode(Status500InternalServerError, result.exceptionMessage);
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.MangaConnectors;
using API.Schema.MangaContext;
using API.Schema.MangaContext.MangaConnectors;
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{v:apiVersion}/[controller]")]
public class MangaConnectorController(PgsqlContext context, ILog Log) : Controller
public class MangaConnectorController(MangaContext context) : Controller
{
/// <summary>
/// Get all available Connectors (Scanlation-Sites)
/// Get all <see cref="MangaConnector"/> (Scanlation-Sites)
/// </summary>
/// <response code="200"></response>
/// <response code="200">Names of <see cref="MangaConnector"/> (Scanlation-Sites)</response>
[HttpGet]
[ProducesResponseType<MangaConnector[]>(Status200OK, "application/json")]
public IActionResult GetConnectors()
{
MangaConnector[] connectors = context.MangaConnectors.ToArray();
return Ok(connectors);
return Ok(context.MangaConnectors.Select(c => c.Name).ToArray());
}
/// <summary>
/// Returns the MangaConnector with the requested Name
/// Returns the <see cref="MangaConnector"/> (Scanlation-Sites) with the requested Name
/// </summary>
/// <param name="MangaConnectorName"></param>
/// <param name="MangaConnectorName"><see cref="MangaConnector"/>.Name</param>
/// <response code="200"></response>
/// <response code="404">Connector with ID not found.</response>
/// <response code="500">Error during Database Operation</response>
/// <response code="404"><see cref="MangaConnector"/> (Scanlation-Sites) with Name not found.</response>
[HttpGet("{MangaConnectorName}")]
[ProducesResponseType<MangaConnector>(Status200OK, "application/json")]
[ProducesResponseType(Status404NotFound)]
public IActionResult GetConnector(string MangaConnectorName)
{
try
{
if(context.MangaConnectors.Find(MangaConnectorName) is not { } connector)
return NotFound();
return Ok(connector);
}
catch (Exception e)
{
Log.Error(e);
return StatusCode(500, e.Message);
}
}
/// <summary>
/// Get all enabled Connectors (Scanlation-Sites)
/// Get all enabled <see cref="MangaConnector"/> (Scanlation-Sites)
/// </summary>
/// <response code="200"></response>
[HttpGet("enabled")]
[HttpGet("Enabled")]
[ProducesResponseType<MangaConnector[]>(Status200OK, "application/json")]
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>
/// Get all disabled Connectors (Scanlation-Sites)
/// Get all disabled <see cref="MangaConnector"/> (Scanlation-Sites)
/// </summary>
/// <response code="200"></response>
[HttpGet("disabled")]
[HttpGet("Disabled")]
[ProducesResponseType<MangaConnector[]>(Status200OK, "application/json")]
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>
/// Enabled or disables a Connector
/// Enabled or disables <see cref="MangaConnector"/> (Scanlation-Sites) with Name
/// </summary>
/// <param name="MangaConnectorName">ID of the connector</param>
/// <param name="enabled">Set true to enable</param>
/// <response code="200"></response>
/// <response code="404">Connector with ID not found.</response>
/// <param name="MangaConnectorName"><see cref="MangaConnector"/>.Name</param>
/// <param name="Enabled">Set true to enable, false to disable</param>
/// <response code="202"></response>
/// <response code="404"><see cref="MangaConnector"/> (Scanlation-Sites) with Name not found.</response>
/// <response code="500">Error during Database Operation</response>
[HttpPatch("{MangaConnectorName}/SetEnabled/{enabled}")]
[ProducesResponseType(Status200OK)]
[HttpPatch("{MangaConnectorName}/SetEnabled/{Enabled}")]
[ProducesResponseType(Status202Accepted)]
[ProducesResponseType(Status404NotFound)]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public IActionResult SetEnabled(string MangaConnectorName, bool enabled)
public IActionResult SetEnabled(string MangaConnectorName, bool Enabled)
{
try
{
MangaConnector? connector = context.MangaConnectors.Find(MangaConnectorName);
if (connector is null)
if(context.MangaConnectors.Find(MangaConnectorName) is not { } connector)
return NotFound();
connector.Enabled = enabled;
context.SaveChanges();
connector.Enabled = Enabled;
return Ok();
}
catch (Exception e)
{
Log.Error(e);
return StatusCode(500, e.Message);
}
if(context.Sync() is { success: false } result)
return StatusCode(Status500InternalServerError, result.exceptionMessage);
return Accepted();
}
}

View File

@ -1,10 +1,8 @@
using API.Schema;
using API.Schema.Contexts;
using API.Schema.Jobs;
using API.Schema.MangaContext;
using API.Schema.MangaContext.MangaConnectors;
using API.Workers;
using Asp.Versioning;
using log4net;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Net.Http.Headers;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Formats.Jpeg;
@ -18,10 +16,10 @@ namespace API.Controllers;
[ApiVersion(2)]
[ApiController]
[Route("v{v:apiVersion}/[controller]")]
public class MangaController(PgsqlContext context, ILog Log) : Controller
public class MangaController(MangaContext context) : Controller
{
/// <summary>
/// Returns all cached Obj
/// Returns all cached <see cref="Manga"/>
/// </summary>
/// <response code="200"></response>
[HttpGet]
@ -33,41 +31,40 @@ public class MangaController(PgsqlContext context, ILog Log) : Controller
}
/// <summary>
/// Returns all cached Obj with IDs
/// Returns all cached <see cref="Manga"/> with <paramref name="MangaIds"/>
/// </summary>
/// <param name="ids">Array of Obj-IDs</param>
/// <param name="MangaIds">Array of <<see cref="Manga"/>.Key</param>
/// <response code="200"></response>
[HttpPost("WithIDs")]
[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);
}
/// <summary>
/// Return Obj with ID
/// Return <see cref="Manga"/> with <paramref name="MangaId"/>
/// </summary>
/// <param name="MangaId">Obj-ID</param>
/// <param name="MangaId"><see cref="Manga"/>.Key</param>
/// <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}")]
[ProducesResponseType<Manga>(Status200OK, "application/json")]
[ProducesResponseType(Status404NotFound)]
public IActionResult GetManga(string MangaId)
{
Manga? ret = context.Mangas.Find(MangaId);
if (ret is null)
return NotFound();
return Ok(ret);
if (context.Mangas.Find(MangaId) is not { } manga)
return NotFound(nameof(MangaId));
return Ok(manga);
}
/// <summary>
/// Delete Obj with ID
/// Delete <see cref="Manga"/> with <paramref name="MangaId"/>
/// </summary>
/// <param name="MangaId">Obj-ID</param>
/// <param name="MangaId"><see cref="Manga"/>.Key</param>
/// <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>
[HttpDelete("{MangaId}")]
[ProducesResponseType(Status200OK)]
@ -75,62 +72,50 @@ public class MangaController(PgsqlContext context, ILog Log) : Controller
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public IActionResult DeleteManga(string MangaId)
{
try
{
Manga? ret = context.Mangas.Find(MangaId);
if (ret is null)
return NotFound();
if (context.Mangas.Find(MangaId) is not { } manga)
return NotFound(nameof(MangaId));
context.Remove(ret);
context.SaveChanges();
context.Mangas.Remove(manga);
if(context.Sync() is { success: false } result)
return StatusCode(Status500InternalServerError, result.exceptionMessage);
return Ok();
}
catch (Exception e)
{
Log.Error(e);
return StatusCode(500, e.Message);
}
}
/// <summary>
/// Merge two Manga into one. THIS IS NOT REVERSIBLE!
/// Merge two <see cref="Manga"/> into one. THIS IS NOT REVERSIBLE!
/// </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="404">MangaId not found</response>
/// <response code="500">Error during Database Operation</response>
[HttpPatch("{MangaIdFrom}/MergeInto/{MangaIdTo}")]
/// <response code="404"><see cref="Manga"/> with <paramref name="MangaIdFrom"/> or <paramref name="MangaIdInto"/> not found</response>
[HttpPatch("{MangaIdFrom}/MergeInto/{MangaIdInto}")]
[ProducesResponseType<byte[]>(Status200OK,"image/jpeg")]
[ProducesResponseType(Status404NotFound)]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public IActionResult MergeIntoManga(string MangaIdFrom, string MangaIdTo)
public IActionResult MergeIntoManga(string MangaIdFrom, string MangaIdInto)
{
if (context.Mangas.Find(MangaIdFrom) is not { } from)
return NotFound(MangaIdFrom);
if(context.Mangas.Find(MangaIdTo) is not { } to)
return NotFound(MangaIdTo);
try
{
to.MergeFrom(from, context);
return NotFound(nameof(MangaIdFrom));
if (context.Mangas.Find(MangaIdInto) is not { } into)
return NotFound(nameof(MangaIdInto));
BaseWorker[] newJobs = into.MergeFrom(from, context);
Tranga.AddWorkers(newJobs);
return Ok();
}
catch (DbUpdateException e)
{
Log.Error(e);
return StatusCode(500, e.Message);
}
}
/// <summary>
/// Returns Cover of Obj
/// Returns Cover of <see cref="Manga"/> with <paramref name="MangaId"/>
/// </summary>
/// <param name="MangaId">Obj-ID</param>
/// <param name="width">If width is provided, height needs to also be provided</param>
/// <param name="height">If height is provided, width needs to also be provided</param>
/// <param name="MangaId"><see cref="Manga"/>.Key</param>
/// <param name="width">If <paramref name="width"/> is provided, <paramref name="height"/> 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="204">Cover not loaded</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>
[HttpGet("{MangaId}/Cover")]
[ProducesResponseType<byte[]>(Status200OK,"image/jpeg")]
@ -140,22 +125,21 @@ public class MangaController(PgsqlContext context, ILog Log) : Controller
[ProducesResponseType<int>(Status503ServiceUnavailable, "text/plain")]
public IActionResult GetCover(string MangaId, [FromQuery]int? width, [FromQuery]int? height)
{
if(context.Mangas.Find(MangaId) is not { } m)
return NotFound();
if (context.Mangas.Find(MangaId) is not { } manga)
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 (coverDownloadJobs.Any(j => j is DownloadMangaCoverJob dmc && dmc.MangaId == MangaId && dmc.state < JobState.Completed))
if (Tranga.GetRunningWorkers().Any(worker => worker is DownloadCoverFromMangaconnectorWorker w && context.MangaConnectorToManga.Find(w.MangaConnectorIdId)?.ObjId == MangaId))
{
Response.Headers.Append("Retry-After", $"{TrangaSettings.startNewJobTimeoutMs * coverDownloadJobs.Count() * 2 / 1000:D}");
return StatusCode(Status503ServiceUnavailable, TrangaSettings.startNewJobTimeoutMs * coverDownloadJobs.Count() * 2 / 1000);
Response.Headers.Append("Retry-After", $"{Tranga.Settings.WorkCycleTimeoutMs * 2 / 1000:D}");
return StatusCode(Status503ServiceUnavailable, Tranga.Settings.WorkCycleTimeoutMs * 2 / 1000);
}
else
return NoContent();
}
Image image = Image.Load(m.CoverFileNameInCache);
Image image = Image.Load(manga.CoverFileNameInCache);
if (width is { } w && height is { } h)
{
@ -170,46 +154,46 @@ public class MangaController(PgsqlContext context, ILog Log) : Controller
using MemoryStream ms = new();
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";
return File(ms.GetBuffer(), "image/jpeg", new DateTimeOffset(lastModified), EntityTagHeaderValue.Parse($"\"{lastModified.Ticks}\""));
}
/// <summary>
/// Returns all Chapters of Obj
/// Returns all <see cref="Chapter"/> of <see cref="Manga"/> with <paramref name="MangaId"/>
/// </summary>
/// <param name="MangaId">Obj-ID</param>
/// <param name="MangaId"><see cref="Manga"/>.Key</param>
/// <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")]
[ProducesResponseType<Chapter[]>(Status200OK, "application/json")]
[ProducesResponseType(Status404NotFound)]
public IActionResult GetChapters(string MangaId)
{
if(context.Mangas.Find(MangaId) is not { } m)
return NotFound();
if (context.Mangas.Find(MangaId) is not { } manga)
return NotFound(nameof(MangaId));
Chapter[] chapters = m.Chapters.ToArray();
Chapter[] chapters = manga.Chapters.ToArray();
return Ok(chapters);
}
/// <summary>
/// Returns all downloaded Chapters for Obj with ID
/// Returns all downloaded <see cref="Chapter"/> for <see cref="Manga"/> with <paramref name="MangaId"/>
/// </summary>
/// <param name="MangaId">Obj-ID</param>
/// <param name="MangaId"><see cref="Manga"/>.Key</param>
/// <response code="200"></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")]
[ProducesResponseType<Chapter[]>(Status200OK, "application/json")]
[ProducesResponseType(Status204NoContent)]
[ProducesResponseType(Status404NotFound)]
public IActionResult GetChaptersDownloaded(string MangaId)
{
if(context.Mangas.Find(MangaId) is not { } m)
return NotFound();
if (context.Mangas.Find(MangaId) is not { } manga)
return NotFound(nameof(MangaId));
List<Chapter> chapters = m.Chapters.ToList();
List<Chapter> chapters = manga.Chapters.Where(c => c.Downloaded).ToList();
if (chapters.Count == 0)
return NoContent();
@ -217,22 +201,22 @@ public class MangaController(PgsqlContext context, ILog Log) : Controller
}
/// <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>
/// <param name="MangaId">Obj-ID</param>
/// <param name="MangaId"><see cref="Manga"/>.Key</param>
/// <response code="200"></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")]
[ProducesResponseType<Chapter[]>(Status200OK, "application/json")]
[ProducesResponseType(Status204NoContent)]
[ProducesResponseType(Status404NotFound)]
public IActionResult GetChaptersNotDownloaded(string MangaId)
{
if(context.Mangas.Find(MangaId) is not { } m)
return NotFound();
if (context.Mangas.Find(MangaId) is not { } manga)
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)
return NoContent();
@ -240,13 +224,13 @@ public class MangaController(PgsqlContext context, ILog Log) : Controller
}
/// <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>
/// <param name="MangaId">Obj-ID</param>
/// <param name="MangaId"><see cref="Manga"/>.Key</param>
/// <response code="200"></response>
/// <response code="204">No available chapters</response>
/// <response code="404">Obj with ID not found.</response>
/// <response code="500">Could not retrieve the maximum chapter-number</response>
/// <response code="404"><see cref="Manga"/> with <paramref name="MangaId"/> not found.</response>
/// <response code="412">Could not retrieve the maximum chapter-number</response>
/// <response code="503">Retry after timeout, updating value</response>
[HttpGet("{MangaId}/Chapter/LatestAvailable")]
[ProducesResponseType<Chapter>(Status200OK, "application/json")]
@ -256,130 +240,151 @@ public class MangaController(PgsqlContext context, ILog Log) : Controller
[ProducesResponseType<int>(Status503ServiceUnavailable, "text/plain")]
public IActionResult GetLatestChapter(string MangaId)
{
if(context.Mangas.Find(MangaId) is not { } m)
return NotFound();
if (context.Mangas.Find(MangaId) is not { } manga)
return NotFound(nameof(MangaId));
List<Chapter> chapters = m.Chapters.ToList();
List<Chapter> chapters = manga.Chapters.ToList();
if (chapters.Count == 0)
{
List<Job> retrieveChapterJobs = context.Jobs.Where(j => j.JobType == JobType.RetrieveChaptersJob).Include(j => ((RetrieveChaptersJob)j).Manga).ToList();
if (retrieveChapterJobs.Any(j => j is RetrieveChaptersJob rcj && rcj.MangaId == MangaId && rcj.state < JobState.Completed))
if (Tranga.GetRunningWorkers().Any(worker => worker is RetrieveMangaChaptersFromMangaconnectorWorker w && context.MangaConnectorToManga.Find(w.MangaConnectorIdId)?.ObjId == MangaId && w.State < WorkerExecutionState.Completed))
{
Response.Headers.Append("Retry-After", $"{TrangaSettings.startNewJobTimeoutMs * retrieveChapterJobs.Count() * 2 / 1000:D}");
return StatusCode(Status503ServiceUnavailable, TrangaSettings.startNewJobTimeoutMs * retrieveChapterJobs.Count() * 2/ 1000);
Response.Headers.Append("Retry-After", $"{Tranga.Settings.WorkCycleTimeoutMs * 2 / 1000:D}");
return StatusCode(Status503ServiceUnavailable, Tranga.Settings.WorkCycleTimeoutMs * 2/ 1000);
}else
return Ok(0);
}
Chapter? max = chapters.Max();
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);
}
/// <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>
/// <param name="MangaId">Obj-ID</param>
/// <param name="MangaId"><see cref="Manga"/>.Key</param>
/// <response code="200"></response>
/// <response code="204">No available chapters</response>
/// <response code="404">Obj with ID not found.</response>
/// <response code="500">Could not retrieve the maximum chapter-number</response>
/// <response code="404"><see cref="Manga"/> with <paramref name="MangaId"/> not found.</response>
/// <response code="412">Could not retrieve the maximum chapter-number</response>
/// <response code="503">Retry after timeout, updating value</response>
[HttpGet("{MangaId}/Chapter/LatestDownloaded")]
[ProducesResponseType<Chapter>(Status200OK, "application/json")]
[ProducesResponseType(Status204NoContent)]
[ProducesResponseType(Status404NotFound)]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
[ProducesResponseType<string>(Status412PreconditionFailed, "text/plain")]
[ProducesResponseType<int>(Status503ServiceUnavailable, "text/plain")]
public IActionResult GetLatestChapterDownloaded(string MangaId)
{
if(context.Mangas.Find(MangaId) is not { } m)
return NotFound();
if (context.Mangas.Find(MangaId) is not { } manga)
return NotFound(nameof(MangaId));
List<Chapter> chapters = m.Chapters.ToList();
List<Chapter> chapters = manga.Chapters.ToList();
if (chapters.Count == 0)
{
List<Job> retrieveChapterJobs = context.Jobs.Where(j => j.JobType == JobType.RetrieveChaptersJob).Include(j => ((RetrieveChaptersJob)j).Manga).ToList();
if (retrieveChapterJobs.Any(j => j is RetrieveChaptersJob rcj && rcj.MangaId == MangaId && rcj.state < JobState.Completed))
if (Tranga.GetRunningWorkers().Any(worker => worker is RetrieveMangaChaptersFromMangaconnectorWorker w && context.MangaConnectorToManga.Find(w.MangaConnectorIdId)?.ObjId == MangaId && w.State < WorkerExecutionState.Completed))
{
Response.Headers.Append("Retry-After", $"{TrangaSettings.startNewJobTimeoutMs * retrieveChapterJobs.Count() * 2 / 1000:D}");
return StatusCode(Status503ServiceUnavailable, TrangaSettings.startNewJobTimeoutMs * retrieveChapterJobs.Count() * 2 / 1000);
Response.Headers.Append("Retry-After", $"{Tranga.Settings.WorkCycleTimeoutMs * 2 / 1000:D}");
return StatusCode(Status503ServiceUnavailable, Tranga.Settings.WorkCycleTimeoutMs * 2/ 1000);
}else
return NoContent();
}
Chapter? max = chapters.Max();
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);
}
/// <summary>
/// Configure the cut-off for Obj
/// Configure the <see cref="Chapter"/> cut-off for <see cref="Manga"/>
/// </summary>
/// <param name="MangaId">Obj-ID</param>
/// <param name="chapterThreshold">Threshold (Chapter Number)</param>
/// <response code="200"></response>
/// <response code="404">Obj with ID not found.</response>
/// <param name="MangaId"><see cref="Manga"/>.Key</param>
/// <param name="chapterThreshold">Threshold (<see cref="Chapter"/> ChapterNumber)</param>
/// <response code="202"></response>
/// <response code="404"><see cref="Manga"/> with <paramref name="MangaId"/> not found.</response>
/// <response code="500">Error during Database Operation</response>
[HttpPatch("{MangaId}/IgnoreChaptersBefore")]
[ProducesResponseType(Status200OK)]
[ProducesResponseType(Status202Accepted)]
[ProducesResponseType(Status404NotFound)]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public IActionResult IgnoreChaptersBefore(string MangaId, [FromBody]float chapterThreshold)
{
Manga? m = context.Mangas.Find(MangaId);
if (m is null)
if (context.Mangas.Find(MangaId) is not { } manga)
return NotFound();
try
{
m.IgnoreChaptersBefore = chapterThreshold;
context.SaveChanges();
return Ok();
}
catch (Exception e)
{
Log.Error(e);
return StatusCode(500, e.Message);
}
manga.IgnoreChaptersBefore = chapterThreshold;
if(context.Sync() is { success: false } result)
return StatusCode(Status500InternalServerError, result.exceptionMessage);
return Accepted();
}
/// <summary>
/// Move Obj to different ToFileLibrary
/// Move <see cref="Manga"/> to different <see cref="FileLibrary"/>
/// </summary>
/// <param name="MangaId">Obj-ID</param>
/// <param name="LibraryId">ToFileLibrary-Id</param>
/// <param name="MangaId"><see cref="Manga"/>.Key</param>
/// <param name="LibraryId"><see cref="FileLibrary"/>.Key</param>
/// <response code="202">Folder is going to be moved</response>
/// <response code="404">MangaId or LibraryId not found</response>
/// <response code="500">Error during Database Operation</response>
/// <response code="404"><paramref name="MangaId"/> or <paramref name="LibraryId"/> not found</response>
[HttpPost("{MangaId}/ChangeLibrary/{LibraryId}")]
[ProducesResponseType(Status202Accepted)]
[ProducesResponseType(Status404NotFound)]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public IActionResult MoveFolder(string MangaId, string LibraryId)
{
if (context.Mangas.Find(MangaId) is not { } manga)
return NotFound();
if(context.LocalLibraries.Find(LibraryId) is not { } library)
return NotFound();
return NotFound(nameof(MangaId));
if(context.FileLibraries.Find(LibraryId) is not { } library)
return NotFound(nameof(LibraryId));
MoveMangaLibraryJob moveLibrary = new(manga, library);
UpdateChaptersDownloadedJob updateDownloadedFiles = new(manga, 0, dependsOnJobs: [moveLibrary]);
MoveMangaLibraryWorker moveLibrary = new(manga, library);
Tranga.AddWorkers([moveLibrary]);
try
{
context.Jobs.AddRange(moveLibrary, updateDownloadedFiles);
context.SaveChanges();
return Accepted();
}
catch (Exception e)
/// <summary>
/// (Un-)Marks <see cref="Manga"/> as requested for Download from <see cref="MangaConnector"/>
/// </summary>
/// <param name="MangaId"><see cref="Manga"/> with <paramref name="MangaId"/></param>
/// <param name="MangaConnectorName"><see cref="MangaConnector"/> with <paramref name="MangaConnectorName"/></param>
/// <param name="IsRequested">true to mark as requested, false to mark as not-requested</param>
/// <response code="200"></response>
/// <response code="404"><paramref name="MangaId"/> or <paramref name="MangaConnectorName"/> not found</response>
/// <response code="412"><see cref="Manga"/> was not linked to <see cref="MangaConnector"/>, so nothing changed</response>
/// <response code="428"><see cref="Manga"/> is not linked to <see cref="MangaConnector"/> yet. Search for <see cref="Manga"/> on <see cref="MangaConnector"/> first (to create a <see cref="MangaConnectorId{T}"/>).</response>
/// <response code="500">Error during Database Operation</response>
[HttpPost("{MangaId}/SetAsDownloadFrom/{MangaConnectorName}/{IsRequested}")]
[ProducesResponseType(Status200OK)]
[ProducesResponseType<string>(Status404NotFound, "text/plain")]
[ProducesResponseType<string>(Status412PreconditionFailed, "text/plain")]
[ProducesResponseType<string>(Status428PreconditionRequired, "text/plain")]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public IActionResult MarkAsRequested(string MangaId, string MangaConnectorName, bool IsRequested)
{
Log.Error(e);
return StatusCode(500, e.Message);
}
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.Contexts;
using API.Schema.MetadataFetchers;
using API.Schema.MangaContext;
using API.Schema.MangaContext.MetadataFetchers;
using Asp.Versioning;
using log4net;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using static Microsoft.AspNetCore.Http.StatusCodes;
@ -13,38 +11,39 @@ namespace API.Controllers;
[ApiVersion(2)]
[ApiController]
[Route("v{v:apiVersion}/[controller]")]
public class MetadataFetcherController(PgsqlContext context, ILog Log) : Controller
public class MetadataFetcherController(MangaContext context) : Controller
{
/// <summary>
/// Get all available Connectors (Metadata-Sites)
/// Get all <see cref="MetadataFetcher"/> (Metadata-Sites)
/// </summary>
/// <response code="200">Names of Metadata-Fetchers</response>
/// <response code="200">Names of <see cref="MetadataFetcher"/> (Metadata-Sites)</response>
[HttpGet]
[ProducesResponseType<string[]>(Status200OK, "application/json")]
public IActionResult GetConnectors()
{
string[] connectors = Tranga.MetadataFetchers.Select(f => f.MetadataFetcherName).ToArray();
return Ok(connectors);
return Ok(Tranga.MetadataFetchers.Select(m => m.Name).ToArray());
}
/// <summary>
/// Returns all Mangas which have a linked Metadata-Provider
/// Returns all <see cref="MetadataEntry"/>
/// </summary>
/// <response code="200"></response>
[HttpGet("Links")]
[ProducesResponseType<MetadataEntry>(Status200OK, "application/json")]
[ProducesResponseType<MetadataEntry[]>(Status200OK, "application/json")]
public IActionResult GetLinkedEntries()
{
return Ok(context.MetadataEntries.ToArray());
}
/// <summary>
/// Searches Metadata-Provider for Manga-Metadata
/// Searches <see cref="MetadataFetcher"/> (Metadata-Sites) for Manga-Metadata
/// </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="400">Metadata-fetcher with Name does not exist</response>
/// <response code="404">Manga with ID not found</response>
/// <response code="400"><see cref="MetadataFetcher"/> (Metadata-Sites) with <paramref name="MetadataFetcherName"/> does not exist</response>
/// <response code="404"><see cref="Manga"/> with <paramref name="MangaId"/> not found</response>
[HttpPost("{MetadataFetcherName}/SearchManga/{MangaId}")]
[ProducesResponseType<MetadataSearchResult[]>(Status200OK, "application/json")]
[ProducesResponseType(Status400BadRequest)]
@ -53,7 +52,7 @@ public class MetadataFetcherController(PgsqlContext context, ILog Log) : Control
{
if(context.Mangas.Find(MangaId) is not { } manga)
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();
MetadataSearchResult[] searchResults = searchTerm is null ? fetcher.SearchMetadataEntry(manga) : fetcher.SearchMetadataEntry(searchTerm);
@ -61,14 +60,17 @@ public class MetadataFetcherController(PgsqlContext context, ILog Log) : Control
}
/// <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>
/// <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="400">Metadata-fetcher with Name does not exist</response>
/// <response code="404">Manga with ID not found</response>
/// <response code="400"><see cref="MetadataFetcher"/> (Metadata-Sites) with <paramref name="MetadataFetcherName"/> does not exist</response>
/// <response code="404"><see cref="Manga"/> with <paramref name="MangaId"/> not found</response>
/// <response code="500">Error during Database Operation</response>
[HttpPost("{MetadataFetcherName}/Link/{MangaId}")]
[ProducesResponseType(Status200OK)]
[ProducesResponseType<MetadataEntry>(Status200OK, "application/json")]
[ProducesResponseType(Status400BadRequest)]
[ProducesResponseType(Status404NotFound)]
[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)
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();
MetadataEntry entry = fetcher.CreateMetadataEntry(manga, Identifier);
try
{
MetadataEntry entry = fetcher.CreateMetadataEntry(manga, Identifier);
context.MetadataEntries.Add(entry);
context.SaveChanges();
}
catch (Exception e)
{
Log.Error(e);
return StatusCode(500, e.Message);
}
return Ok();
if(context.Sync() is { } errorMessage)
return StatusCode(Status500InternalServerError, errorMessage);
return Ok(entry);
}
/// <summary>
/// Un-Links Metadata-Provider using Provider-Specific Identifier to Manga
/// Un-Links <see cref="MetadataFetcher"/> (Metadata-Sites) from <see cref="Manga"/>
/// </summary>
/// <response code="200"></response>
/// <response code="400">Metadata-fetcher with Name does not exist</response>
/// <response code="404">Manga with ID not found</response>
/// <response code="412">No Entry linking Manga and Metadata-Provider found</response>
/// <response code="400"><see cref="MetadataFetcher"/> (Metadata-Sites) with <paramref name="MetadataFetcherName"/> does not exist</response>
/// <response code="404"><see cref="Manga"/> with <paramref name="MangaId"/> not 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>
[HttpPost("{MetadataFetcherName}/Unlink/{MangaId}")]
[ProducesResponseType(Status200OK)]
@ -109,58 +105,17 @@ public class MetadataFetcherController(PgsqlContext context, ILog Log) : Control
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public IActionResult UnlinkMangaMetadata(string MangaId, string MetadataFetcherName)
{
if(context.Mangas.Find(MangaId) is not { } manga)
if(context.Mangas.Find(MangaId) is null)
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();
MetadataEntry? entry = context.MetadataEntries.FirstOrDefault(e => e.MangaId == MangaId && e.MetadataFetcherName == MetadataFetcherName);
if (entry is null)
if(context.MetadataEntries.FirstOrDefault(e => e.MangaId == MangaId && e.MetadataFetcherName == MetadataFetcherName) is not { } entry)
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>
/// Tries linking a Manga to a Metadata-Provider-Site
/// </summary>
/// <response code="200"></response>
/// <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);
}
context.Remove(entry);
if(context.Sync() is { success: false } result)
return StatusCode(Status500InternalServerError, result.exceptionMessage);
return Ok();
}
}

View File

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

View File

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

View File

@ -1,10 +1,7 @@
using API.Schema;
using API.Schema.Contexts;
using API.Schema.MangaContext;
using API.Schema.MangaContext.MangaConnectors;
using Asp.Versioning;
using log4net;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Soenneker.Utils.String.NeedlemanWunsch;
using static Microsoft.AspNetCore.Http.StatusCodes;
// ReSharper disable InconsistentNaming
@ -13,75 +10,50 @@ namespace API.Controllers;
[ApiVersion(2)]
[ApiController]
[Route("v{v:apiVersion}/[controller]")]
public class SearchController(PgsqlContext context, ILog Log) : Controller
public class SearchController(MangaContext context) : Controller
{
/// <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>
/// <param name="MangaConnectorName"></param>
/// <param name="Query"></param>
/// <param name="MangaConnectorName"><see cref="MangaConnector"/>.Name</param>
/// <param name="Query">searchTerm</param>
/// <response code="200"></response>
/// <response code="404">MangaConnector with ID not found</response>
/// <response code="406">MangaConnector with ID is disabled</response>
/// <response code="500">Error during Database Operation</response>
/// <response code="404"><see cref="MangaConnector"/> with Name not found</response>
/// <response code="412"><see cref="MangaConnector"/> with Name is disabled</response>
[HttpGet("{MangaConnectorName}/{Query}")]
[ProducesResponseType<Manga[]>(Status200OK, "application/json")]
[ProducesResponseType(Status404NotFound)]
[ProducesResponseType(Status406NotAcceptable)]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public IActionResult SearchManga(string MangaConnectorName, string Query)
{
if(context.MangaConnectors.Find(MangaConnectorName) is not { } connector)
return NotFound();
else if (connector.Enabled is false)
return StatusCode(Status406NotAcceptable);
if (connector.Enabled is false)
return StatusCode(Status412PreconditionFailed);
(Manga, MangaConnectorId<Manga>)[] mangas = connector.SearchManga(Query);
List<Manga> retMangas = new();
foreach ((Manga manga, MangaConnectorId<Manga> mcId) manga in mangas)
{
try
{
if(AddMangaToContext(manga) is { } add)
if(Tranga.AddMangaToContext(manga, context, out Manga? add))
retMangas.Add(add);
}
catch (DbUpdateException e)
{
Log.Error(e);
return StatusCode(Status500InternalServerError, e.Message);
}
}
return Ok(retMangas.ToArray());
}
/// <summary>
/// Search for a known Obj
/// Returns <see cref="Manga"/> from the <see cref="MangaConnector"/> associated with <paramref name="url"/>
/// </summary>
/// <param name="Query"></param>
/// <param name="url"></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>
/// Returns Obj from MangaConnector associated with URL
/// </summary>
/// <param name="url">Obj-Page URL</param>
/// <response code="200"></response>
/// <response code="300">Multiple connectors found for URL</response>
/// <response code="404">Obj not found</response>
/// <response code="300">Multiple <see cref="MangaConnector"/> found for URL</response>
/// <response code="404"><see cref="Manga"/> not found</response>
/// <response code="500">Error during Database Operation</response>
[HttpPost("Url")]
[ProducesResponseType<Manga>(Status200OK, "application/json")]
[ProducesResponseType(Status404NotFound)]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
[ProducesResponseType(Status500InternalServerError)]
public IActionResult GetMangaFromUrl([FromBody]string url)
{
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)
return NotFound();
try
{
if(AddMangaToContext(manga) is { } add)
return Ok(add);
if(Tranga.AddMangaToContext(manga, context, out Manga? add) == false)
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 =>
{
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;
return Ok(add);
}
}

View File

@ -1,29 +1,25 @@
using API.MangaDownloadClients;
using API.Schema;
using API.Schema.Contexts;
using API.Schema.Jobs;
using Asp.Versioning;
using log4net;
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json.Linq;
using static Microsoft.AspNetCore.Http.StatusCodes;
// ReSharper disable InconsistentNaming
namespace API.Controllers;
[ApiVersion(2)]
[ApiController]
[Route("v{v:apiVersion}/[controller]")]
public class SettingsController(PgsqlContext context, ILog Log) : Controller
public class SettingsController() : Controller
{
/// <summary>
/// Get all Settings
/// Get all <see cref="Tranga.Settings"/>
/// </summary>
/// <response code="200"></response>
[HttpGet]
[ProducesResponseType<JObject>(Status200OK, "application/json")]
[ProducesResponseType<TrangaSettings>(Status200OK, "application/json")]
public IActionResult GetSettings()
{
return Ok(JObject.Parse(TrangaSettings.Serialize()));
return Ok(Tranga.Settings);
}
/// <summary>
@ -34,7 +30,7 @@ public class SettingsController(PgsqlContext context, ILog Log) : Controller
[ProducesResponseType<string>(Status200OK, "text/plain")]
public IActionResult GetUserAgent()
{
return Ok(TrangaSettings.userAgent);
return Ok(Tranga.Settings.UserAgent);
}
/// <summary>
@ -45,7 +41,8 @@ public class SettingsController(PgsqlContext context, ILog Log) : Controller
[ProducesResponseType(Status200OK)]
public IActionResult SetUserAgent([FromBody]string userAgent)
{
TrangaSettings.UpdateUserAgent(userAgent);
//TODO Validate
Tranga.Settings.SetUserAgent(userAgent);
return Ok();
}
@ -57,7 +54,7 @@ public class SettingsController(PgsqlContext context, ILog Log) : Controller
[ProducesResponseType(Status200OK)]
public IActionResult ResetUserAgent()
{
TrangaSettings.UpdateUserAgent(TrangaSettings.DefaultUserAgent);
Tranga.Settings.SetUserAgent(TrangaSettings.DefaultUserAgent);
return Ok();
}
@ -69,7 +66,7 @@ public class SettingsController(PgsqlContext context, ILog Log) : Controller
[ProducesResponseType<Dictionary<RequestType,int>>(Status200OK, "application/json")]
public IActionResult GetRequestLimits()
{
return Ok(TrangaSettings.requestLimits);
return Ok(Tranga.Settings.RequestLimits);
}
/// <summary>
@ -97,7 +94,7 @@ public class SettingsController(PgsqlContext context, ILog Log) : Controller
{
if (requestLimit <= 0)
return BadRequest();
TrangaSettings.UpdateRequestLimit(RequestType, requestLimit);
Tranga.Settings.SetRequestLimit(RequestType, requestLimit);
return Ok();
}
@ -109,7 +106,7 @@ public class SettingsController(PgsqlContext context, ILog Log) : Controller
[ProducesResponseType<string>(Status200OK)]
public IActionResult ResetRequestLimits(RequestType RequestType)
{
TrangaSettings.UpdateRequestLimit(RequestType, TrangaSettings.DefaultRequestLimits[RequestType]);
Tranga.Settings.SetRequestLimit(RequestType, TrangaSettings.DefaultRequestLimits[RequestType]);
return Ok();
}
@ -121,35 +118,35 @@ public class SettingsController(PgsqlContext context, ILog Log) : Controller
[ProducesResponseType<string>(Status200OK)]
public IActionResult ResetRequestLimits()
{
TrangaSettings.ResetRequestLimits();
Tranga.Settings.ResetRequestLimits();
return Ok();
}
/// <summary>
/// Returns Level of Image-Compression for Images
/// </summary>
/// <response code="200">JPEG compression-level as Integer</response>
[HttpGet("ImageCompression")]
/// <response code="200">JPEG ImageCompression-level as Integer</response>
[HttpGet("ImageCompressionLevel")]
[ProducesResponseType<int>(Status200OK, "text/plain")]
public IActionResult GetImageCompression()
{
return Ok(TrangaSettings.compression);
return Ok(Tranga.Settings.ImageCompression);
}
/// <summary>
/// Set the Image-Compression-Level for Images
/// </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="400">Level outside permitted range</response>
[HttpPatch("ImageCompression")]
[HttpPatch("ImageCompressionLevel/{level}")]
[ProducesResponseType(Status200OK)]
[ProducesResponseType(Status400BadRequest)]
public IActionResult SetImageCompression([FromBody]int level)
public IActionResult SetImageCompression(int level)
{
if (level < 1 || level > 100)
return BadRequest();
TrangaSettings.UpdateCompressImages(level);
Tranga.Settings.UpdateImageCompression(level);
return Ok();
}
@ -161,7 +158,7 @@ public class SettingsController(PgsqlContext context, ILog Log) : Controller
[ProducesResponseType<bool>(Status200OK, "text/plain")]
public IActionResult GetBwImagesToggle()
{
return Ok(TrangaSettings.bwImages);
return Ok(Tranga.Settings.BlackWhiteImages);
}
/// <summary>
@ -169,37 +166,11 @@ public class SettingsController(PgsqlContext context, ILog Log) : Controller
/// </summary>
/// <param name="enabled">true to enable</param>
/// <response code="200"></response>
[HttpPatch("BWImages")]
[HttpPatch("BWImages/{enabled}")]
[ProducesResponseType(Status200OK)]
public IActionResult SetBwImagesToggle([FromBody]bool enabled)
public IActionResult SetBwImagesToggle(bool enabled)
{
TrangaSettings.UpdateBwImages(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);
Tranga.Settings.SetBlackWhiteImageEnabled(enabled);
return Ok();
}
@ -225,7 +196,7 @@ public class SettingsController(PgsqlContext context, ILog Log) : Controller
[ProducesResponseType<string>(Status200OK, "text/plain")]
public IActionResult GetCustomNamingScheme()
{
return Ok(TrangaSettings.chapterNamingScheme);
return Ok(Tranga.Settings.ChapterNamingScheme);
}
/// <summary>
@ -238,58 +209,20 @@ public class SettingsController(PgsqlContext context, ILog Log) : Controller
/// %C Chapter
/// %T Title
/// %A Author (first in list)
/// %I Chapter Internal ID
/// %i Obj Internal ID
/// %Y Year (Obj)
///
/// ?_(...) replace _ with a value from above:
/// Everything inside the braces will only be added if the value of %_ is not null
/// </remarks>
/// <response code="200"></response>
/// <response code="500">Error during Database Operation</response>
[HttpPatch("ChapterNamingScheme")]
[ProducesResponseType(Status200OK)]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public IActionResult SetCustomNamingScheme([FromBody]string namingScheme)
{
try
{
Dictionary<Chapter, string> oldPaths = context.Chapters.ToDictionary(c => c, c => c.FullArchiveFilePath);
TrangaSettings.UpdateChapterNamingScheme(namingScheme);
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);
}
}
//TODO Move old Chapters
Tranga.Settings.SetChapterNamingScheme(namingScheme);
/// <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);
}
return Ok();
}
/// <summary>
@ -301,7 +234,7 @@ public class SettingsController(PgsqlContext context, ILog Log) : Controller
[ProducesResponseType(Status200OK)]
public IActionResult SetFlareSolverrUrl([FromBody]string flareSolverrUrl)
{
TrangaSettings.UpdateFlareSolverrUrl(flareSolverrUrl);
Tranga.Settings.SetFlareSolverrUrl(flareSolverrUrl);
return Ok();
}
@ -313,7 +246,7 @@ public class SettingsController(PgsqlContext context, ILog Log) : Controller
[ProducesResponseType(Status200OK)]
public IActionResult ClearFlareSolverrUrl()
{
TrangaSettings.UpdateFlareSolverrUrl(string.Empty);
Tranga.Settings.SetFlareSolverrUrl(string.Empty);
return Ok();
}
@ -332,4 +265,28 @@ public class SettingsController(PgsqlContext context, ILog Log) : Controller
RequestResult result = client.MakeRequestInternal(knownProtectedUrl);
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)
{
Log.Debug($"Requesting {requestType} {url}");
if (!TrangaSettings.requestLimits.ContainsKey(requestType))
if (!Tranga.Settings.RequestLimits.ContainsKey(requestType))
{
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.requestLimits[requestType];
: Tranga.Settings.RequestLimits[requestType];
TimeSpan timeBetweenRequests = TimeSpan.FromMinutes(1).Divide(rateLimit);
DateTime now = DateTime.Now;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,5 @@
// <auto-generated />
using System;
using API.Schema.Contexts;
using API.Schema.MangaContext;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
@ -9,11 +8,11 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace API.Migrations.pgsql
namespace API.Migrations.Manga
{
[DbContext(typeof(PgsqlContext))]
[Migration("20250630182650_OofV2.1")]
partial class OofV21
[DbContext(typeof(MangaContext))]
[Migration("20250703192023_Initial")]
partial class Initial
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
@ -25,7 +24,7 @@ namespace API.Migrations.pgsql
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("API.Schema.Author", b =>
modelBuilder.Entity("API.Schema.MangaContext.Author", b =>
{
b.Property<string>("Key")
.HasColumnType("text");
@ -40,7 +39,7 @@ namespace API.Migrations.pgsql
b.ToTable("Authors");
});
modelBuilder.Entity("API.Schema.Chapter", b =>
modelBuilder.Entity("API.Schema.MangaContext.Chapter", b =>
{
b.Property<string>("Key")
.HasColumnType("text");
@ -77,7 +76,7 @@ namespace API.Migrations.pgsql
b.ToTable("Chapters");
});
modelBuilder.Entity("API.Schema.FileLibrary", b =>
modelBuilder.Entity("API.Schema.MangaContext.FileLibrary", b =>
{
b.Property<string>("Key")
.HasColumnType("text");
@ -94,45 +93,10 @@ namespace API.Migrations.pgsql
b.HasKey("Key");
b.ToTable("LocalLibraries");
b.ToTable("FileLibraries");
});
modelBuilder.Entity("API.Schema.Jobs.Job", 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 =>
modelBuilder.Entity("API.Schema.MangaContext.Manga", b =>
{
b.Property<string>("Key")
.HasColumnType("text");
@ -184,7 +148,7 @@ namespace API.Migrations.pgsql
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")
.HasColumnType("text");
@ -204,6 +168,9 @@ namespace API.Migrations.pgsql
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<bool>("UseForDownload")
.HasColumnType("boolean");
b.Property<string>("WebsiteUrl")
.HasMaxLength(512)
.HasColumnType("character varying(512)");
@ -217,7 +184,7 @@ namespace API.Migrations.pgsql
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")
.HasColumnType("text");
@ -237,6 +204,9 @@ namespace API.Migrations.pgsql
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<bool>("UseForDownload")
.HasColumnType("boolean");
b.Property<string>("WebsiteUrl")
.HasMaxLength(512)
.HasColumnType("character varying(512)");
@ -250,7 +220,7 @@ namespace API.Migrations.pgsql
b.ToTable("MangaConnectorToManga");
});
modelBuilder.Entity("API.Schema.MangaConnectors.MangaConnector", b =>
modelBuilder.Entity("API.Schema.MangaContext.MangaConnectors.MangaConnector", b =>
{
b.Property<string>("Name")
.HasMaxLength(32)
@ -283,7 +253,7 @@ namespace API.Migrations.pgsql
b.UseTphMappingStrategy();
});
modelBuilder.Entity("API.Schema.MangaTag", b =>
modelBuilder.Entity("API.Schema.MangaContext.MangaTag", b =>
{
b.Property<string>("Tag")
.HasMaxLength(64)
@ -294,6 +264,44 @@ namespace API.Migrations.pgsql
b.ToTable("Tags");
});
modelBuilder.Entity("API.Schema.MangaContext.MetadataFetchers.MetadataEntry", b =>
{
b.Property<string>("MetadataFetcherName")
.HasColumnType("text");
b.Property<string>("Identifier")
.HasColumnType("text");
b.Property<string>("MangaId")
.IsRequired()
.HasColumnType("text");
b.HasKey("MetadataFetcherName", "Identifier");
b.HasIndex("MangaId");
b.ToTable("MetadataEntries");
});
modelBuilder.Entity("API.Schema.MangaContext.MetadataFetchers.MetadataFetcher", b =>
{
b.Property<string>("Name")
.HasColumnType("text");
b.Property<string>("MetadataEntry")
.IsRequired()
.HasMaxLength(21)
.HasColumnType("character varying(21)");
b.HasKey("Name");
b.ToTable("MetadataFetcher");
b.HasDiscriminator<string>("MetadataEntry").HasValue("MetadataFetcher");
b.UseTphMappingStrategy();
});
modelBuilder.Entity("AuthorToManga", b =>
{
b.Property<string>("AuthorIds")
@ -309,21 +317,6 @@ namespace API.Migrations.pgsql
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 =>
{
b.Property<string>("MangaTagIds")
@ -339,187 +332,37 @@ namespace API.Migrations.pgsql
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.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.HasBaseType("API.Schema.MangaContext.MangaConnectors.MangaConnector");
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");
});
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");
});
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")
.HasForeignKey("ParentMangaId")
.OnDelete(DeleteBehavior.Cascade)
@ -528,24 +371,14 @@ namespace API.Migrations.pgsql
b.Navigation("ParentManga");
});
modelBuilder.Entity("API.Schema.Jobs.Job", b =>
modelBuilder.Entity("API.Schema.MangaContext.Manga", 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.FileLibrary", "Library")
b.HasOne("API.Schema.MangaContext.FileLibrary", "Library")
.WithMany()
.HasForeignKey("LibraryId")
.OnDelete(DeleteBehavior.SetNull);
b.OwnsMany("API.Schema.AltTitle", "AltTitles", b1 =>
b.OwnsMany("API.Schema.MangaContext.AltTitle", "AltTitles", b1 =>
{
b1.Property<string>("Key")
.HasColumnType("text");
@ -574,7 +407,7 @@ namespace API.Migrations.pgsql
.HasForeignKey("MangaKey");
});
b.OwnsMany("API.Schema.Link", "Links", b1 =>
b.OwnsMany("API.Schema.MangaContext.Link", "Links", b1 =>
{
b1.Property<string>("Key")
.HasColumnType("text");
@ -610,15 +443,15 @@ namespace API.Migrations.pgsql
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()
.HasForeignKey("MangaConnectorName")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("API.Schema.Chapter", "Obj")
b.HasOne("API.Schema.MangaContext.Chapter", "Obj")
.WithMany("MangaConnectorIds")
.HasForeignKey("ObjId")
.OnDelete(DeleteBehavior.NoAction)
@ -629,15 +462,15 @@ namespace API.Migrations.pgsql
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()
.HasForeignKey("MangaConnectorName")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("API.Schema.Manga", "Obj")
b.HasOne("API.Schema.MangaContext.Manga", "Obj")
.WithMany("MangaConnectorIds")
.HasForeignKey("ObjId")
.OnDelete(DeleteBehavior.Cascade)
@ -648,142 +481,61 @@ namespace API.Migrations.pgsql
b.Navigation("Obj");
});
modelBuilder.Entity("API.Schema.MangaContext.MetadataFetchers.MetadataEntry", b =>
{
b.HasOne("API.Schema.MangaContext.Manga", "Manga")
.WithMany()
.HasForeignKey("MangaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("API.Schema.MangaContext.MetadataFetchers.MetadataFetcher", "MetadataFetcher")
.WithMany()
.HasForeignKey("MetadataFetcherName")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Manga");
b.Navigation("MetadataFetcher");
});
modelBuilder.Entity("AuthorToManga", b =>
{
b.HasOne("API.Schema.Author", null)
b.HasOne("API.Schema.MangaContext.Author", null)
.WithMany()
.HasForeignKey("AuthorIds")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("API.Schema.Manga", null)
b.HasOne("API.Schema.MangaContext.Manga", null)
.WithMany()
.HasForeignKey("MangaIds")
.OnDelete(DeleteBehavior.Cascade)
.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 =>
{
b.HasOne("API.Schema.Manga", null)
b.HasOne("API.Schema.MangaContext.Manga", null)
.WithMany()
.HasForeignKey("MangaIds")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("API.Schema.MangaTag", null)
b.HasOne("API.Schema.MangaContext.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.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 =>
modelBuilder.Entity("API.Schema.MangaContext.Chapter", b =>
{
b.Navigation("MangaConnectorIds");
});
modelBuilder.Entity("API.Schema.Manga", b =>
modelBuilder.Entity("API.Schema.MangaContext.Manga", b =>
{
b.Navigation("Chapters");

View File

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

View File

@ -1,6 +1,5 @@
// <auto-generated />
using System;
using API.Schema.Contexts;
using API.Schema.MangaContext;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
@ -8,10 +7,10 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace API.Migrations.pgsql
namespace API.Migrations.Manga
{
[DbContext(typeof(PgsqlContext))]
partial class PgsqlContextModelSnapshot : ModelSnapshot
[DbContext(typeof(MangaContext))]
partial class MangaContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
@ -22,7 +21,7 @@ namespace API.Migrations.pgsql
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("API.Schema.Author", b =>
modelBuilder.Entity("API.Schema.MangaContext.Author", b =>
{
b.Property<string>("Key")
.HasColumnType("text");
@ -37,7 +36,7 @@ namespace API.Migrations.pgsql
b.ToTable("Authors");
});
modelBuilder.Entity("API.Schema.Chapter", b =>
modelBuilder.Entity("API.Schema.MangaContext.Chapter", b =>
{
b.Property<string>("Key")
.HasColumnType("text");
@ -74,7 +73,7 @@ namespace API.Migrations.pgsql
b.ToTable("Chapters");
});
modelBuilder.Entity("API.Schema.FileLibrary", b =>
modelBuilder.Entity("API.Schema.MangaContext.FileLibrary", b =>
{
b.Property<string>("Key")
.HasColumnType("text");
@ -91,45 +90,10 @@ namespace API.Migrations.pgsql
b.HasKey("Key");
b.ToTable("LocalLibraries");
b.ToTable("FileLibraries");
});
modelBuilder.Entity("API.Schema.Jobs.Job", 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 =>
modelBuilder.Entity("API.Schema.MangaContext.Manga", b =>
{
b.Property<string>("Key")
.HasColumnType("text");
@ -181,7 +145,7 @@ namespace API.Migrations.pgsql
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")
.HasColumnType("text");
@ -201,6 +165,9 @@ namespace API.Migrations.pgsql
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<bool>("UseForDownload")
.HasColumnType("boolean");
b.Property<string>("WebsiteUrl")
.HasMaxLength(512)
.HasColumnType("character varying(512)");
@ -214,7 +181,7 @@ namespace API.Migrations.pgsql
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")
.HasColumnType("text");
@ -234,6 +201,9 @@ namespace API.Migrations.pgsql
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<bool>("UseForDownload")
.HasColumnType("boolean");
b.Property<string>("WebsiteUrl")
.HasMaxLength(512)
.HasColumnType("character varying(512)");
@ -247,7 +217,7 @@ namespace API.Migrations.pgsql
b.ToTable("MangaConnectorToManga");
});
modelBuilder.Entity("API.Schema.MangaConnectors.MangaConnector", b =>
modelBuilder.Entity("API.Schema.MangaContext.MangaConnectors.MangaConnector", b =>
{
b.Property<string>("Name")
.HasMaxLength(32)
@ -280,7 +250,7 @@ namespace API.Migrations.pgsql
b.UseTphMappingStrategy();
});
modelBuilder.Entity("API.Schema.MangaTag", b =>
modelBuilder.Entity("API.Schema.MangaContext.MangaTag", b =>
{
b.Property<string>("Tag")
.HasMaxLength(64)
@ -291,7 +261,7 @@ namespace API.Migrations.pgsql
b.ToTable("Tags");
});
modelBuilder.Entity("API.Schema.MetadataFetchers.MetadataEntry", b =>
modelBuilder.Entity("API.Schema.MangaContext.MetadataFetchers.MetadataEntry", b =>
{
b.Property<string>("MetadataFetcherName")
.HasColumnType("text");
@ -301,7 +271,7 @@ namespace API.Migrations.pgsql
b.Property<string>("MangaId")
.IsRequired()
.HasColumnType("character varying(64)");
.HasColumnType("text");
b.HasKey("MetadataFetcherName", "Identifier");
@ -310,9 +280,9 @@ namespace API.Migrations.pgsql
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");
b.Property<string>("MetadataEntry")
@ -320,7 +290,7 @@ namespace API.Migrations.pgsql
.HasMaxLength(21)
.HasColumnType("character varying(21)");
b.HasKey("MetadataFetcherName");
b.HasKey("Name");
b.ToTable("MetadataFetcher");
@ -344,21 +314,6 @@ namespace API.Migrations.pgsql
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 =>
{
b.Property<string>("MangaTagIds")
@ -374,194 +329,37 @@ namespace API.Migrations.pgsql
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.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.HasBaseType("API.Schema.MangaContext.MangaConnectors.MangaConnector");
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");
});
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");
});
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");
});
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")
.HasForeignKey("ParentMangaId")
.OnDelete(DeleteBehavior.Cascade)
@ -570,24 +368,14 @@ namespace API.Migrations.pgsql
b.Navigation("ParentManga");
});
modelBuilder.Entity("API.Schema.Jobs.Job", b =>
modelBuilder.Entity("API.Schema.MangaContext.Manga", 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.FileLibrary", "Library")
b.HasOne("API.Schema.MangaContext.FileLibrary", "Library")
.WithMany()
.HasForeignKey("LibraryId")
.OnDelete(DeleteBehavior.SetNull);
b.OwnsMany("API.Schema.AltTitle", "AltTitles", b1 =>
b.OwnsMany("API.Schema.MangaContext.AltTitle", "AltTitles", b1 =>
{
b1.Property<string>("Key")
.HasColumnType("text");
@ -616,7 +404,7 @@ namespace API.Migrations.pgsql
.HasForeignKey("MangaKey");
});
b.OwnsMany("API.Schema.Link", "Links", b1 =>
b.OwnsMany("API.Schema.MangaContext.Link", "Links", b1 =>
{
b1.Property<string>("Key")
.HasColumnType("text");
@ -652,15 +440,15 @@ namespace API.Migrations.pgsql
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()
.HasForeignKey("MangaConnectorName")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("API.Schema.Chapter", "Obj")
b.HasOne("API.Schema.MangaContext.Chapter", "Obj")
.WithMany("MangaConnectorIds")
.HasForeignKey("ObjId")
.OnDelete(DeleteBehavior.NoAction)
@ -671,15 +459,15 @@ namespace API.Migrations.pgsql
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()
.HasForeignKey("MangaConnectorName")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("API.Schema.Manga", "Obj")
b.HasOne("API.Schema.MangaContext.Manga", "Obj")
.WithMany("MangaConnectorIds")
.HasForeignKey("ObjId")
.OnDelete(DeleteBehavior.Cascade)
@ -690,15 +478,15 @@ namespace API.Migrations.pgsql
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()
.HasForeignKey("MangaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("API.Schema.MetadataFetchers.MetadataFetcher", "MetadataFetcher")
b.HasOne("API.Schema.MangaContext.MetadataFetchers.MetadataFetcher", "MetadataFetcher")
.WithMany()
.HasForeignKey("MetadataFetcherName")
.OnDelete(DeleteBehavior.Cascade)
@ -711,140 +499,40 @@ namespace API.Migrations.pgsql
modelBuilder.Entity("AuthorToManga", b =>
{
b.HasOne("API.Schema.Author", null)
b.HasOne("API.Schema.MangaContext.Author", null)
.WithMany()
.HasForeignKey("AuthorIds")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("API.Schema.Manga", null)
b.HasOne("API.Schema.MangaContext.Manga", null)
.WithMany()
.HasForeignKey("MangaIds")
.OnDelete(DeleteBehavior.Cascade)
.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 =>
{
b.HasOne("API.Schema.Manga", null)
b.HasOne("API.Schema.MangaContext.Manga", null)
.WithMany()
.HasForeignKey("MangaIds")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("API.Schema.MangaTag", null)
b.HasOne("API.Schema.MangaContext.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.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 =>
modelBuilder.Entity("API.Schema.MangaContext.Chapter", b =>
{
b.Navigation("MangaConnectorIds");
});
modelBuilder.Entity("API.Schema.Manga", b =>
modelBuilder.Entity("API.Schema.MangaContext.Manga", b =>
{
b.Navigation("Chapters");

View File

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

View File

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

View File

@ -1,7 +1,7 @@
// <auto-generated />
using System;
using System.Collections.Generic;
using API.Schema.Contexts;
using API.Schema.NotificationsContext;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
@ -9,7 +9,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace API.Migrations.notifications
namespace API.Migrations.Notifications
{
[DbContext(typeof(NotificationsContext))]
partial class NotificationsContextModelSnapshot : ModelSnapshot
@ -18,21 +18,23 @@ namespace API.Migrations.notifications
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "9.0.3")
.HasAnnotation("ProductVersion", "9.0.5")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "hstore");
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("API.Schema.Notification", b =>
modelBuilder.Entity("API.Schema.NotificationsContext.Notification", b =>
{
b.Property<string>("NotificationId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("Key")
.HasColumnType("text");
b.Property<DateTime>("Date")
.HasColumnType("timestamp with time zone");
b.Property<bool>("IsSent")
.HasColumnType("boolean");
b.Property<string>("Message")
.IsRequired()
.HasMaxLength(512)
@ -46,12 +48,12 @@ namespace API.Migrations.notifications
b.Property<byte>("Urgency")
.HasColumnType("smallint");
b.HasKey("NotificationId");
b.HasKey("Key");
b.ToTable("Notifications");
});
modelBuilder.Entity("API.Schema.NotificationConnectors.NotificationConnector", b =>
modelBuilder.Entity("API.Schema.NotificationsContext.NotificationConnectors.NotificationConnector", b =>
{
b.Property<string>("Name")
.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 API;
using API.Controllers;
using API.Schema;
using API.Schema.Contexts;
using API.Schema.Jobs;
using API.Schema.MangaConnectors;
using API.Schema.LibraryContext;
using API.Schema.MangaContext;
using API.Schema.MangaContext.MangaConnectors;
using API.Schema.NotificationsContext;
using Asp.Versioning;
using Asp.Versioning.Builder;
using Asp.Versioning.Conventions;
@ -56,17 +55,17 @@ builder.Services.AddSwaggerGen(opt =>
});
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"}; " +
$"Username={Environment.GetEnvironmentVariable("POSTGRES_USER") ?? "postgres"}; " +
$"Password={Environment.GetEnvironmentVariable("POSTGRES_PASSWORD") ?? "postgres"}";
builder.Services.AddDbContext<PgsqlContext>(options =>
options.UseNpgsql(ConnectionString));
builder.Services.AddDbContext<MangaContext>(options =>
options.UseNpgsql(connectionString));
builder.Services.AddDbContext<NotificationsContext>(options =>
options.UseNpgsql(ConnectionString));
options.UseNpgsql(connectionString));
builder.Services.AddDbContext<LibraryContext>(options =>
options.UseNpgsql(ConnectionString));
options.UseNpgsql(connectionString));
builder.Services.AddControllers(options =>
{
@ -98,8 +97,7 @@ app.MapControllers()
app.UseSwagger();
app.UseSwaggerUI(options =>
{
options.SwaggerEndpoint(
$"/swagger/v2/swagger.json", "v2");
options.SwaggerEndpoint($"/swagger/v2/swagger.json", "v2");
});
app.UseHttpsRedirection();
@ -108,52 +106,21 @@ app.UseMiddleware<RequestTimeMiddleware>();
using (IServiceScope scope = app.Services.CreateScope())
{
PgsqlContext context = scope.ServiceProvider.GetRequiredService<PgsqlContext>();
//TODO Remove after migrations complete
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");
MangaContext context = scope.ServiceProvider.GetRequiredService<MangaContext>();
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 =
[
new MangaDex(),
new ComickIo(),
new Global(scope.ServiceProvider.GetService<PgsqlContext>()!)
new Global(scope.ServiceProvider.GetService<MangaContext>()!)
];
MangaConnector[] newConnectors = connectors.Where(c => !context.MangaConnectors.Contains(c)).ToArray();
context.MangaConnectors.AddRange(newConnectors);
if (!context.LocalLibraries.Any())
context.LocalLibraries.Add(new FileLibrary(TrangaSettings.downloadLocation, "Default FileLibrary"));
if (!context.FileLibraries.Any())
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())
@ -161,23 +128,24 @@ using (IServiceScope scope = app.Services.CreateScope())
NotificationsContext context = scope.ServiceProvider.GetRequiredService<NotificationsContext>();
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=)"};
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())
{
PgsqlContext context = scope.ServiceProvider.GetRequiredService<PgsqlContext>();
Tranga.RemoveStaleFiles(context);
LibraryContext context = scope.ServiceProvider.GetRequiredService<LibraryContext>();
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");

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;
[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;
}

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.Nodes;
namespace API.Schema.LibraryConnectors;
namespace API.Schema.LibraryContext.LibraryConnectors;
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.Nodes;
namespace API.Schema.LibraryConnectors;
namespace API.Schema.LibraryContext.LibraryConnectors;
public class Komga : LibraryConnector
{
public Komga(string baseUrl, string auth) : base(TokenGen.CreateToken(typeof(Komga), baseUrl), LibraryType.Komga,
baseUrl, auth)
public Komga(string baseUrl, string auth) : base(LibraryType.Komga, 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 log4net;
namespace API.Schema.LibraryConnectors;
namespace API.Schema.LibraryContext.LibraryConnectors;
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 Microsoft.EntityFrameworkCore;
namespace API.Schema;
namespace API.Schema.MangaContext;
[PrimaryKey("Key")]
public class AltTitle(string language, string title) : Identifiable(TokenGen.CreateToken("AltTitle"))

View File

@ -1,7 +1,7 @@
using System.ComponentModel.DataAnnotations;
using Microsoft.EntityFrameworkCore;
namespace API.Schema;
namespace API.Schema.MangaContext;
[PrimaryKey("Key")]
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 Newtonsoft.Json;
namespace API.Schema;
namespace API.Schema.MangaContext;
[PrimaryKey("Key")]
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 string GetArchiveFilePath()
{
string archiveNamingScheme = TrangaSettings.chapterNamingScheme;
string archiveNamingScheme = Tranga.Settings.ChapterNamingScheme;
StringBuilder stringBuilder = new();
foreach (Match nullable in NullableRex.Matches(archiveNamingScheme))
{

View File

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

View File

@ -1,7 +1,7 @@
using System.ComponentModel.DataAnnotations;
using Microsoft.EntityFrameworkCore;
namespace API.Schema;
namespace API.Schema.MangaContext;
[PrimaryKey("Key")]
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.Runtime.InteropServices;
using System.Text;
using API.Schema.Contexts;
using API.Schema.Jobs;
using API.Workers;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Newtonsoft.Json;
using static System.IO.UnixFileMode;
namespace API.Schema;
namespace API.Schema.MangaContext;
[PrimaryKey("Key")]
public class Manga : Identifiable
@ -157,17 +156,15 @@ public class Manga : Identifiable
}
/// <summary>
///
/// Merges another Manga (MangaConnectorIds and Chapters)
/// </summary>
/// <param name="other"></param>
/// <param name="context"></param>
/// <exception cref="DbUpdateException"></exception>
public void MergeFrom(Manga other, PgsqlContext context)
{
try
/// <param name="other">The other <see cref="Manga" /> to merge</param>
/// <param name="context"><see cref="MangaContext"/> to use for Database operations</param>
/// <returns>An array of <see cref="MoveFileOrFolderWorker"/> for moving <see cref="Chapter"/> to new Directory</returns>
public BaseWorker[] MergeFrom(Manga other, MangaContext context)
{
context.Mangas.Remove(other);
List<Job> newJobs = new();
List<BaseWorker> newJobs = new();
this.MangaConnectorIds = this.MangaConnectorIds
.UnionBy(other.MangaConnectorIds, id => id.MangaConnectorName)
@ -180,19 +177,10 @@ public class Manga : Identifiable
otherChapter.Title);
this.Chapters.Add(newChapter);
string newPath = newChapter.FullArchiveFilePath;
newJobs.Add(new MoveFileOrFolderJob(oldPath, newPath));
newJobs.Add(new MoveFileOrFolderWorker(newPath, oldPath));
}
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}";

View File

@ -1,10 +1,10 @@
using System.ComponentModel.DataAnnotations;
using API.Schema.MangaConnectors;
using API.Schema.MangaContext.MangaConnectors;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Newtonsoft.Json;
namespace API.Schema;
namespace API.Schema.MangaContext;
[PrimaryKey("Key")]
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; }
[Url] [StringLength(512)] public string? WebsiteUrl { get; internal init; }
public bool UseForDownload { get; internal set; }
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))
{
this.Obj = obj;
this.MangaConnector = mangaConnector;
this.IdOnConnectorSite = idOnConnectorSite;
this.WebsiteUrl = websiteUrl;
this.UseForDownload = useForDownload;
}
/// <summary>
/// EF CORE ONLY!!!
/// </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)
{
this._lazyLoader = lazyLoader;
@ -61,6 +63,7 @@ public class MangaConnectorId<T> : Identifiable where T : Identifiable
this.MangaConnectorName = mangaConnectorName;
this.IdOnConnectorSite = idOnConnectorSite;
this.WebsiteUrl = websiteUrl;
this.UseForDownload = useForDownload;
}
public override string ToString() => $"{base.ToString()} {_obj}";

View File

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

View File

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

View File

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

View File

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

View File

@ -1,124 +1,23 @@
using API.Schema.Jobs;
using API.Schema.MangaConnectors;
using API.Schema.MetadataFetchers;
using log4net;
using API.Schema.MangaContext.MangaConnectors;
using API.Schema.MangaContext.MetadataFetchers;
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<Manga> Mangas { get; set; }
public DbSet<FileLibrary> LocalLibraries { get; set; }
public DbSet<FileLibrary> FileLibraries { get; set; }
public DbSet<Chapter> Chapters { get; set; }
public DbSet<Author> Authors { get; set; }
public DbSet<MangaTag> Tags { get; set; }
public DbSet<MangaConnectorId<Manga>> MangaConnectorToManga { get; set; }
public DbSet<MangaConnectorId<Chapter>> MangaConnectorToChapter { 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)
{
//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
modelBuilder.Entity<MangaConnector>()
.HasDiscriminator(c => c.Name)

View File

@ -1,7 +1,7 @@
using System.ComponentModel.DataAnnotations;
using Microsoft.EntityFrameworkCore;
namespace API.Schema;
namespace API.Schema.MangaContext;
[PrimaryKey("Tag")]
public class MangaTag(string tag)
@ -10,8 +10,5 @@ public class MangaTag(string tag)
[Required]
public string Tag { get; init; } = tag;
public override string ToString()
{
return $"{Tag}";
}
public override string ToString() => Tag;
}

View File

@ -1,7 +1,7 @@
using Microsoft.EntityFrameworkCore;
using Newtonsoft.Json;
namespace API.Schema.MetadataFetchers;
namespace API.Schema.MangaContext.MetadataFetchers;
[PrimaryKey("MetadataFetcherName", "Identifier")]
public class MetadataEntry
@ -19,7 +19,7 @@ public class MetadataEntry
this.Manga = manga;
this.MangaId = manga.Key;
this.MetadataFetcher = fetcher;
this.MetadataFetcherName = fetcher.MetadataFetcherName;
this.MetadataFetcherName = fetcher.Name;
this.Identifier = identifier;
}

View File

@ -1,26 +1,24 @@
using System.Diagnostics.CodeAnalysis;
using API.Schema.Contexts;
using Microsoft.EntityFrameworkCore;
namespace API.Schema.MetadataFetchers;
namespace API.Schema.MangaContext.MetadataFetchers;
[PrimaryKey("MetadataFetcherName")]
[PrimaryKey("Name")]
public abstract class MetadataFetcher
{
// ReSharper disable once EntityFramework.ModelValidation.UnlimitedStringLength
public string MetadataFetcherName { get; init; }
public string Name { get; init; }
protected MetadataFetcher()
{
this.MetadataFetcherName = this.GetType().Name;
this.Name = this.GetType().Name;
}
/// <summary>
/// EFCORE ONLY!!!
/// </summary>
internal MetadataFetcher(string metadataFetcherName)
internal MetadataFetcher(string name)
{
this.MetadataFetcherName = metadataFetcherName;
this.Name = name;
}
internal MetadataEntry CreateMetadataEntry(Manga manga, string identifier) =>
@ -33,5 +31,5 @@ public abstract class MetadataFetcher
/// <summary>
/// Updates the Manga linked in the MetadataEntry
/// </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);

View File

@ -1,9 +1,8 @@
using System.Text.RegularExpressions;
using API.Schema.Contexts;
using JikanDotNet;
using Microsoft.EntityFrameworkCore;
namespace API.Schema.MetadataFetchers;
namespace API.Schema.MangaContext.MetadataFetchers;
public class MyAnimeList : MetadataFetcher
{
@ -45,7 +44,7 @@ public class MyAnimeList : MetadataFetcher
/// <param name="dbContext"></param>
/// <exception cref="FormatException"></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)!;
MangaFull resultData;
@ -68,7 +67,7 @@ public class MyAnimeList : MetadataFetcher
dbManga.Authors.Clear();
dbManga.Authors = resultData.Authors.Select(a => new Author(a.Name)).ToList();
dbContext.SaveChanges();
dbContext.Sync();
}
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 Microsoft.EntityFrameworkCore;
namespace API.Schema;
namespace API.Schema.NotificationsContext;
[PrimaryKey("NotificationId")]
public class Notification
[PrimaryKey(nameof(Key))]
public class Notification : Identifiable
{
[StringLength(64)]
[Required]
public string NotificationId { get; init; }
[Required]
public NotificationUrgency Urgency { get; init; }
@ -24,29 +20,37 @@ public class Notification
[Required]
public DateTime Date { get; init; }
public bool IsSent { get; internal set; }
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.Message = message;
this.Urgency = urgency;
this.Date = date ?? DateTime.UtcNow;
this.IsSent = false;
}
/// <summary>
/// EF ONLY!!!
/// </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.Message = message;
this.Urgency = urgency;
this.Date = date;
this.IsSent = isSent;
}
public override string ToString()
public override string ToString() => $"{base.ToString()} {Urgency} {Title} {Message}";
}
public enum NotificationUrgency : byte
{
return $"{NotificationId} {Urgency} {Title}";
}
Low = 1,
Normal = 3,
High = 5
}

View File

@ -5,7 +5,7 @@ using log4net;
using Microsoft.EntityFrameworkCore;
using Newtonsoft.Json;
namespace API.Schema.NotificationConnectors;
namespace API.Schema.NotificationsContext.NotificationConnectors;
[PrimaryKey("Name")]
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]
private readonly HttpClient Client = new()
{
DefaultRequestHeaders = { { "User-Agent", TrangaSettings.userAgent } }
DefaultRequestHeaders = { { "User-Agent", Tranga.Settings.UserAgent } }
};
[JsonIgnore]
@ -79,4 +79,6 @@ public class NotificationConnector(string name, string url, Dictionary<string, s
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 API.Schema.Contexts;
using API.Schema.Jobs;
using API.Schema.MangaConnectors;
using API.Schema.MetadataFetchers;
using API.Schema.NotificationConnectors;
using System.Diagnostics.CodeAnalysis;
using API.Schema.LibraryContext;
using API.Schema.MangaContext;
using API.Schema.MangaContext.MetadataFetchers;
using API.Schema.NotificationsContext;
using API.Workers;
using API.Workers.MaintenanceWorkers;
using log4net;
using log4net.Config;
using Microsoft.EntityFrameworkCore;
namespace API;
@ -22,99 +22,61 @@ public static class Tranga
" |___| |__| |___._||__|__||___ ||___._|\n" +
" |_____| \n\n";
public static Thread NotificationSenderThread { get; } = new (NotificationSender);
public static Thread JobStarterThread { get; } = new (JobStarter);
public static Thread PeriodicWorkerStarterThread { get; } = new (WorkerStarter);
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()
{
BasicConfigurator.Configure();
Log.Info("Logger Configured.");
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...");
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)
foreach (BaseWorker baseWorker in workers)
{
Log.Info($"Deleting {path}");
File.Delete(path);
AddWorker(baseWorker);
}
}
private static void NotificationSender(object? serviceProviderObj)
public static void RemoveWorker(BaseWorker removeWorker)
{
if (serviceProviderObj is null)
{
Log.Error("serviceProviderObj is null");
return;
}
IServiceProvider serviceProvider = (IServiceProvider)serviceProviderObj!;
using IServiceScope scope = serviceProvider.CreateScope();
NotificationsContext context = scope.ServiceProvider.GetRequiredService<NotificationsContext>();
IEnumerable<BaseWorker> baseWorkers = AllWorkers.Where(w => w.DependenciesAndSelf.Any(worker => worker == removeWorker));
try
foreach (BaseWorker worker in baseWorkers)
{
//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)
{
SendNotifications(serviceProvider, NotificationUrgency.High);
SendNotifications(serviceProvider, NotificationUrgency.Normal);
SendNotifications(serviceProvider, NotificationUrgency.Low);
Thread.Sleep(2000);
StopWorker(worker);
AllWorkers.Remove(worker);
}
}
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}");
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.");
Log.Info("WorkerStarter Thread running.");
if (serviceProviderObj is null)
{
Log.Error("serviceProviderObj is null");
@ -124,170 +86,117 @@ public static class Tranga
while (true)
{
Log.Debug("Starting Job-Cycle...");
DateTime cycleStart = DateTime.UtcNow;
using IServiceScope scope = serviceProvider.CreateScope();
PgsqlContext cycleContext = scope.ServiceProvider.GetRequiredService<PgsqlContext>();
CheckRunningWorkers();
//Get Running Jobs
List<Job> runningJobs = cycleContext.Jobs.GetRunningJobs();
foreach (BaseWorker baseWorker in AllWorkers.DueWorkers())
StartWorkers.Add(baseWorker);
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)
foreach (BaseWorker worker in StartWorkers.ToArray())
{
bool running = false;
Thread t = new(() =>
if(RunningWorkers.ContainsKey(worker))
continue;
if (worker is BaseWorkerWithContext<MangaContext> mangaContextWorker)
{
using IServiceScope jobScope = serviceProvider.CreateScope();
PgsqlContext jobContext = jobScope.ServiceProvider.GetRequiredService<PgsqlContext>();
if (jobContext.Jobs.Find(job.Key) is not { } inContext)
mangaContextWorker.SetScope(serviceProvider.CreateScope());
RunningWorkers.Add(mangaContextWorker, mangaContextWorker.DoWork());
}else if (worker is BaseWorkerWithContext<NotificationsContext> notificationContextWorker)
{
notificationContextWorker.SetScope(serviceProvider.CreateScope());
RunningWorkers.Add(notificationContextWorker, notificationContextWorker.DoWork());
}else if (worker is BaseWorkerWithContext<LibraryContext> libraryContextWorker)
{
libraryContextWorker.SetScope(serviceProvider.CreateScope());
RunningWorkers.Add(libraryContextWorker, libraryContextWorker.DoWork());
}else
RunningWorkers.Add(worker, worker.DoWork());
StartWorkers.Remove(worker);
}
Thread.Sleep(Settings.WorkCycleTimeoutMs);
}
}
private static void CheckRunningWorkers()
{
KeyValuePair<BaseWorker, Task<BaseWorker[]>>[] done = RunningWorkers.Where(kv => kv.Value.IsCompleted).ToArray();
if (done.Length < 1)
return;
inContext.Run(jobContext, ref running); //FIND the job IN THE NEW CONTEXT!!!!!!! SO WE DON'T GET TRACKING PROBLEMS AND AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
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;
});
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
internal static void MarkWorkerForStart(BaseWorker worker) => StartWorkers.Add(worker);
internal static void StopWorker(BaseWorker worker)
{
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);
}
StartWorkers.Remove(worker);
worker.Cancel();
RunningWorkers.Remove(worker);
}
private static List<Job> GetRunningJobs(this IQueryable<Job> jobs)
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)
{
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;
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;
}
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;
}
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;
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;
}
if(context.MangaConnectorToChapter.Find(chId.Key) is null)
context.MangaConnectorToChapter.Add(chId);
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
if (running.TryGetValue(connector, out Dictionary<JobType, List<Job>>? jobTypeJobsRunning))
{
//MangaConnector has running Jobs
//Match per JobType (MangaConnector can have 1 Job per Type running at the same time)
foreach ((JobType jobType, List<Job> jobsWaiting) in jobTypeJobsWaiting)
{
if(jobTypeJobsRunning.ContainsKey(jobType))
//Already a job of Type running on MangaConnector
continue;
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))
//Already a job of type to be started
continue;
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());
}
}
}
DateTime end = DateTime.UtcNow;
Log.Debug($"Getting eligible jobs (not held back by Connector) took {end.Subtract(start).TotalMilliseconds}ms");
return ret;
if (context.Sync() is { success: false })
return false;
return true;
}
}

View File

@ -1,21 +1,25 @@
using System.Runtime.InteropServices;
using API.MangaDownloadClients;
using API.Schema;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
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]
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 static int compression{ get; private set; } = 40;
public static bool bwImages { get; private set; } = false;
public static string flareSolverrUrl { get; private set; } = string.Empty;
public string UserAgent { get; set; } = DefaultUserAgent;
public int ImageCompression{ get; set; } = 40;
public bool BlackWhiteImages { get; set; } = false;
public string FlareSolverrUrl { get; set; } = string.Empty;
/// <summary>
/// Placeholders:
/// %M Obj Name
@ -30,13 +34,8 @@ public static class TrangaSettings
/// ?_(...) replace _ with a value from above:
/// Everything inside the braces will only be added if the value of %_ is not null
/// </summary>
public static string chapterNamingScheme { get; private set; } = "%M - ?V(Vol.%V )Ch.%C?T( - %T)";
[JsonIgnore]
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;
public string ChapterNamingScheme { get; set; } = "%M - ?V(Vol.%V )Ch.%C?T( - %T)";
public int WorkCycleTimeoutMs { get; set; } = 20000;
[JsonIgnore]
internal static readonly Dictionary<RequestType, int> DefaultRequestLimits = new ()
{
@ -47,142 +46,67 @@ public static class TrangaSettings
{RequestType.MangaCover, 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,
NotificationUrgency.Normal => TimeSpan.FromMinutes(5),
NotificationUrgency.Low => TimeSpan.FromMinutes(10),
_ => 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();
if (!File.Exists(settingsFilePath))
new TrangaSettings().Save();
return JsonConvert.DeserializeObject<TrangaSettings>(File.ReadAllText(settingsFilePath));
}
public static void UpdateAprilFoolsMode(bool enabled)
public void Save()
{
aprilFoolsMode = enabled;
ExportSettings();
File.WriteAllText(settingsFilePath, JsonConvert.SerializeObject(this));
}
public static void UpdateCompressImages(int value)
public void SetUserAgent(string value)
{
compression = int.Clamp(value, 1, 100);
ExportSettings();
this.UserAgent = value;
Save();
}
public static void UpdateBwImages(bool enabled)
public void SetRequestLimit(RequestType type, int value)
{
bwImages = enabled;
ExportSettings();
this.RequestLimits[type] = value;
Save();
}
public static void UpdateUserAgent(string? customUserAgent)
public void ResetRequestLimits()
{
userAgent = customUserAgent ?? DefaultUserAgent;
ExportSettings();
this.RequestLimits = DefaultRequestLimits;
Save();
}
public static void UpdateRequestLimit(RequestType requestType, int newLimit)
public void UpdateImageCompression(int value)
{
requestLimits[requestType] = newLimit;
ExportSettings();
this.ImageCompression = value;
Save();
}
public static void UpdateChapterNamingScheme(string namingScheme)
public void SetBlackWhiteImageEnabled(bool enabled)
{
chapterNamingScheme = namingScheme;
ExportSettings();
this.BlackWhiteImages = enabled;
Save();
}
public static void UpdateFlareSolverrUrl(string url)
public void SetChapterNamingScheme(string scheme)
{
flareSolverrUrl = url;
ExportSettings();
this.ChapterNamingScheme = scheme;
Save();
}
public static void ResetRequestLimits()
public void SetFlareSolverrUrl(string url)
{
requestLimits = DefaultRequestLimits;
ExportSettings();
this.FlareSolverrUrl = url;
Save();
}
public static void ExportSettings()
public void SetDownloadLanguage(string language)
{
if (File.Exists(settingsFilePath))
{
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>()!;
this.DownloadLanguage = language;
Save();
}
}

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.Runtime.InteropServices;
using API.MangaDownloadClients;
using API.Schema.Contexts;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Newtonsoft.Json;
using API.Schema.MangaContext;
using API.Schema.MangaContext.MangaConnectors;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Formats.Jpeg;
using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp.Processing.Processors.Binarization;
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!;
private Chapter? _chapter;
[JsonIgnore]
public Chapter Chapter
internal readonly string MangaConnectorIdId = chId.Key;
protected override BaseWorker[] DoWorkInternal()
{
get => LazyLoader.Load(this, ref _chapter) ?? throw new InvalidOperationException();
init
{
ChapterId = value.Key;
_chapter = value;
}
}
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)
if (DbContext.MangaConnectorToChapter.Find(MangaConnectorIdId) is not { } MangaConnectorId)
return []; //TODO Exception?
MangaConnector mangaConnector = MangaConnectorId.MangaConnector;
Chapter chapter = MangaConnectorId.Obj;
if (chapter.Downloaded)
{
Log.Info("Chapter was already downloaded.");
return [];
}
//TODO MangaConnector Selection
MangaConnectorId<Chapter> mcId = Chapter.MangaConnectorIds.First();
string[] imageUrls = mcId.MangaConnector.GetChapterImageUrls(mcId);
string[] imageUrls = mangaConnector.GetChapterImageUrls(MangaConnectorId);
if (imageUrls.Length < 1)
{
Log.Info($"No imageUrls for chapter {Chapter}");
Log.Info($"No imageUrls for chapter {chapter}");
return [];
}
string saveArchiveFilePath = Chapter.FullArchiveFilePath;
string saveArchiveFilePath = chapter.FullArchiveFilePath;
Log.Debug($"Chapter path: {saveArchiveFilePath}");
//Check if Publication Directory already exists
@ -69,7 +41,7 @@ public class DownloadSingleChapterJob : JobWithDownloading
if (directoryPath is null)
{
Log.Error($"Directory path could not be found: {saveArchiveFilePath}");
this.state = JobState.Failed;
this.Fail();
return [];
}
if (!Directory.Exists(directoryPath))
@ -92,7 +64,7 @@ public class DownloadSingleChapterJob : JobWithDownloading
string tempFolder = Directory.CreateTempSubdirectory("trangatemp").FullName;
Log.Debug($"Created temp folder: {tempFolder}");
Log.Info($"Downloading images: {Chapter}");
Log.Info($"Downloading images: {chapter}");
int chapterNum = 0;
//Download all Images to temporary Folder
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}");
File.WriteAllText(Path.Join(tempFolder, "ComicInfo.xml"), Chapter.GetComicInfoXmlString());
Log.Debug($"Creating ComicInfo.xml {chapter}");
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
ZipFile.CreateFromDirectory(tempFolder, saveArchiveFilePath);
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
File.SetUnixFileMode(saveArchiveFilePath, UserRead | UserWrite | UserExecute | GroupRead | GroupWrite | GroupExecute | OtherRead | OtherExecute);
Directory.Delete(tempFolder, true); //Cleanup
Chapter.Downloaded = true;
context.SaveChanges();
chapter.Downloaded = true;
DbContext.Sync();
if (context.Jobs.ToList().Any(j =>
{
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)
{
if (!TrangaSettings.bwImages && TrangaSettings.compression == 100)
if (!Tranga.Settings.BlackWhiteImages && Tranga.Settings.ImageCompression == 100)
{
Log.Debug("No processing requested for image");
return;
@ -147,12 +110,12 @@ public class DownloadSingleChapterJob : JobWithDownloading
try
{
using Image image = Image.Load(imagePath);
if (TrangaSettings.bwImages)
if (Tranga.Settings.BlackWhiteImages)
image.Mutate(i => i.ApplyProcessor(new AdaptiveThresholdProcessor()));
File.Delete(imagePath);
image.SaveAsJpeg(imagePath, new JpegEncoder()
{
Quality = TrangaSettings.compression
Quality = Tranga.Settings.ImageCompression
});
}
catch (Exception e)
@ -217,4 +180,6 @@ public class DownloadSingleChapterJob : JobWithDownloading
ProcessImage(savePath);
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