Compare commits

...

6 Commits

Author SHA1 Message Date
94678e744f Replace occurences of "ID" with more meaningful names in documentation 2025-03-08 13:26:07 +01:00
73eb02e7cb Fix Route for new Endpoint
Adjust Routes for Search
2025-03-08 13:20:28 +01:00
c3ebd6acac Database Migrations 2025-03-08 13:10:39 +01:00
a694b9f5ab Add endpoint to add Manga by URL 2025-03-08 13:09:19 +01:00
b24d2e12fc Provide Method to validate URL for GetMangaFromUrl 2025-03-08 12:54:29 +01:00
6909c367e5 Types return only Ids of related Entities 2025-03-08 12:54:07 +01:00
18 changed files with 930 additions and 121 deletions

View File

@ -40,41 +40,41 @@ public class JobController(PgsqlContext context) : Controller
/// <summary>
/// Get all Jobs in requested State
/// </summary>
/// <param name="state">Requested Job-State</param>
/// <param name="JobState">Requested Job-State</param>
/// <response code="200"></response>
[HttpGet("State/{state}")]
[HttpGet("State/{JobState}")]
[ProducesResponseType<Job[]>(Status200OK, "application/json")]
public IActionResult GetJobsInState(JobState state)
public IActionResult GetJobsInState(JobState JobState)
{
Job[] jobsInState = context.Jobs.Where(job => job.state == state).ToArray();
Job[] jobsInState = context.Jobs.Where(job => job.state == JobState).ToArray();
return Ok(jobsInState);
}
/// <summary>
/// Returns all Jobs of requested Type
/// </summary>
/// <param name="type">Requested Job-Type</param>
/// <param name="JobType">Requested Job-Type</param>
/// <response code="200"></response>
[HttpGet("Type/{type}")]
[HttpGet("Type/{JobType}")]
[ProducesResponseType<Job[]>(Status200OK, "application/json")]
public IActionResult GetJobsOfType(JobType type)
public IActionResult GetJobsOfType(JobType JobType)
{
Job[] jobsOfType = context.Jobs.Where(job => job.JobType == type).ToArray();
Job[] jobsOfType = context.Jobs.Where(job => job.JobType == JobType).ToArray();
return Ok(jobsOfType);
}
/// <summary>
/// Return Job with ID
/// </summary>
/// <param name="id">Job-ID</param>
/// <param name="JobId">Job-ID</param>
/// <response code="200"></response>
/// <response code="404">Job with ID could not be found</response>
[HttpGet("{id}")]
[HttpGet("{JobId}")]
[ProducesResponseType<Job>(Status200OK, "application/json")]
[ProducesResponseType(Status404NotFound)]
public IActionResult GetJob(string id)
public IActionResult GetJob(string JobId)
{
Job? ret = context.Jobs.Find(id);
Job? ret = context.Jobs.Find(JobId);
return (ret is not null) switch
{
true => Ok(ret),
@ -85,46 +85,46 @@ public class JobController(PgsqlContext context) : Controller
/// <summary>
/// Create a new CreateNewDownloadChapterJob
/// </summary>
/// <param name="mangaId">ID of Manga</param>
/// <param name="MangaId">ID of Manga</param>
/// <param name="recurrenceTime">How often should we check for new chapters</param>
/// <response code="201">Created new Job</response>
/// <response code="500">Error during Database Operation</response>
[HttpPut("NewDownloadChapterJob/{mangaId}")]
[HttpPut("NewDownloadChapterJob/{MangaId}")]
[ProducesResponseType(Status201Created)]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public IActionResult CreateNewDownloadChapterJob(string mangaId, [FromBody]ulong recurrenceTime)
public IActionResult CreateNewDownloadChapterJob(string MangaId, [FromBody]ulong recurrenceTime)
{
Job job = new DownloadNewChaptersJob(recurrenceTime, mangaId);
Job job = new DownloadNewChaptersJob(recurrenceTime, MangaId);
return AddJob(job);
}
/// <summary>
/// Create a new DownloadSingleChapterJob
/// </summary>
/// <param name="chapterId">ID of the Chapter</param>
/// <param name="ChapterId">ID of the Chapter</param>
/// <response code="201">Created new Job</response>
/// <response code="500">Error during Database Operation</response>
[HttpPut("DownloadSingleChapterJob/{chapterId}")]
[HttpPut("DownloadSingleChapterJob/{ChapterId}")]
[ProducesResponseType(Status201Created)]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public IActionResult CreateNewDownloadChapterJob(string chapterId)
public IActionResult CreateNewDownloadChapterJob(string ChapterId)
{
Job job = new DownloadSingleChapterJob(chapterId);
Job job = new DownloadSingleChapterJob(ChapterId);
return AddJob(job);
}
/// <summary>
/// Create a new UpdateMetadataJob
/// </summary>
/// <param name="mangaId">ID of the Manga</param>
/// <param name="MangaId">ID of the Manga</param>
/// <response code="201">Created new Job</response>
/// <response code="500">Error during Database Operation</response>
[HttpPut("UpdateMetadataJob/{mangaId}")]
[HttpPut("UpdateMetadataJob/{MangaId}")]
[ProducesResponseType(Status201Created)]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public IActionResult CreateUpdateMetadataJob(string mangaId)
public IActionResult CreateUpdateMetadataJob(string MangaId)
{
Job job = new UpdateMetadataJob(0, mangaId);
Job job = new UpdateMetadataJob(0, MangaId);
return AddJob(job);
}
@ -169,22 +169,22 @@ public class JobController(PgsqlContext context) : Controller
/// <summary>
/// Delete Job with ID and all children
/// </summary>
/// <param name="id">Job-ID</param>
/// <param name="JobId">Job-ID</param>
/// <response code="200">Job(s) deleted</response>
/// <response code="404">Job could not be found</response>
/// <response code="500">Error during Database Operation</response>
[HttpDelete("{id}")]
[HttpDelete("{JobId}")]
[ProducesResponseType<string[]>(Status200OK, "application/json")]
[ProducesResponseType(Status404NotFound)]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public IActionResult DeleteJob(string id)
public IActionResult DeleteJob(string JobId)
{
try
{
Job? ret = context.Jobs.Find(id);
Job? ret = context.Jobs.Find(JobId);
if(ret is null)
return NotFound();
IQueryable<Job> children = GetChildJobs(id);
IQueryable<Job> children = GetChildJobs(JobId);
context.RemoveRange(children);
context.Remove(ret);
@ -209,22 +209,22 @@ public class JobController(PgsqlContext context) : Controller
/// <summary>
/// Modify Job with ID
/// </summary>
/// <param name="id">Job-ID</param>
/// <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("{id}/")]
[HttpPatch("{JobId}")]
[ProducesResponseType<Job>(Status202Accepted, "application/json")]
[ProducesResponseType(Status400BadRequest)]
[ProducesResponseType(Status404NotFound)]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public IActionResult ModifyJob(string id, [FromBody]ModifyJobRecord modifyJobRecord)
public IActionResult ModifyJob(string JobId, [FromBody]ModifyJobRecord modifyJobRecord)
{
try
{
Job? ret = context.Jobs.Find(id);
Job? ret = context.Jobs.Find(JobId);
if(ret is null)
return NotFound();
@ -243,19 +243,19 @@ public class JobController(PgsqlContext context) : Controller
/// <summary>
/// Starts the Job with the requested ID
/// </summary>
/// <param name="id">Job-ID</param>
/// <param name="JobId">Job-ID</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("{id}/Start")]
[HttpPost("{JobId}/Start")]
[ProducesResponseType(Status202Accepted)]
[ProducesResponseType(Status404NotFound)]
[ProducesResponseType(Status409Conflict)]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public IActionResult StartJob(string id)
public IActionResult StartJob(string JobId)
{
Job? ret = context.Jobs.Find(id);
Job? ret = context.Jobs.Find(JobId);
if (ret is null)
return NotFound();
try
@ -275,10 +275,10 @@ public class JobController(PgsqlContext context) : Controller
/// <summary>
/// Stops the Job with the requested ID
/// </summary>
/// <param name="id">Job-ID</param>
/// <param name="JobId">Job-ID</param>
/// <remarks>NOT IMPLEMENTED</remarks>
[HttpPost("{id}/Stop")]
public IActionResult StopJob(string id)
[HttpPost("{JobId}/Stop")]
public IActionResult StopJob(string JobId)
{
throw new NotImplementedException();
}

View File

@ -26,15 +26,15 @@ public class LibraryConnectorController(PgsqlContext context) : Controller
/// <summary>
/// Returns Library-Connector with requested ID
/// </summary>
/// <param name="id">Library-Connector-ID</param>
/// <param name="LibraryControllerId">Library-Connector-ID</param>
/// <response code="200"></response>
/// <response code="404">Connector with ID not found.</response>
[HttpGet("{id}")]
[HttpGet("{LibraryControllerId}")]
[ProducesResponseType<LibraryConnector>(Status200OK, "application/json")]
[ProducesResponseType(Status404NotFound)]
public IActionResult GetConnector(string id)
public IActionResult GetConnector(string LibraryControllerId)
{
LibraryConnector? ret = context.LibraryConnectors.Find(id);
LibraryConnector? ret = context.LibraryConnectors.Find(LibraryControllerId);
return (ret is not null) switch
{
true => Ok(ret),
@ -68,19 +68,19 @@ public class LibraryConnectorController(PgsqlContext context) : Controller
/// <summary>
/// Deletes the Library-Connector with the requested ID
/// </summary>
/// <param name="id">Library-Connector-ID</param>
/// <param name="LibraryControllerId">Library-Connector-ID</param>
/// <response code="200"></response>
/// <response code="404">Connector with ID not found.</response>
/// <response code="500">Error during Database Operation</response>
[HttpDelete("{id}")]
[HttpDelete("{LibraryControllerId}")]
[ProducesResponseType(Status200OK)]
[ProducesResponseType(Status404NotFound)]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public IActionResult DeleteConnector(string id)
public IActionResult DeleteConnector(string LibraryControllerId)
{
try
{
LibraryConnector? ret = context.LibraryConnectors.Find(id);
LibraryConnector? ret = context.LibraryConnectors.Find(LibraryControllerId);
if (ret is null)
return NotFound();

View File

@ -55,7 +55,7 @@ public class MangaConnectorController(PgsqlContext context) : Controller
/// <response code="200"></response>
/// <response code="404">Connector with ID not found.</response>
/// <response code="500">Error during Database Operation</response>
[HttpPatch("{id}/SetEnabled/{enabled}")]
[HttpPatch("{MangaConnectorName}/SetEnabled/{enabled}")]
[ProducesResponseType(Status200OK)]
[ProducesResponseType(Status404NotFound)]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]

View File

@ -44,15 +44,15 @@ public class MangaController(PgsqlContext context) : Controller
/// <summary>
/// Return Manga with ID
/// </summary>
/// <param name="id">Manga-ID</param>
/// <param name="MangaId">Manga-ID</param>
/// <response code="200"></response>
/// <response code="404">Manga with ID not found</response>
[HttpGet("{id}")]
[HttpGet("{MangaId}")]
[ProducesResponseType<Manga>(Status200OK, "application/json")]
[ProducesResponseType(Status404NotFound)]
public IActionResult GetManga(string id)
public IActionResult GetManga(string MangaId)
{
Manga? ret = context.Manga.Find(id);
Manga? ret = context.Manga.Find(MangaId);
if (ret is null)
return NotFound();
return Ok(ret);
@ -61,19 +61,19 @@ public class MangaController(PgsqlContext context) : Controller
/// <summary>
/// Delete Manga with ID
/// </summary>
/// <param name="id">Manga-ID</param>
/// <param name="MangaId">Manga-ID</param>
/// <response code="200"></response>
/// <response code="404">Manga with ID not found</response>
/// <response code="500">Error during Database Operation</response>
[HttpDelete("{id}")]
[HttpDelete("{MangaId}")]
[ProducesResponseType(Status200OK)]
[ProducesResponseType(Status404NotFound)]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public IActionResult DeleteManga(string id)
public IActionResult DeleteManga(string MangaId)
{
try
{
Manga? ret = context.Manga.Find(id);
Manga? ret = context.Manga.Find(MangaId);
if (ret is null)
return NotFound();
@ -90,20 +90,20 @@ public class MangaController(PgsqlContext context) : Controller
/// <summary>
/// Returns Cover of Manga
/// </summary>
/// <param name="id">Manga-ID</param>
/// <param name="MangaId">Manga-ID</param>
/// <param name="formatRequest">Formatting/Resizing Request</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">Manga with ID not found</response>
[HttpPost("{id}/Cover")]
[HttpPost("{MangaId}/Cover")]
[ProducesResponseType<byte[]>(Status200OK,"image/jpeg")]
[ProducesResponseType(Status204NoContent)]
[ProducesResponseType(Status400BadRequest)]
[ProducesResponseType(Status404NotFound)]
public IActionResult GetCover(string id, [FromBody(EmptyBodyBehavior = EmptyBodyBehavior.Allow)]CoverFormatRequestRecord? formatRequest)
public IActionResult GetCover(string MangaId, [FromBody(EmptyBodyBehavior = EmptyBodyBehavior.Allow)]CoverFormatRequestRecord? formatRequest)
{
Manga? m = context.Manga.Find(id);
Manga? m = context.Manga.Find(MangaId);
if (m is null)
return NotFound();
if (!System.IO.File.Exists(m.CoverFileNameInCache))
@ -130,15 +130,15 @@ public class MangaController(PgsqlContext context) : Controller
/// <summary>
/// Returns all Chapters of Manga
/// </summary>
/// <param name="id">Manga-ID</param>
/// <param name="MangaId">Manga-ID</param>
/// <response code="200"></response>
/// <response code="404">Manga with ID not found</response>
[HttpGet("{id}/Chapters")]
[HttpGet("{MangaId}/Chapters")]
[ProducesResponseType<Chapter[]>(Status200OK, "application/json")]
[ProducesResponseType(Status404NotFound)]
public IActionResult GetChapters(string id)
public IActionResult GetChapters(string MangaId)
{
Manga? m = context.Manga.Find(id);
Manga? m = context.Manga.Find(MangaId);
if (m is null)
return NotFound();
@ -149,19 +149,19 @@ public class MangaController(PgsqlContext context) : Controller
/// <summary>
/// Returns the latest Chapter of requested Manga available on Website
/// </summary>
/// <param name="id">Manga-ID</param>
/// <param name="MangaId">Manga-ID</param>
/// <response code="200"></response>
/// <response code="204">No available chapters</response>
/// <response code="404">Manga with ID not found.</response>
/// <response code="500">Could not retrieve the maximum chapter-number</response>
[HttpGet("{id}/Chapter/LatestAvailable")]
[HttpGet("{MangaId}/Chapter/LatestAvailable")]
[ProducesResponseType<Chapter>(Status200OK, "application/json")]
[ProducesResponseType(Status204NoContent)]
[ProducesResponseType(Status404NotFound)]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public IActionResult GetLatestChapter(string id)
public IActionResult GetLatestChapter(string MangaId)
{
Manga? m = context.Manga.Find(id);
Manga? m = context.Manga.Find(MangaId);
if (m is null)
return NotFound();
@ -179,19 +179,19 @@ public class MangaController(PgsqlContext context) : Controller
/// <summary>
/// Returns the latest Chapter of requested Manga that is downloaded
/// </summary>
/// <param name="id">Manga-ID</param>
/// <param name="MangaId">Manga-ID</param>
/// <response code="200"></response>
/// <response code="204">No available chapters</response>
/// <response code="404">Manga with ID not found.</response>
/// <response code="500">Could not retrieve the maximum chapter-number</response>
[HttpGet("{id}/Chapter/LatestDownloaded")]
[HttpGet("{MangaId}/Chapter/LatestDownloaded")]
[ProducesResponseType<Chapter>(Status200OK, "application/json")]
[ProducesResponseType(Status204NoContent)]
[ProducesResponseType(Status404NotFound)]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public IActionResult GetLatestChapterDownloaded(string id)
public IActionResult GetLatestChapterDownloaded(string MangaId)
{
Manga? m = context.Manga.Find(id);
Manga? m = context.Manga.Find(MangaId);
if (m is null)
return NotFound();
@ -209,15 +209,15 @@ public class MangaController(PgsqlContext context) : Controller
/// <summary>
/// Configure the cut-off for Manga
/// </summary>
/// <param name="id">Manga-ID</param>
/// <param name="MangaId">Manga-ID</param>
/// <response code="200"></response>
/// <response code="404">Manga with ID not found.</response>
[HttpPatch("{id}/IgnoreChaptersBefore")]
[HttpPatch("{MangaId}/IgnoreChaptersBefore")]
[ProducesResponseType<float>(Status200OK, "text/plain")]
[ProducesResponseType(Status404NotFound)]
public IActionResult IgnoreChaptersBefore(string id)
public IActionResult IgnoreChaptersBefore(string MangaId)
{
Manga? m = context.Manga.Find(id);
Manga? m = context.Manga.Find(MangaId);
if (m is null)
return NotFound();
return Ok(m.IgnoreChapterBefore);
@ -226,11 +226,11 @@ public class MangaController(PgsqlContext context) : Controller
/// <summary>
/// Move the Directory the .cbz-files are located in
/// </summary>
/// <param name="id">Manga-ID</param>
/// <param name="MangaId">Manga-ID</param>
/// <param name="folder">New Directory-Path</param>
/// <remarks>NOT IMPLEMENTED</remarks>
[HttpPost("{id}/MoveFolder")]
public IActionResult MoveFolder(string id, [FromBody]string folder)
[HttpPost("{MangaId}/MoveFolder")]
public IActionResult MoveFolder(string MangaId, [FromBody]string folder)
{
throw new NotImplementedException();
}

View File

@ -29,15 +29,15 @@ public class NotificationConnectorController(PgsqlContext context) : Controller
/// <summary>
/// Returns Notification-Connector with requested ID
/// </summary>
/// <param name="id">Notification-Connector-ID</param>
/// <param name="NotificationConnectorId">Notification-Connector-ID</param>
/// <response code="200"></response>
/// <response code="404">NotificationConnector with ID not found</response>
[HttpGet("{id}")]
[HttpGet("{NotificationConnectorId}")]
[ProducesResponseType<NotificationConnector>(Status200OK, "application/json")]
[ProducesResponseType(Status404NotFound)]
public IActionResult GetConnector(string id)
public IActionResult GetConnector(string NotificationConnectorId)
{
NotificationConnector? ret = context.NotificationConnectors.Find(id);
NotificationConnector? ret = context.NotificationConnectors.Find(NotificationConnectorId);
return (ret is not null) switch
{
true => Ok(ret),
@ -161,19 +161,19 @@ public class NotificationConnectorController(PgsqlContext context) : Controller
/// <summary>
/// Deletes the Notification-Connector with the requested ID
/// </summary>
/// <param name="id">Notification-Connector-ID</param>
/// <param name="NotificationConnectorId">Notification-Connector-ID</param>
/// <response code="200"></response>
/// <response code="404">NotificationConnector with ID not found</response>
/// <response code="500">Error during Database Operation</response>
[HttpDelete("{id}")]
[HttpDelete("{NotificationConnectorId}")]
[ProducesResponseType(Status200OK)]
[ProducesResponseType(Status404NotFound)]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public IActionResult DeleteConnector(string id)
public IActionResult DeleteConnector(string NotificationConnectorId)
{
try
{
NotificationConnector? ret = context.NotificationConnectors.Find(id);
NotificationConnector? ret = context.NotificationConnectors.Find(NotificationConnectorId);
if(ret is null)
return NotFound();

View File

@ -20,10 +20,10 @@ public class SearchController(PgsqlContext context) : Controller
/// <param name="name">Name/Title of the Manga</param>
/// <response code="200"></response>
/// <response code="500">Error during Database Operation</response>
[HttpPost("{name}")]
[HttpPost("Name")]
[ProducesResponseType<Manga[]>(Status200OK, "application/json")]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public IActionResult SearchMangaGlobal(string name)
public IActionResult SearchMangaGlobal([FromBody]string name)
{
List<(Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)> allManga = new();
foreach (MangaConnector contextMangaConnector in context.MangaConnectors.Where(connector => connector.Enabled))
@ -55,7 +55,7 @@ public class SearchController(PgsqlContext context) : Controller
/// <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>
[HttpPost("{id}/{name}")]
[HttpPost("{MangaConnectorName}/{SearchName}")]
[ProducesResponseType<Manga[]>(Status200OK, "application/json")]
[ProducesResponseType(Status404NotFound)]
[ProducesResponseType(Status406NotAcceptable)]
@ -86,6 +86,45 @@ public class SearchController(PgsqlContext context) : Controller
return Ok(retMangas.ToArray());
}
/// <summary>
/// Returns Manga from MangaConnector associated with URL
/// </summary>
/// <param name="url">Manga-Page URL</param>
/// <response code="200"></response>
/// <response code="300">Multiple connectors found for URL</response>
/// <response code="400">No Manga at URL</response>
/// <response code="404">No connector found for URL</response>
/// <response code="500">Error during Database Operation</response>
[HttpPost("Url")]
[ProducesResponseType<Manga>(Status200OK, "application/json")]
[ProducesResponseType(Status300MultipleChoices)]
[ProducesResponseType(Status400BadRequest)]
[ProducesResponseType(Status404NotFound)]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public IActionResult GetMangaFromUrl([FromBody]string url)
{
List<MangaConnector> connectors = context.MangaConnectors.AsEnumerable().Where(c => c.ValidateUrl(url)).ToList();
if (connectors.Count == 0)
return NotFound();
else if (connectors.Count > 1)
return StatusCode(Status300MultipleChoices);
(Manga manga, List<Author>? authors, List<MangaTag>? tags, List<Link>? links, List<MangaAltTitle>? altTitles)? x = connectors.First().GetMangaFromUrl(url);
if (x is null)
return BadRequest();
try
{
Manga? add = AddMangaToContext(x.Value.manga, x.Value.authors, x.Value.tags, x.Value.links, x.Value.altTitles);
if (add is not null)
return Ok(add);
return StatusCode(500);
}
catch (Exception e)
{
return StatusCode(500, e.Message);
}
}
private Manga? AddMangaToContext(Manga? manga, List<Author>? authors, List<MangaTag>? tags, List<Link>? links,
List<MangaAltTitle>? altTitles)

View File

@ -0,0 +1,689 @@
// <auto-generated />
using System;
using System.Collections.Generic;
using API.Schema;
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
{
[DbContext(typeof(PgsqlContext))]
[Migration("20250308121028_dev-080325-1")]
partial class dev0803251
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "9.0.2")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "hstore");
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("API.Schema.Author", b =>
{
b.Property<string>("AuthorId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("AuthorName")
.IsRequired()
.HasColumnType("text");
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>("ArchiveFileName")
.IsRequired()
.HasColumnType("text");
b.Property<string>("ChapterNumber")
.IsRequired()
.HasMaxLength(10)
.HasColumnType("character varying(10)");
b.Property<bool>("Downloaded")
.HasColumnType("boolean");
b.Property<string>("ParentMangaId")
.IsRequired()
.HasColumnType("character varying(64)");
b.Property<string>("Title")
.HasColumnType("text");
b.Property<string>("Url")
.IsRequired()
.HasColumnType("text");
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.PrimitiveCollection<string[]>("DependsOnJobsIds")
.HasMaxLength(64)
.HasColumnType("text[]");
b.Property<bool>("Enabled")
.HasColumnType("boolean");
b.Property<string>("JobId1")
.HasColumnType("character varying(64)");
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("JobId1");
b.HasIndex("ParentJobId");
b.ToTable("Jobs");
b.HasDiscriminator<byte>("JobType");
b.UseTphMappingStrategy();
});
modelBuilder.Entity("API.Schema.LibraryConnectors.LibraryConnector", b =>
{
b.Property<string>("LibraryConnectorId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("Auth")
.IsRequired()
.HasColumnType("text");
b.Property<string>("BaseUrl")
.IsRequired()
.HasColumnType("text");
b.Property<byte>("LibraryType")
.HasColumnType("smallint");
b.HasKey("LibraryConnectorId");
b.ToTable("LibraryConnectors");
b.HasDiscriminator<byte>("LibraryType");
b.UseTphMappingStrategy();
});
modelBuilder.Entity("API.Schema.Link", b =>
{
b.Property<string>("LinkId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("LinkProvider")
.IsRequired()
.HasColumnType("text");
b.Property<string>("LinkUrl")
.IsRequired()
.HasColumnType("text");
b.Property<string>("MangaId")
.HasColumnType("character varying(64)");
b.HasKey("LinkId");
b.HasIndex("MangaId");
b.ToTable("Link");
});
modelBuilder.Entity("API.Schema.Manga", b =>
{
b.Property<string>("MangaId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.PrimitiveCollection<string[]>("AltTitleIds")
.IsRequired()
.HasColumnType("text[]");
b.PrimitiveCollection<string[]>("AuthorIds")
.IsRequired()
.HasColumnType("text[]");
b.Property<string>("ConnectorId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("CoverFileNameInCache")
.HasColumnType("text");
b.Property<string>("CoverUrl")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Description")
.IsRequired()
.HasColumnType("text");
b.Property<string>("FolderName")
.IsRequired()
.HasColumnType("text");
b.Property<float>("IgnoreChapterBefore")
.HasColumnType("real");
b.PrimitiveCollection<string[]>("LinkIds")
.IsRequired()
.HasColumnType("text[]");
b.Property<string>("MangaConnectorId")
.IsRequired()
.HasColumnType("character varying(32)");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.Property<string>("OriginalLanguage")
.HasColumnType("text");
b.Property<byte>("ReleaseStatus")
.HasColumnType("smallint");
b.PrimitiveCollection<string[]>("TagIds")
.IsRequired()
.HasColumnType("text[]");
b.Property<string>("WebsiteUrl")
.IsRequired()
.HasColumnType("text");
b.Property<long>("Year")
.HasColumnType("bigint");
b.HasKey("MangaId");
b.HasIndex("MangaConnectorId");
b.ToTable("Manga");
});
modelBuilder.Entity("API.Schema.MangaAltTitle", b =>
{
b.Property<string>("AltTitleId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("Language")
.IsRequired()
.HasMaxLength(8)
.HasColumnType("character varying(8)");
b.Property<string>("MangaId")
.HasColumnType("character varying(64)");
b.Property<string>("Title")
.IsRequired()
.HasColumnType("text");
b.HasKey("AltTitleId");
b.HasIndex("MangaId");
b.ToTable("AltTitles");
});
modelBuilder.Entity("API.Schema.MangaConnectors.MangaConnector", b =>
{
b.Property<string>("Name")
.HasMaxLength(32)
.HasColumnType("character varying(32)");
b.PrimitiveCollection<string[]>("BaseUris")
.IsRequired()
.HasColumnType("text[]");
b.Property<bool>("Enabled")
.HasColumnType("boolean");
b.Property<string>("IconUrl")
.IsRequired()
.HasColumnType("text");
b.PrimitiveCollection<string[]>("SupportedLanguages")
.IsRequired()
.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")
.HasColumnType("text");
b.HasKey("Tag");
b.ToTable("Tags");
});
modelBuilder.Entity("API.Schema.Notification", b =>
{
b.Property<string>("NotificationId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<DateTime>("Date")
.HasColumnType("timestamp with time zone");
b.Property<string>("Message")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Title")
.IsRequired()
.HasColumnType("text");
b.Property<byte>("Urgency")
.HasColumnType("smallint");
b.HasKey("NotificationId");
b.ToTable("Notifications");
});
modelBuilder.Entity("API.Schema.NotificationConnectors.NotificationConnector", b =>
{
b.Property<string>("Name")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("Body")
.IsRequired()
.HasColumnType("text");
b.Property<Dictionary<string, string>>("Headers")
.IsRequired()
.HasColumnType("hstore");
b.Property<string>("HttpMethod")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Url")
.IsRequired()
.HasColumnType("text");
b.HasKey("Name");
b.ToTable("NotificationConnectors");
});
modelBuilder.Entity("AuthorManga", b =>
{
b.Property<string>("AuthorsAuthorId")
.HasColumnType("character varying(64)");
b.Property<string>("MangaId")
.HasColumnType("character varying(64)");
b.HasKey("AuthorsAuthorId", "MangaId");
b.HasIndex("MangaId");
b.ToTable("AuthorManga");
});
modelBuilder.Entity("MangaMangaTag", b =>
{
b.Property<string>("MangaId")
.HasColumnType("character varying(64)");
b.Property<string>("TagsTag")
.HasColumnType("text");
b.HasKey("MangaId", "TagsTag");
b.HasIndex("TagsTag");
b.ToTable("MangaMangaTag");
});
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.ToTable("Jobs", t =>
{
t.Property("MangaId")
.HasColumnName("DownloadMangaCoverJob_MangaId");
});
b.HasDiscriminator().HasValue((byte)4);
});
modelBuilder.Entity("API.Schema.Jobs.DownloadNewChaptersJob", 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)1);
});
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()
.HasColumnType("text");
b.Property<string>("ToLocation")
.IsRequired()
.HasColumnType("text");
b.HasDiscriminator().HasValue((byte)3);
});
modelBuilder.Entity("API.Schema.Jobs.UpdateMetadataJob", 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("UpdateMetadataJob_MangaId");
});
b.HasDiscriminator().HasValue((byte)2);
});
modelBuilder.Entity("API.Schema.LibraryConnectors.Kavita", b =>
{
b.HasBaseType("API.Schema.LibraryConnectors.LibraryConnector");
b.HasDiscriminator().HasValue((byte)1);
});
modelBuilder.Entity("API.Schema.LibraryConnectors.Komga", b =>
{
b.HasBaseType("API.Schema.LibraryConnectors.LibraryConnector");
b.HasDiscriminator().HasValue((byte)0);
});
modelBuilder.Entity("API.Schema.MangaConnectors.AsuraToon", b =>
{
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
b.HasDiscriminator().HasValue("AsuraToon");
});
modelBuilder.Entity("API.Schema.MangaConnectors.Bato", b =>
{
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
b.HasDiscriminator().HasValue("Bato");
});
modelBuilder.Entity("API.Schema.MangaConnectors.MangaDex", b =>
{
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
b.HasDiscriminator().HasValue("MangaDex");
});
modelBuilder.Entity("API.Schema.MangaConnectors.MangaHere", b =>
{
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
b.HasDiscriminator().HasValue("MangaHere");
});
modelBuilder.Entity("API.Schema.MangaConnectors.MangaKatana", b =>
{
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
b.HasDiscriminator().HasValue("MangaKatana");
});
modelBuilder.Entity("API.Schema.MangaConnectors.Mangaworld", b =>
{
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
b.HasDiscriminator().HasValue("Mangaworld");
});
modelBuilder.Entity("API.Schema.MangaConnectors.ManhuaPlus", b =>
{
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
b.HasDiscriminator().HasValue("ManhuaPlus");
});
modelBuilder.Entity("API.Schema.MangaConnectors.Weebcentral", b =>
{
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
b.HasDiscriminator().HasValue("Weebcentral");
});
modelBuilder.Entity("API.Schema.Chapter", b =>
{
b.HasOne("API.Schema.Manga", "ParentManga")
.WithMany()
.HasForeignKey("ParentMangaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("ParentManga");
});
modelBuilder.Entity("API.Schema.Jobs.Job", b =>
{
b.HasOne("API.Schema.Jobs.Job", null)
.WithMany("DependsOnJobs")
.HasForeignKey("JobId1");
b.HasOne("API.Schema.Jobs.Job", "ParentJob")
.WithMany()
.HasForeignKey("ParentJobId");
b.Navigation("ParentJob");
});
modelBuilder.Entity("API.Schema.Link", b =>
{
b.HasOne("API.Schema.Manga", null)
.WithMany("Links")
.HasForeignKey("MangaId");
});
modelBuilder.Entity("API.Schema.Manga", b =>
{
b.HasOne("API.Schema.MangaConnectors.MangaConnector", "MangaConnector")
.WithMany()
.HasForeignKey("MangaConnectorId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("MangaConnector");
});
modelBuilder.Entity("API.Schema.MangaAltTitle", b =>
{
b.HasOne("API.Schema.Manga", null)
.WithMany("AltTitles")
.HasForeignKey("MangaId");
});
modelBuilder.Entity("AuthorManga", b =>
{
b.HasOne("API.Schema.Author", null)
.WithMany()
.HasForeignKey("AuthorsAuthorId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("API.Schema.Manga", null)
.WithMany()
.HasForeignKey("MangaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("MangaMangaTag", b =>
{
b.HasOne("API.Schema.Manga", null)
.WithMany()
.HasForeignKey("MangaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("API.Schema.MangaTag", null)
.WithMany()
.HasForeignKey("TagsTag")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
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.DownloadNewChaptersJob", 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.UpdateMetadataJob", b =>
{
b.HasOne("API.Schema.Manga", "Manga")
.WithMany()
.HasForeignKey("MangaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Manga");
});
modelBuilder.Entity("API.Schema.Jobs.Job", b =>
{
b.Navigation("DependsOnJobs");
});
modelBuilder.Entity("API.Schema.Manga", b =>
{
b.Navigation("AltTitles");
b.Navigation("Links");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,62 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace API.Migrations
{
/// <inheritdoc />
public partial class dev0803251 : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string[]>(
name: "AltTitleIds",
table: "Manga",
type: "text[]",
nullable: false,
defaultValue: new string[0]);
migrationBuilder.AddColumn<string[]>(
name: "AuthorIds",
table: "Manga",
type: "text[]",
nullable: false,
defaultValue: new string[0]);
migrationBuilder.AddColumn<string[]>(
name: "LinkIds",
table: "Manga",
type: "text[]",
nullable: false,
defaultValue: new string[0]);
migrationBuilder.AddColumn<string[]>(
name: "TagIds",
table: "Manga",
type: "text[]",
nullable: false,
defaultValue: new string[0]);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "AltTitleIds",
table: "Manga");
migrationBuilder.DropColumn(
name: "AuthorIds",
table: "Manga");
migrationBuilder.DropColumn(
name: "LinkIds",
table: "Manga");
migrationBuilder.DropColumn(
name: "TagIds",
table: "Manga");
}
}
}

View File

@ -179,6 +179,14 @@ namespace API.Migrations
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.PrimitiveCollection<string[]>("AltTitleIds")
.IsRequired()
.HasColumnType("text[]");
b.PrimitiveCollection<string[]>("AuthorIds")
.IsRequired()
.HasColumnType("text[]");
b.Property<string>("ConnectorId")
.IsRequired()
.HasMaxLength(64)
@ -202,6 +210,10 @@ namespace API.Migrations
b.Property<float>("IgnoreChapterBefore")
.HasColumnType("real");
b.PrimitiveCollection<string[]>("LinkIds")
.IsRequired()
.HasColumnType("text[]");
b.Property<string>("MangaConnectorId")
.IsRequired()
.HasColumnType("character varying(32)");
@ -216,6 +228,10 @@ namespace API.Migrations
b.Property<byte>("ReleaseStatus")
.HasColumnType("smallint");
b.PrimitiveCollection<string[]>("TagIds")
.IsRequired()
.HasColumnType("text[]");
b.Property<string>("WebsiteUrl")
.IsRequired()
.HasColumnType("text");

View File

@ -2,6 +2,7 @@
using System.Xml.Linq;
using API.Schema.Jobs;
using Microsoft.EntityFrameworkCore;
using Newtonsoft.Json;
namespace API.Schema;
@ -38,6 +39,7 @@ public class Chapter : IComparable<Chapter>
public bool Downloaded { get; internal set; } = false;
public string ParentMangaId { get; internal set; }
[JsonIgnore]
public Manga? ParentManga { get; init; }
public int CompareTo(Chapter? other)

View File

@ -3,6 +3,7 @@ using System.IO.Compression;
using System.Runtime.InteropServices;
using API.MangaDownloadClients;
using API.Schema.MangaConnectors;
using Newtonsoft.Json;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Formats.Jpeg;
using SixLabors.ImageSharp.Processing;
@ -16,6 +17,7 @@ public class DownloadMangaCoverJob(string mangaId, string? parentJobId = null, I
{
[MaxLength(64)]
public string MangaId { get; init; } = mangaId;
[JsonIgnore]
public Manga? Manga { get; init; }
protected override IEnumerable<Job> RunInternal(PgsqlContext context)

View File

@ -1,5 +1,6 @@
using System.ComponentModel.DataAnnotations;
using API.Schema.MangaConnectors;
using Newtonsoft.Json;
namespace API.Schema.Jobs;
@ -8,6 +9,8 @@ public class DownloadNewChaptersJob(ulong recurrenceMs, string mangaId, string?
{
[MaxLength(64)]
public string MangaId { get; init; } = mangaId;
[JsonIgnore]
public Manga? Manga { get; init; }
protected override IEnumerable<Job> RunInternal(PgsqlContext context)

View File

@ -3,6 +3,7 @@ using System.IO.Compression;
using System.Runtime.InteropServices;
using API.MangaDownloadClients;
using API.Schema.MangaConnectors;
using Newtonsoft.Json;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Formats.Jpeg;
using SixLabors.ImageSharp.Processing;
@ -16,6 +17,8 @@ public class DownloadSingleChapterJob(string chapterId, string? parentJobId = nu
{
[MaxLength(64)]
public string ChapterId { get; init; } = chapterId;
[JsonIgnore]
public Chapter? Chapter { get; init; }
protected override IEnumerable<Job> RunInternal(PgsqlContext context)

View File

@ -13,10 +13,12 @@ public abstract class Job
[MaxLength(64)]
public string? ParentJobId { get; init; }
[JsonIgnore]
public Job? ParentJob { get; init; }
[MaxLength(64)]
public ICollection<string>? DependsOnJobsIds { get; init; }
[JsonIgnore]
public ICollection<Job>? DependsOnJobs { get; init; }
public JobType JobType { get; init; }

View File

@ -1,29 +0,0 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using JsonSerializer = Newtonsoft.Json.JsonSerializer;
namespace API.Schema.Jobs;
public class JobJsonDeserializer : JsonConverter<Job>
{
public override bool CanWrite { get; } = false;
public override void WriteJson(JsonWriter writer, Job? value, JsonSerializer serializer)
{
throw new NotImplementedException();
}
public override Job? ReadJson(JsonReader reader, Type objectType, Job? existingValue, bool hasExistingValue, JsonSerializer serializer)
{
JObject j = JObject.Load(reader);
JobType? type = Enum.Parse<JobType>(j.GetValue("jobType")!.Value<string>()!);
return type switch
{
JobType.DownloadSingleChapterJob => j.ToObject<DownloadSingleChapterJob>(),
JobType.DownloadNewChaptersJob => j.ToObject<DownloadNewChaptersJob>(),
JobType.UpdateMetaDataJob => j.ToObject<UpdateMetadataJob>(),
JobType.MoveFileOrFolderJob => j.ToObject<MoveFileOrFolderJob>(),
_ => null
};
}
}

View File

@ -1,5 +1,6 @@
using System.ComponentModel.DataAnnotations;
using API.Schema.MangaConnectors;
using Newtonsoft.Json;
namespace API.Schema.Jobs;
@ -8,6 +9,8 @@ public class UpdateMetadataJob(ulong recurrenceMs, string mangaId, string? paren
{
[MaxLength(64)]
public string MangaId { get; init; } = mangaId;
[JsonIgnore]
public virtual Manga? Manga { get; init; }
/// <summary>

View File

@ -1,3 +1,4 @@
using System.Collections;
using System.ComponentModel.DataAnnotations;
using System.Net;
using System.Runtime.InteropServices;
@ -6,6 +7,7 @@ using API.MangaDownloadClients;
using API.Schema.Jobs;
using API.Schema.MangaConnectors;
using Microsoft.EntityFrameworkCore;
using Newtonsoft.Json;
using static System.IO.UnixFileMode;
namespace API.Schema;
@ -30,15 +32,23 @@ public class Manga
public float IgnoreChapterBefore { get; internal set; }
public string MangaConnectorId { get; private set; }
[JsonIgnore]
public MangaConnector? MangaConnector { get; private set; }
public ICollection<string> AuthorIds { get; internal set; }
[JsonIgnore]
public ICollection<Author>? Authors { get; internal set; }
public ICollection<string> TagIds { get; internal set; }
[JsonIgnore]
public ICollection<MangaTag>? Tags { get; internal set; }
public ICollection<string> LinkIds { get; internal set; }
[JsonIgnore]
public ICollection<Link>? Links { get; internal set; }
public ICollection<string> AltTitleIds { get; internal set; }
[JsonIgnore]
public ICollection<MangaAltTitle>? AltTitles { get; internal set; }
public Manga(string connectorId, string name, string description, string websiteUrl, string coverUrl,
@ -48,9 +58,13 @@ public class Manga
: this(connectorId, name, description, websiteUrl, coverUrl, coverFileNameInCache, year, originalLanguage,
releaseStatus, ignoreChapterBefore, mangaConnector.Name)
{
this.AuthorIds = authors.Select(a => a.AuthorId) as ICollection<string>;
this.Authors = authors;
this.TagIds = tags.Select(t => t.Tag) as ICollection<string>;
this.Tags = tags;
this.LinkIds = links.Select(l => l.LinkId) as ICollection<string>;
this.Links = links;
this.AltTitleIds = altTitles.Select(t => t.AltTitleId) as ICollection<string>;
this.AltTitles = altTitles;
}

View File

@ -1,5 +1,6 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Text.RegularExpressions;
using API.MangaDownloadClients;
using Microsoft.EntityFrameworkCore;
using Newtonsoft.Json;
@ -39,4 +40,6 @@ public abstract class MangaConnector(string name, string[] supportedLanguages, s
}
internal abstract string[] GetChapterImageUrls(Chapter chapter);
public bool ValidateUrl(string url) => BaseUris.Any(baseUri => Regex.IsMatch(url, "https?://" + baseUri + "/.*"));
}