MangaConnectors do not have to return an Object with 6 Parameters.

Job-Start Logic readable and optimized
More robust Database design
This commit is contained in:
Glax 2025-05-09 06:28:44 +02:00
parent 7477f4d04d
commit 7d4a6be569
56 changed files with 2924 additions and 5855 deletions

View File

@ -0,0 +1,5 @@
using System.ComponentModel.DataAnnotations;
namespace API.APIEndpointRecords;
public record DownloadAvailableChaptersJobRecord([Required]string language, [Required]ulong recurrenceTimeMs, [Required]string localLibraryId);

View File

@ -1,5 +0,0 @@
using System.ComponentModel.DataAnnotations;
namespace API.APIEndpointRecords;
public record DownloadAvailableJobsRecord([Required]ulong recurrenceTimeMs, [Required]string localLibraryId);

View File

@ -2,15 +2,17 @@
using API.Schema; using API.Schema;
using API.Schema.Jobs; using API.Schema.Jobs;
using Asp.Versioning; using Asp.Versioning;
using log4net;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using static Microsoft.AspNetCore.Http.StatusCodes; using static Microsoft.AspNetCore.Http.StatusCodes;
// ReSharper disable InconsistentNaming
namespace API.Controllers; namespace API.Controllers;
[ApiVersion(2)] [ApiVersion(2)]
[ApiController] [ApiController]
[Route("v{version:apiVersion}/[controller]")] [Route("v{version:apiVersion}/[controller]")]
public class JobController(PgsqlContext context) : Controller public class JobController(PgsqlContext context, ILog Log) : Controller
{ {
/// <summary> /// <summary>
/// Returns all Jobs /// Returns all Jobs
@ -102,7 +104,7 @@ public class JobController(PgsqlContext context) : Controller
/// <param name="MangaId">ID of Manga</param> /// <param name="MangaId">ID of Manga</param>
/// <param name="record">Job-Configuration</param> /// <param name="record">Job-Configuration</param>
/// <response code="201">Job-IDs</response> /// <response code="201">Job-IDs</response>
/// <response code="400">Could not find Library with ID</response> /// <response code="400">Could not find ToLibrary with ID</response>
/// <response code="404">Could not find Manga with ID</response> /// <response code="404">Could not find Manga with ID</response>
/// <response code="500">Error during Database Operation</response> /// <response code="500">Error during Database Operation</response>
[HttpPut("DownloadAvailableChaptersJob/{MangaId}")] [HttpPut("DownloadAvailableChaptersJob/{MangaId}")]
@ -110,7 +112,7 @@ public class JobController(PgsqlContext context) : Controller
[ProducesResponseType(Status400BadRequest)] [ProducesResponseType(Status400BadRequest)]
[ProducesResponseType(Status404NotFound)] [ProducesResponseType(Status404NotFound)]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")] [ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public IActionResult CreateDownloadAvailableChaptersJob(string MangaId, [FromBody]DownloadAvailableJobsRecord record) public IActionResult CreateDownloadAvailableChaptersJob(string MangaId, [FromBody]DownloadAvailableChaptersJobRecord record)
{ {
if (context.Mangas.Find(MangaId) is not { } m) if (context.Mangas.Find(MangaId) is not { } m)
return NotFound(); return NotFound();
@ -126,13 +128,13 @@ public class JobController(PgsqlContext context) : Controller
} }
catch (Exception e) catch (Exception e)
{ {
Log.Error(e);
return StatusCode(500, e.Message); return StatusCode(500, e.Message);
} }
} }
Job job = new DownloadAvailableChaptersJob(record.recurrenceTimeMs, MangaId); Job retrieveChapters = new RetrieveChaptersJob(m, record.language, record.recurrenceTimeMs);
Job dep = new RetrieveChaptersJob(record.recurrenceTimeMs, MangaId, job.JobId); Job downloadChapters = new DownloadAvailableChaptersJob(m, record.recurrenceTimeMs, dependsOnJobs: [retrieveChapters]);
job.DependsOnJobsIds?.Add(dep.JobId); return AddJobs([retrieveChapters, downloadChapters]);
return AddJobs([dep, job]);
} }
/// <summary> /// <summary>
@ -148,9 +150,9 @@ public class JobController(PgsqlContext context) : Controller
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")] [ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public IActionResult CreateNewDownloadChapterJob(string ChapterId) public IActionResult CreateNewDownloadChapterJob(string ChapterId)
{ {
if(context.Chapters.Find(ChapterId) is null) if(context.Chapters.Find(ChapterId) is not { } c)
return NotFound(); return NotFound();
Job job = new DownloadSingleChapterJob(ChapterId); Job job = new DownloadSingleChapterJob(c);
return AddJobs([job]); return AddJobs([job]);
} }
@ -167,9 +169,9 @@ public class JobController(PgsqlContext context) : Controller
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")] [ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public IActionResult CreateUpdateFilesDownloadedJob(string MangaId) public IActionResult CreateUpdateFilesDownloadedJob(string MangaId)
{ {
if(context.Mangas.Find(MangaId) is null) if(context.Mangas.Find(MangaId) is not { } m)
return NotFound(); return NotFound();
Job job = new UpdateFilesDownloadedJob(0, MangaId); Job job = new UpdateFilesDownloadedJob(m, 0);
return AddJobs([job]); return AddJobs([job]);
} }
@ -183,8 +185,7 @@ public class JobController(PgsqlContext context) : Controller
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")] [ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public IActionResult CreateUpdateAllFilesDownloadedJob() public IActionResult CreateUpdateAllFilesDownloadedJob()
{ {
List<string> ids = context.Mangas.Select(m => m.MangaId).ToList(); List<UpdateFilesDownloadedJob> jobs = context.Mangas.Select(m => new UpdateFilesDownloadedJob(m, 0, null, null)).ToList();
List<UpdateFilesDownloadedJob> jobs = ids.Select(id => new UpdateFilesDownloadedJob(0, id)).ToList();
try try
{ {
context.Jobs.AddRange(jobs); context.Jobs.AddRange(jobs);
@ -193,12 +194,13 @@ public class JobController(PgsqlContext context) : Controller
} }
catch (Exception e) catch (Exception e)
{ {
Log.Error(e);
return StatusCode(500, e.Message); return StatusCode(500, e.Message);
} }
} }
/// <summary> /// <summary>
/// Create a new UpdateMetadataJob /// Not Implemented: Create a new UpdateMetadataJob
/// </summary> /// </summary>
/// <param name="MangaId">ID of the Manga</param> /// <param name="MangaId">ID of the Manga</param>
/// <response code="201">Job-IDs</response> /// <response code="201">Job-IDs</response>
@ -210,14 +212,11 @@ public class JobController(PgsqlContext context) : Controller
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")] [ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public IActionResult CreateUpdateMetadataJob(string MangaId) public IActionResult CreateUpdateMetadataJob(string MangaId)
{ {
if(context.Mangas.Find(MangaId) is null) return StatusCode(Status501NotImplemented);
return NotFound();
Job job = new UpdateMetadataJob(0, MangaId);
return AddJobs([job]);
} }
/// <summary> /// <summary>
/// Create a new UpdateMetadataJob for all Manga /// Not Implemented: Create a new UpdateMetadataJob for all Manga
/// </summary> /// </summary>
/// <response code="201">Job-IDs</response> /// <response code="201">Job-IDs</response>
/// <response code="500">Error during Database Operation</response> /// <response code="500">Error during Database Operation</response>
@ -226,18 +225,7 @@ public class JobController(PgsqlContext context) : Controller
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")] [ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public IActionResult CreateUpdateAllMetadataJob() public IActionResult CreateUpdateAllMetadataJob()
{ {
List<string> ids = context.Mangas.Select(m => m.MangaId).ToList(); return StatusCode(Status501NotImplemented);
List<UpdateMetadataJob> jobs = ids.Select(id => new UpdateMetadataJob(0, id)).ToList();
try
{
context.Jobs.AddRange(jobs);
context.SaveChanges();
return Created();
}
catch (Exception e)
{
return StatusCode(500, e.Message);
}
} }
private IActionResult AddJobs(Job[] jobs) private IActionResult AddJobs(Job[] jobs)
@ -250,6 +238,7 @@ public class JobController(PgsqlContext context) : Controller
} }
catch (Exception e) catch (Exception e)
{ {
Log.Error(e);
return StatusCode(500, e.Message); return StatusCode(500, e.Message);
} }
} }
@ -269,8 +258,7 @@ public class JobController(PgsqlContext context) : Controller
{ {
try try
{ {
Job? ret = context.Jobs.Find(JobId); if(context.Jobs.Find(JobId) is not { } ret)
if(ret is null)
return NotFound(); return NotFound();
context.Remove(ret); context.Remove(ret);
@ -279,6 +267,7 @@ public class JobController(PgsqlContext context) : Controller
} }
catch (Exception e) catch (Exception e)
{ {
Log.Error(e);
return StatusCode(500, e.Message); return StatusCode(500, e.Message);
} }
} }
@ -322,6 +311,7 @@ public class JobController(PgsqlContext context) : Controller
} }
catch (Exception e) catch (Exception e)
{ {
Log.Error(e);
return StatusCode(500, e.Message); return StatusCode(500, e.Message);
} }
} }
@ -354,6 +344,7 @@ public class JobController(PgsqlContext context) : Controller
} }
catch (Exception e) catch (Exception e)
{ {
Log.Error(e);
return StatusCode(500, e.Message); return StatusCode(500, e.Message);
} }
} }
@ -367,6 +358,6 @@ public class JobController(PgsqlContext context) : Controller
[ProducesResponseType(Status501NotImplemented)] [ProducesResponseType(Status501NotImplemented)]
public IActionResult StopJob(string JobId) public IActionResult StopJob(string JobId)
{ {
return StatusCode(501); return StatusCode(Status501NotImplemented);
} }
} }

View File

@ -1,6 +1,7 @@
using API.Schema; using API.Schema;
using API.Schema.LibraryConnectors; using API.Schema.LibraryConnectors;
using Asp.Versioning; using Asp.Versioning;
using log4net;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using static Microsoft.AspNetCore.Http.StatusCodes; using static Microsoft.AspNetCore.Http.StatusCodes;
@ -9,10 +10,10 @@ namespace API.Controllers;
[ApiVersion(2)] [ApiVersion(2)]
[ApiController] [ApiController]
[Route("v{v:apiVersion}/[controller]")] [Route("v{v:apiVersion}/[controller]")]
public class LibraryConnectorController(PgsqlContext context) : Controller public class LibraryConnectorController(PgsqlContext context, ILog Log) : Controller
{ {
/// <summary> /// <summary>
/// Gets all configured Library-Connectors /// Gets all configured ToLibrary-Connectors
/// </summary> /// </summary>
/// <response code="200"></response> /// <response code="200"></response>
[HttpGet] [HttpGet]
@ -24,9 +25,9 @@ public class LibraryConnectorController(PgsqlContext context) : Controller
} }
/// <summary> /// <summary>
/// Returns Library-Connector with requested ID /// Returns ToLibrary-Connector with requested ID
/// </summary> /// </summary>
/// <param name="LibraryControllerId">Library-Connector-ID</param> /// <param name="LibraryControllerId">ToLibrary-Connector-ID</param>
/// <response code="200"></response> /// <response code="200"></response>
/// <response code="404">Connector with ID not found.</response> /// <response code="404">Connector with ID not found.</response>
[HttpGet("{LibraryControllerId}")] [HttpGet("{LibraryControllerId}")]
@ -43,9 +44,9 @@ public class LibraryConnectorController(PgsqlContext context) : Controller
} }
/// <summary> /// <summary>
/// Creates a new Library-Connector /// Creates a new ToLibrary-Connector
/// </summary> /// </summary>
/// <param name="libraryConnector">Library-Connector</param> /// <param name="libraryConnector">ToLibrary-Connector</param>
/// <response code="201"></response> /// <response code="201"></response>
/// <response code="500">Error during Database Operation</response> /// <response code="500">Error during Database Operation</response>
[HttpPut] [HttpPut]
@ -61,14 +62,15 @@ public class LibraryConnectorController(PgsqlContext context) : Controller
} }
catch (Exception e) catch (Exception e)
{ {
Log.Error(e);
return StatusCode(500, e.Message); return StatusCode(500, e.Message);
} }
} }
/// <summary> /// <summary>
/// Deletes the Library-Connector with the requested ID /// Deletes the ToLibrary-Connector with the requested ID
/// </summary> /// </summary>
/// <param name="LibraryControllerId">Library-Connector-ID</param> /// <param name="LibraryControllerId">ToLibrary-Connector-ID</param>
/// <response code="200"></response> /// <response code="200"></response>
/// <response code="404">Connector with ID not found.</response> /// <response code="404">Connector with ID not found.</response>
/// <response code="500">Error during Database Operation</response> /// <response code="500">Error during Database Operation</response>
@ -90,6 +92,7 @@ public class LibraryConnectorController(PgsqlContext context) : Controller
} }
catch (Exception e) catch (Exception e)
{ {
Log.Error(e);
return StatusCode(500, e.Message); return StatusCode(500, e.Message);
} }
} }

View File

@ -1,6 +1,7 @@
using API.APIEndpointRecords; using API.APIEndpointRecords;
using API.Schema; using API.Schema;
using Asp.Versioning; using Asp.Versioning;
using log4net;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using static Microsoft.AspNetCore.Http.StatusCodes; using static Microsoft.AspNetCore.Http.StatusCodes;
@ -9,7 +10,7 @@ namespace API.Controllers;
[ApiVersion(2)] [ApiVersion(2)]
[ApiController] [ApiController]
[Route("v{v:apiVersion}/[controller]")] [Route("v{v:apiVersion}/[controller]")]
public class LocalLibrariesController(PgsqlContext context) : Controller public class LocalLibrariesController(PgsqlContext context, ILog Log) : Controller
{ {
[HttpGet] [HttpGet]
[ProducesResponseType<LocalLibrary[]>(Status200OK, "application/json")] [ProducesResponseType<LocalLibrary[]>(Status200OK, "application/json")]
@ -52,6 +53,7 @@ public class LocalLibrariesController(PgsqlContext context) : Controller
} }
catch (Exception e) catch (Exception e)
{ {
Log.Error(e);
return StatusCode(500, e.Message); return StatusCode(500, e.Message);
} }
} }
@ -79,6 +81,7 @@ public class LocalLibrariesController(PgsqlContext context) : Controller
} }
catch (Exception e) catch (Exception e)
{ {
Log.Error(e);
return StatusCode(500, e.Message); return StatusCode(500, e.Message);
} }
} }
@ -106,6 +109,7 @@ public class LocalLibrariesController(PgsqlContext context) : Controller
} }
catch (Exception e) catch (Exception e)
{ {
Log.Error(e);
return StatusCode(500, e.Message); return StatusCode(500, e.Message);
} }
} }
@ -128,6 +132,7 @@ public class LocalLibrariesController(PgsqlContext context) : Controller
} }
catch (Exception e) catch (Exception e)
{ {
Log.Error(e);
return StatusCode(500, e.Message); return StatusCode(500, e.Message);
} }
} }
@ -151,6 +156,7 @@ public class LocalLibrariesController(PgsqlContext context) : Controller
} }
catch (Exception e) catch (Exception e)
{ {
Log.Error(e);
return StatusCode(500, e.Message); return StatusCode(500, e.Message);
} }
} }

View File

@ -1,6 +1,7 @@
using API.Schema; using API.Schema;
using API.Schema.MangaConnectors; using API.Schema.MangaConnectors;
using Asp.Versioning; using Asp.Versioning;
using log4net;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using static Microsoft.AspNetCore.Http.StatusCodes; using static Microsoft.AspNetCore.Http.StatusCodes;
@ -9,7 +10,7 @@ namespace API.Controllers;
[ApiVersion(2)] [ApiVersion(2)]
[ApiController] [ApiController]
[Route("v{v:apiVersion}/[controller]")] [Route("v{v:apiVersion}/[controller]")]
public class MangaConnectorController(PgsqlContext context) : Controller public class MangaConnectorController(PgsqlContext context, ILog Log) : Controller
{ {
/// <summary> /// <summary>
/// Get all available Connectors (Scanlation-Sites) /// Get all available Connectors (Scanlation-Sites)
@ -74,6 +75,7 @@ public class MangaConnectorController(PgsqlContext context) : Controller
} }
catch (Exception e) catch (Exception e)
{ {
Log.Error(e);
return StatusCode(500, e.Message); return StatusCode(500, e.Message);
} }
} }

View File

@ -1,19 +1,21 @@
using API.Schema; using API.Schema;
using API.Schema.Jobs; using API.Schema.Jobs;
using Asp.Versioning; using Asp.Versioning;
using log4net;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using SixLabors.ImageSharp; using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Formats.Jpeg; using SixLabors.ImageSharp.Formats.Jpeg;
using SixLabors.ImageSharp.Processing; using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp.Processing.Processors.Transforms; using SixLabors.ImageSharp.Processing.Processors.Transforms;
using static Microsoft.AspNetCore.Http.StatusCodes; using static Microsoft.AspNetCore.Http.StatusCodes;
// ReSharper disable InconsistentNaming
namespace API.Controllers; namespace API.Controllers;
[ApiVersion(2)] [ApiVersion(2)]
[ApiController] [ApiController]
[Route("v{v:apiVersion}/[controller]")] [Route("v{v:apiVersion}/[controller]")]
public class MangaController(PgsqlContext context) : Controller public class MangaController(PgsqlContext context, ILog Log) : Controller
{ {
/// <summary> /// <summary>
/// Returns all cached Manga /// Returns all cached Manga
@ -82,6 +84,7 @@ public class MangaController(PgsqlContext context) : Controller
} }
catch (Exception e) catch (Exception e)
{ {
Log.Error(e);
return StatusCode(500, e.Message); return StatusCode(500, e.Message);
} }
} }
@ -292,6 +295,7 @@ public class MangaController(PgsqlContext context) : Controller
/// Configure the cut-off for Manga /// Configure the cut-off for Manga
/// </summary> /// </summary>
/// <param name="MangaId">Manga-ID</param> /// <param name="MangaId">Manga-ID</param>
/// <param name="chapterThreshold">Threshold (Chapter Number)</param>
/// <response code="200"></response> /// <response code="200"></response>
/// <response code="404">Manga with ID not found.</response> /// <response code="404">Manga with ID not found.</response>
/// <response code="500">Error during Database Operation</response> /// <response code="500">Error during Database Operation</response>
@ -307,21 +311,22 @@ public class MangaController(PgsqlContext context) : Controller
try try
{ {
m.IgnoreChapterBefore = chapterThreshold; m.IgnoreChaptersBefore = chapterThreshold;
context.SaveChanges(); context.SaveChanges();
return Ok(); return Ok();
} }
catch (Exception e) catch (Exception e)
{ {
Log.Error(e);
return StatusCode(500, e.Message); return StatusCode(500, e.Message);
} }
} }
/// <summary> /// <summary>
/// Move Manga to different Library /// Move Manga to different ToLibrary
/// </summary> /// </summary>
/// <param name="MangaId">Manga-ID</param> /// <param name="MangaId">Manga-ID</param>
/// <param name="LibraryId">Library-Id</param> /// <param name="LibraryId">ToLibrary-Id</param>
/// <response code="202">Folder is going to be moved</response> /// <response code="202">Folder is going to be moved</response>
/// <response code="404">MangaId or LibraryId not found</response> /// <response code="404">MangaId or LibraryId not found</response>
/// <response code="500">Error during Database Operation</response> /// <response code="500">Error during Database Operation</response>
@ -331,24 +336,23 @@ public class MangaController(PgsqlContext context) : Controller
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")] [ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public IActionResult MoveFolder(string MangaId, string LibraryId) public IActionResult MoveFolder(string MangaId, string LibraryId)
{ {
Manga? manga = context.Mangas.Find(MangaId); if (context.Mangas.Find(MangaId) is not { } manga)
if (manga is null)
return NotFound(); return NotFound();
LocalLibrary? library = context.LocalLibraries.Find(LibraryId); if(context.LocalLibraries.Find(LibraryId) is not { } library)
if (library is null)
return NotFound(); return NotFound();
MoveMangaLibraryJob dep = new (MangaId, LibraryId); MoveMangaLibraryJob moveLibrary = new(manga, library);
UpdateFilesDownloadedJob up = new (0, manga.MangaId, null, [dep.JobId]); UpdateFilesDownloadedJob updateDownloadedFiles = new(manga, 0, dependsOnJobs: [moveLibrary]);
try try
{ {
context.Jobs.AddRange([dep, up]); context.Jobs.AddRange(moveLibrary, updateDownloadedFiles);
context.SaveChanges(); context.SaveChanges();
return Accepted(); return Accepted();
} }
catch (Exception e) catch (Exception e)
{ {
Log.Error(e);
return StatusCode(500, e.Message); return StatusCode(500, e.Message);
} }
} }

View File

@ -3,6 +3,7 @@ using API.APIEndpointRecords;
using API.Schema; using API.Schema;
using API.Schema.NotificationConnectors; using API.Schema.NotificationConnectors;
using Asp.Versioning; using Asp.Versioning;
using log4net;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using static Microsoft.AspNetCore.Http.StatusCodes; using static Microsoft.AspNetCore.Http.StatusCodes;
@ -12,7 +13,7 @@ namespace API.Controllers;
[ApiController] [ApiController]
[Produces("application/json")] [Produces("application/json")]
[Route("v{v:apiVersion}/[controller]")] [Route("v{v:apiVersion}/[controller]")]
public class NotificationConnectorController(PgsqlContext context) : Controller public class NotificationConnectorController(PgsqlContext context, ILog Log) : Controller
{ {
/// <summary> /// <summary>
/// Gets all configured Notification-Connectors /// Gets all configured Notification-Connectors
@ -69,6 +70,7 @@ public class NotificationConnectorController(PgsqlContext context) : Controller
} }
catch (Exception e) catch (Exception e)
{ {
Log.Error(e);
return StatusCode(500, e.Message); return StatusCode(500, e.Message);
} }
} }
@ -209,6 +211,7 @@ public class NotificationConnectorController(PgsqlContext context) : Controller
} }
catch (Exception e) catch (Exception e)
{ {
Log.Error(e);
return StatusCode(500, e.Message); return StatusCode(500, e.Message);
} }
} }

View File

@ -1,14 +1,16 @@
using API.Schema; using API.Schema;
using Asp.Versioning; using Asp.Versioning;
using log4net;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using static Microsoft.AspNetCore.Http.StatusCodes; using static Microsoft.AspNetCore.Http.StatusCodes;
// ReSharper disable InconsistentNaming
namespace API.Controllers; namespace API.Controllers;
[ApiVersion(2)] [ApiVersion(2)]
[ApiController] [ApiController]
[Route("v{v:apiVersion}/[controller]")] [Route("v{v:apiVersion}/[controller]")]
public class QueryController(PgsqlContext context) : Controller public class QueryController(PgsqlContext context, ILog Log) : Controller
{ {
/// <summary> /// <summary>
/// Returns the Author-Information for Author-ID /// Returns the Author-Information for Author-ID
@ -32,13 +34,16 @@ public class QueryController(PgsqlContext context) : Controller
/// </summary> /// </summary>
/// <param name="AuthorId">Author-ID</param> /// <param name="AuthorId">Author-ID</param>
/// <response code="200"></response> /// <response code="200"></response>
/// <response code="404">Author not found</response>
[HttpGet("Mangas/WithAuthorId/{AuthorId}")] [HttpGet("Mangas/WithAuthorId/{AuthorId}")]
[ProducesResponseType<Manga[]>(Status200OK, "application/json")] [ProducesResponseType<Manga[]>(Status200OK, "application/json")]
public IActionResult GetMangaWithAuthorIds(string AuthorId) public IActionResult GetMangaWithAuthorIds(string AuthorId)
{ {
return Ok(context.Mangas.Where(m => m.AuthorIds.Contains(AuthorId))); if(context.Authors.Find(AuthorId) is not { } a)
return NotFound();
return Ok(context.Mangas.Where(m => m.Authors.Contains(a)));
} }
/*
/// <summary> /// <summary>
/// Returns Link-Information for Link-Id /// Returns Link-Information for Link-Id
/// </summary> /// </summary>
@ -71,18 +76,21 @@ public class QueryController(PgsqlContext context) : Controller
if (ret is null) if (ret is null)
return NotFound(); return NotFound();
return Ok(ret); return Ok(ret);
} }*/
/// <summary> /// <summary>
/// Returns all Manga with Tag /// Returns all Manga with Tag
/// </summary> /// </summary>
/// <param name="Tag"></param> /// <param name="Tag"></param>
/// <response code="200"></response> /// <response code="200"></response>
/// <response code="404">Tag not found</response>
[HttpGet("Mangas/WithTag/{Tag}")] [HttpGet("Mangas/WithTag/{Tag}")]
[ProducesResponseType<Manga[]>(Status200OK, "application/json")] [ProducesResponseType<Manga[]>(Status200OK, "application/json")]
public IActionResult GetMangasWithTag(string Tag) public IActionResult GetMangasWithTag(string Tag)
{ {
return Ok(context.Mangas.Where(m => m.Tags.Contains(Tag))); if(context.Tags.Find(Tag) is not { } t)
return NotFound();
return Ok(context.Mangas.Where(m => m.MangaTags.Contains(t)));
} }
/// <summary> /// <summary>

View File

@ -2,85 +2,53 @@ using API.Schema;
using API.Schema.Jobs; using API.Schema.Jobs;
using API.Schema.MangaConnectors; using API.Schema.MangaConnectors;
using Asp.Versioning; using Asp.Versioning;
using log4net;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using static Microsoft.AspNetCore.Http.StatusCodes; using static Microsoft.AspNetCore.Http.StatusCodes;
// ReSharper disable InconsistentNaming
namespace API.Controllers; namespace API.Controllers;
[ApiVersion(2)] [ApiVersion(2)]
[ApiController] [ApiController]
[Route("v{v:apiVersion}/[controller]")] [Route("v{v:apiVersion}/[controller]")]
public class SearchController(PgsqlContext context) : Controller public class SearchController(PgsqlContext context, ILog Log) : Controller
{ {
/// <summary>
/// Initiate a search for a Manga on all Connectors
/// </summary>
/// <param name="name">Name/Title of the Manga</param>
/// <response code="200"></response>
/// <response code="500">Error during Database Operation</response>
[HttpPost("Name")]
[ProducesResponseType<Manga[]>(Status200OK, "application/json")]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
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))
allManga.AddRange(contextMangaConnector.GetManga(name));
List<Manga> retMangas = new();
foreach ((Manga? manga, List<Author>? authors, List<MangaTag>? tags, List<Link>? links, List<MangaAltTitle>? altTitles) in allManga)
{
try
{
Manga? add = AddMangaToContext(manga, authors, tags, links, altTitles);
if(add is not null)
retMangas.Add(add);
}
catch (DbUpdateException e)
{
return StatusCode(500, e);
}
}
return Ok(retMangas.ToArray());
}
/// <summary> /// <summary>
/// Initiate a search for a Manga on a specific Connector /// Initiate a search for a Manga on a specific Connector
/// </summary> /// </summary>
/// <param name="MangaConnectorName">Manga-Connector-ID</param> /// <param name="MangaConnectorName"></param>
/// <param name="name">Name/Title of the Manga</param> /// <param name="Query"></param>
/// <response code="200"></response> /// <response code="200"></response>
/// <response code="404">MangaConnector with ID not found</response> /// <response code="404">MangaConnector with ID not found</response>
/// <response code="406">MangaConnector with ID is disabled</response> /// <response code="406">MangaConnector with ID is disabled</response>
/// <response code="500">Error during Database Operation</response> /// <response code="500">Error during Database Operation</response>
[HttpPost("{MangaConnectorName}")] [HttpPost("{MangaConnectorName}/{Query}")]
[ProducesResponseType<Manga[]>(Status200OK, "application/json")] [ProducesResponseType<Manga[]>(Status200OK, "application/json")]
[ProducesResponseType(Status404NotFound)] [ProducesResponseType(Status404NotFound)]
[ProducesResponseType(Status406NotAcceptable)] [ProducesResponseType(Status406NotAcceptable)]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")] [ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public IActionResult SearchManga(string MangaConnectorName, [FromBody]string name) public IActionResult SearchManga(string MangaConnectorName, string Query)
{ {
MangaConnector? connector = context.MangaConnectors.Find(MangaConnectorName); if(context.MangaConnectors.Find(MangaConnectorName) is not { } connector)
if (connector is null)
return NotFound(); return NotFound();
else if (connector.Enabled is false) else if (connector.Enabled is false)
return StatusCode(406); return StatusCode(Status406NotAcceptable);
(Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)[] mangas = connector.GetManga(name); Manga[] mangas = connector.SearchManga(Query);
List<Manga> retMangas = new(); List<Manga> retMangas = new();
foreach ((Manga? manga, List<Author>? authors, List<MangaTag>? tags, List<Link>? links, List<MangaAltTitle>? altTitles) in mangas) foreach (Manga manga in mangas)
{ {
try try
{ {
Manga? add = AddMangaToContext(manga, authors, tags, links, altTitles); if(AddMangaToContext(manga) is { } add)
if(add is not null)
retMangas.Add(add); retMangas.Add(add);
} }
catch (DbUpdateException e) catch (DbUpdateException e)
{ {
return StatusCode(500, e.Message); Log.Error(e);
return StatusCode(Status500InternalServerError, e.Message);
} }
} }
@ -104,98 +72,77 @@ public class SearchController(PgsqlContext context) : Controller
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")] [ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public IActionResult GetMangaFromUrl([FromBody]string url) public IActionResult GetMangaFromUrl([FromBody]string url)
{ {
List<MangaConnector> connectors = context.MangaConnectors.AsEnumerable().Where(c => c.ValidateUrl(url)).ToList(); List<MangaConnector> connectors = context.MangaConnectors.AsEnumerable().Where(c => c.UrlMatchesConnector(url)).ToList();
if (connectors.Count == 0) if (connectors.Count == 0)
return NotFound(); return NotFound();
else if (connectors.Count > 1) else if (connectors.Count > 1)
return StatusCode(Status300MultipleChoices); return StatusCode(Status300MultipleChoices);
(Manga manga, List<Author>? authors, List<MangaTag>? tags, List<Link>? links, List<MangaAltTitle>? altTitles)? x = connectors.First().GetMangaFromUrl(url); if(connectors.First().GetMangaFromUrl(url) is not { } manga)
if (x is null)
return BadRequest(); return BadRequest();
try try
{ {
Manga? add = AddMangaToContext(x.Value.manga, x.Value.authors, x.Value.tags, x.Value.links, x.Value.altTitles); if(AddMangaToContext(manga) is { } add)
if (add is not null)
return Ok(add); return Ok(add);
return StatusCode(500); return StatusCode(500);
} }
catch (DbUpdateException e) catch (DbUpdateException e)
{ {
Log.Error(e);
return StatusCode(500, e.Message); return StatusCode(500, e.Message);
} }
} }
private Manga? AddMangaToContext(Manga? manga, List<Author>? authors, List<MangaTag>? tags, List<Link>? links, private Manga? AddMangaToContext(Manga manga)
List<MangaAltTitle>? altTitles)
{ {
if (manga is null)
return null;
Manga? existing = context.Mangas.Find(manga.MangaId); Manga? existing = context.Mangas.Find(manga.MangaId);
if (tags is not null) IEnumerable<MangaTag> mergedTags = manga.MangaTags.Select(mt =>
{
IEnumerable<MangaTag> mergedTags = tags.Select(mt =>
{ {
MangaTag? inDb = context.Tags.Find(mt.Tag); MangaTag? inDb = context.Tags.Find(mt.Tag);
return inDb ?? mt; return inDb ?? mt;
}); });
manga.MangaTags = mergedTags.ToList(); manga.MangaTags = mergedTags.ToList();
IEnumerable<MangaTag> newTags = manga.MangaTags
.Where(mt => !context.Tags.Select(t => t.Tag).Contains(mt.Tag));
context.Tags.AddRange(newTags);
}
if (authors is not null) IEnumerable<Author> mergedAuthors = manga.Authors.Select(ma =>
{
IEnumerable<Author> mergedAuthors = authors.Select(ma =>
{ {
Author? inDb = context.Authors.Find(ma.AuthorId); Author? inDb = context.Authors.Find(ma.AuthorId);
return inDb ?? ma; return inDb ?? ma;
}); });
manga.Authors = mergedAuthors.ToList(); manga.Authors = mergedAuthors.ToList();
IEnumerable<Author> newAuthors = manga.Authors
.Where(ma => !context.Authors.Select(a => a.AuthorId).Contains(ma.AuthorId));
context.Authors.AddRange(newAuthors);
}
if (links is not null) /*
{ IEnumerable<Link> mergedLinks = manga.Links.Select(ml =>
IEnumerable<Link> mergedLinks = links.Select(ml =>
{ {
Link? inDb = context.Links.Find(ml.LinkId); Link? inDb = context.Links.Find(ml.LinkId);
return inDb ?? ml; return inDb ?? ml;
}); });
manga.Links = mergedLinks.ToList(); manga.Links = mergedLinks.ToList();
IEnumerable<Link> newLinks = manga.Links
.Where(ml => !context.Links.Select(l => l.LinkId).Contains(ml.LinkId));
context.Links.AddRange(newLinks);
}
if (altTitles is not null) IEnumerable<MangaAltTitle> mergedAltTitles = manga.AltTitles.Select(mat =>
{
IEnumerable<MangaAltTitle> mergedAltTitles = altTitles.Select(mat =>
{ {
MangaAltTitle? inDb = context.AltTitles.Find(mat.AltTitleId); MangaAltTitle? inDb = context.AltTitles.Find(mat.AltTitleId);
return inDb ?? mat; return inDb ?? mat;
}); });
manga.AltTitles = mergedAltTitles.ToList(); manga.AltTitles = mergedAltTitles.ToList();
IEnumerable<MangaAltTitle> newAltTitles = manga.AltTitles */
.Where(mat => !context.AltTitles.Select(at => at.AltTitleId).Contains(mat.AltTitleId)); try
context.AltTitles.AddRange(newAltTitles); {
}
existing?.UpdateWithInfo(manga);
if(existing is not null)
context.Mangas.Update(existing);
else
context.Mangas.Add(manga);
context.Jobs.Add(new DownloadMangaCoverJob(manga.MangaId));
context.Jobs.Add(new RetrieveChaptersJob(0, manga.MangaId));
if (context.Mangas.Find(manga.MangaId) is { } r)
{
context.Mangas.Remove(r);
context.SaveChanges(); context.SaveChanges();
return existing ?? manga; }
context.Mangas.Add(manga);
context.Jobs.Add(new DownloadMangaCoverJob(manga));
context.SaveChanges();
}
catch (DbUpdateException e)
{
Log.Error(e);
return null;
}
return manga;
} }
} }

View File

@ -2,6 +2,7 @@
using API.Schema; using API.Schema;
using API.Schema.Jobs; using API.Schema.Jobs;
using Asp.Versioning; using Asp.Versioning;
using log4net;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using static Microsoft.AspNetCore.Http.StatusCodes; using static Microsoft.AspNetCore.Http.StatusCodes;
@ -11,7 +12,7 @@ namespace API.Controllers;
[ApiVersion(2)] [ApiVersion(2)]
[ApiController] [ApiController]
[Route("v{v:apiVersion}/[controller]")] [Route("v{v:apiVersion}/[controller]")]
public class SettingsController(PgsqlContext context) : Controller public class SettingsController(PgsqlContext context, ILog Log) : Controller
{ {
/// <summary> /// <summary>
/// Get all Settings /// Get all Settings
@ -252,14 +253,16 @@ public class SettingsController(PgsqlContext context) : Controller
{ {
try try
{ {
Dictionary<Chapter, string> oldPaths = context.Chapters.ToDictionary(c => c, c => c.FullArchiveFilePath);
TrangaSettings.UpdateChapterNamingScheme(namingScheme); TrangaSettings.UpdateChapterNamingScheme(namingScheme);
MoveFileOrFolderJob[] newJobs = MoveFileOrFolderJob[] newJobs = oldPaths
context.Chapters.Where(c => c.Downloaded).Select(c => c.UpdateArchiveFileName()).Where(x => x != null).ToArray()!; .Select(kv => new MoveFileOrFolderJob(kv.Value, kv.Key.FullArchiveFilePath)).ToArray();
context.Jobs.AddRange(newJobs); context.Jobs.AddRange(newJobs);
return Ok(); return Ok();
} }
catch (Exception e) catch (Exception e)
{ {
Log.Error(e);
return StatusCode(500, e); return StatusCode(500, e);
} }
} }

View File

@ -1,42 +0,0 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace API.Migrations
{
/// <inheritdoc />
public partial class dev1603252 : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_Mangas_LocalLibraries_LibraryLocalLibraryId",
table: "Mangas");
migrationBuilder.AddForeignKey(
name: "FK_Mangas_LocalLibraries_LibraryLocalLibraryId",
table: "Mangas",
column: "LibraryLocalLibraryId",
principalTable: "LocalLibraries",
principalColumn: "LocalLibraryId",
onDelete: ReferentialAction.Restrict);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_Mangas_LocalLibraries_LibraryLocalLibraryId",
table: "Mangas");
migrationBuilder.AddForeignKey(
name: "FK_Mangas_LocalLibraries_LibraryLocalLibraryId",
table: "Mangas",
column: "LibraryLocalLibraryId",
principalTable: "LocalLibraries",
principalColumn: "LocalLibraryId",
onDelete: ReferentialAction.Cascade);
}
}
}

View File

@ -1,827 +0,0 @@
// <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("20250401001439_dev-010425-1")]
partial class dev0104251
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "9.0.3")
.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()
.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()
.HasMaxLength(64)
.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.PrimitiveCollection<string[]>("DependsOnJobsIds")
.HasMaxLength(64)
.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("JobId");
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()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("BaseUrl")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<byte>("LibraryType")
.HasColumnType("smallint");
b.HasKey("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()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("LinkUrl")
.IsRequired()
.HasMaxLength(2048)
.HasColumnType("character varying(2048)");
b.Property<string>("MangaId")
.HasColumnType("character varying(64)");
b.HasKey("LinkId");
b.HasIndex("MangaId");
b.ToTable("Links");
});
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")
.HasColumnType("text");
b.Property<string>("CoverUrl")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Description")
.IsRequired()
.HasColumnType("text");
b.Property<string>("DirectoryName")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("IdOnConnectorSite")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("character varying(128)");
b.Property<float>("IgnoreChapterBefore")
.HasColumnType("real");
b.Property<string>("LibraryLocalLibraryId")
.HasColumnType("character varying(64)");
b.Property<string>("MangaConnectorId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("OriginalLanguage")
.HasMaxLength(8)
.HasColumnType("character varying(8)");
b.Property<byte>("ReleaseStatus")
.HasColumnType("smallint");
b.Property<string>("WebsiteUrl")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<long>("Year")
.HasColumnType("bigint");
b.HasKey("MangaId");
b.HasIndex("LibraryLocalLibraryId");
b.HasIndex("MangaConnectorId");
b.ToTable("Mangas");
});
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()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
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()
.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.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()
.HasMaxLength(512)
.HasColumnType("character varying(512)");
b.Property<string>("Title")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("character varying(128)");
b.Property<byte>("Urgency")
.HasColumnType("smallint");
b.HasKey("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()
.HasMaxLength(4096)
.HasColumnType("character varying(4096)");
b.Property<Dictionary<string, string>>("Headers")
.IsRequired()
.HasColumnType("hstore");
b.Property<string>("HttpMethod")
.IsRequired()
.HasMaxLength(8)
.HasColumnType("character varying(8)");
b.Property<string>("Url")
.IsRequired()
.HasMaxLength(2048)
.HasColumnType("character varying(2048)");
b.HasKey("Name");
b.ToTable("NotificationConnectors");
});
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("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("MangaMangaTag", b =>
{
b.Property<string>("MangaId")
.HasColumnType("character varying(64)");
b.Property<string>("MangaTagsTag")
.HasColumnType("character varying(64)");
b.HasKey("MangaId", "MangaTagsTag");
b.HasIndex("MangaTagsTag");
b.ToTable("MangaMangaTag");
});
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.RetrieveChaptersJob", 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("RetrieveChaptersJob_MangaId");
});
b.HasDiscriminator().HasValue((byte)5);
});
modelBuilder.Entity("API.Schema.Jobs.UpdateFilesDownloadedJob", 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.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.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.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.Manganato", b =>
{
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
b.HasDiscriminator().HasValue("Manganato");
});
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", "ParentJob")
.WithMany()
.HasForeignKey("ParentJobId")
.OnDelete(DeleteBehavior.Cascade);
b.Navigation("ParentJob");
});
modelBuilder.Entity("API.Schema.Link", b =>
{
b.HasOne("API.Schema.Manga", null)
.WithMany("Links")
.HasForeignKey("MangaId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("API.Schema.Manga", b =>
{
b.HasOne("API.Schema.LocalLibrary", "Library")
.WithMany()
.HasForeignKey("LibraryLocalLibraryId")
.OnDelete(DeleteBehavior.Restrict);
b.HasOne("API.Schema.MangaConnectors.MangaConnector", "MangaConnector")
.WithMany()
.HasForeignKey("MangaConnectorId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Library");
b.Navigation("MangaConnector");
});
modelBuilder.Entity("API.Schema.MangaAltTitle", b =>
{
b.HasOne("API.Schema.Manga", null)
.WithMany("AltTitles")
.HasForeignKey("MangaId")
.OnDelete(DeleteBehavior.Cascade);
});
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("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("MangaMangaTag", b =>
{
b.HasOne("API.Schema.Manga", null)
.WithMany()
.HasForeignKey("MangaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("API.Schema.MangaTag", null)
.WithMany()
.HasForeignKey("MangaTagsTag")
.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.RetrieveChaptersJob", b =>
{
b.HasOne("API.Schema.Manga", "Manga")
.WithMany()
.HasForeignKey("MangaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Manga");
});
modelBuilder.Entity("API.Schema.Jobs.UpdateFilesDownloadedJob", b =>
{
b.HasOne("API.Schema.Manga", "Manga")
.WithMany()
.HasForeignKey("MangaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Manga");
});
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.Manga", b =>
{
b.Navigation("AltTitles");
b.Navigation("Links");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -1,98 +0,0 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace API.Migrations
{
/// <inheritdoc />
public partial class dev0104252Longer_Var_Chars : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<string>(
name: "WebsiteUrl",
table: "Mangas",
type: "character varying(512)",
maxLength: 512,
nullable: false,
oldClrType: typeof(string),
oldType: "character varying(256)",
oldMaxLength: 256);
migrationBuilder.AlterColumn<string>(
name: "Name",
table: "Mangas",
type: "character varying(512)",
maxLength: 512,
nullable: false,
oldClrType: typeof(string),
oldType: "character varying(256)",
oldMaxLength: 256);
migrationBuilder.AlterColumn<string>(
name: "IdOnConnectorSite",
table: "Mangas",
type: "character varying(256)",
maxLength: 256,
nullable: false,
oldClrType: typeof(string),
oldType: "character varying(128)",
oldMaxLength: 128);
migrationBuilder.AlterColumn<string>(
name: "DirectoryName",
table: "Mangas",
type: "character varying(1024)",
maxLength: 1024,
nullable: false,
oldClrType: typeof(string),
oldType: "character varying(256)",
oldMaxLength: 256);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<string>(
name: "WebsiteUrl",
table: "Mangas",
type: "character varying(256)",
maxLength: 256,
nullable: false,
oldClrType: typeof(string),
oldType: "character varying(512)",
oldMaxLength: 512);
migrationBuilder.AlterColumn<string>(
name: "Name",
table: "Mangas",
type: "character varying(256)",
maxLength: 256,
nullable: false,
oldClrType: typeof(string),
oldType: "character varying(512)",
oldMaxLength: 512);
migrationBuilder.AlterColumn<string>(
name: "IdOnConnectorSite",
table: "Mangas",
type: "character varying(128)",
maxLength: 128,
nullable: false,
oldClrType: typeof(string),
oldType: "character varying(256)",
oldMaxLength: 256);
migrationBuilder.AlterColumn<string>(
name: "DirectoryName",
table: "Mangas",
type: "character varying(256)",
maxLength: 256,
nullable: false,
oldClrType: typeof(string),
oldType: "character varying(1024)",
oldMaxLength: 1024);
}
}
}

View File

@ -1,762 +0,0 @@
// <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("20250402001438_dev-010425-4")]
partial class dev0104254
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "9.0.3")
.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()
.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()
.HasMaxLength(64)
.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.PrimitiveCollection<string[]>("DependsOnJobsIds")
.HasMaxLength(64)
.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("JobId");
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()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("BaseUrl")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<byte>("LibraryType")
.HasColumnType("smallint");
b.HasKey("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()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("LinkUrl")
.IsRequired()
.HasMaxLength(2048)
.HasColumnType("character varying(2048)");
b.Property<string>("MangaId")
.HasColumnType("character varying(64)");
b.HasKey("LinkId");
b.HasIndex("MangaId");
b.ToTable("Links");
});
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")
.HasColumnType("text");
b.Property<string>("CoverUrl")
.IsRequired()
.HasColumnType("text");
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>("IgnoreChapterBefore")
.HasColumnType("real");
b.Property<string>("LibraryLocalLibraryId")
.HasColumnType("character varying(64)");
b.Property<string>("MangaConnectorId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(512)
.HasColumnType("character varying(512)");
b.Property<string>("OriginalLanguage")
.HasMaxLength(8)
.HasColumnType("character varying(8)");
b.Property<byte>("ReleaseStatus")
.HasColumnType("smallint");
b.Property<string>("WebsiteUrl")
.IsRequired()
.HasMaxLength(512)
.HasColumnType("character varying(512)");
b.Property<long>("Year")
.HasColumnType("bigint");
b.HasKey("MangaId");
b.HasIndex("LibraryLocalLibraryId");
b.HasIndex("MangaConnectorId");
b.ToTable("Mangas");
});
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()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
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()
.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.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()
.HasMaxLength(512)
.HasColumnType("character varying(512)");
b.Property<string>("Title")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("character varying(128)");
b.Property<byte>("Urgency")
.HasColumnType("smallint");
b.HasKey("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()
.HasMaxLength(4096)
.HasColumnType("character varying(4096)");
b.Property<Dictionary<string, string>>("Headers")
.IsRequired()
.HasColumnType("hstore");
b.Property<string>("HttpMethod")
.IsRequired()
.HasMaxLength(8)
.HasColumnType("character varying(8)");
b.Property<string>("Url")
.IsRequired()
.HasMaxLength(2048)
.HasColumnType("character varying(2048)");
b.HasKey("Name");
b.ToTable("NotificationConnectors");
});
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("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("MangaMangaTag", b =>
{
b.Property<string>("MangaId")
.HasColumnType("character varying(64)");
b.Property<string>("MangaTagsTag")
.HasColumnType("character varying(64)");
b.HasKey("MangaId", "MangaTagsTag");
b.HasIndex("MangaTagsTag");
b.ToTable("MangaMangaTag");
});
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.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.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.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.RetrieveChaptersJob", b =>
{
b.HasBaseType("API.Schema.Jobs.Job");
b.Property<string>("MangaId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.ToTable("Jobs", t =>
{
t.Property("MangaId")
.HasColumnName("RetrieveChaptersJob_MangaId");
});
b.HasDiscriminator().HasValue((byte)5);
});
modelBuilder.Entity("API.Schema.Jobs.UpdateFilesDownloadedJob", b =>
{
b.HasBaseType("API.Schema.Jobs.Job");
b.Property<string>("MangaId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.ToTable("Jobs", t =>
{
t.Property("MangaId")
.HasColumnName("UpdateFilesDownloadedJob_MangaId");
});
b.HasDiscriminator().HasValue((byte)6);
});
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.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.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.Manganato", b =>
{
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
b.HasDiscriminator().HasValue("Manganato");
});
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", "ParentJob")
.WithMany()
.HasForeignKey("ParentJobId")
.OnDelete(DeleteBehavior.Cascade);
b.Navigation("ParentJob");
});
modelBuilder.Entity("API.Schema.Link", b =>
{
b.HasOne("API.Schema.Manga", null)
.WithMany("Links")
.HasForeignKey("MangaId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("API.Schema.Manga", b =>
{
b.HasOne("API.Schema.LocalLibrary", "Library")
.WithMany()
.HasForeignKey("LibraryLocalLibraryId")
.OnDelete(DeleteBehavior.Restrict);
b.HasOne("API.Schema.MangaConnectors.MangaConnector", "MangaConnector")
.WithMany()
.HasForeignKey("MangaConnectorId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Library");
b.Navigation("MangaConnector");
});
modelBuilder.Entity("API.Schema.MangaAltTitle", b =>
{
b.HasOne("API.Schema.Manga", null)
.WithMany("AltTitles")
.HasForeignKey("MangaId")
.OnDelete(DeleteBehavior.Cascade);
});
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("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("MangaMangaTag", b =>
{
b.HasOne("API.Schema.Manga", null)
.WithMany()
.HasForeignKey("MangaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("API.Schema.MangaTag", null)
.WithMany()
.HasForeignKey("MangaTagsTag")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
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.Manga", b =>
{
b.Navigation("AltTitles");
b.Navigation("Links");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -1,123 +0,0 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace API.Migrations
{
/// <inheritdoc />
public partial class dev0104254 : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_Jobs_Chapters_ChapterId",
table: "Jobs");
migrationBuilder.DropForeignKey(
name: "FK_Jobs_Mangas_DownloadAvailableChaptersJob_MangaId",
table: "Jobs");
migrationBuilder.DropForeignKey(
name: "FK_Jobs_Mangas_MangaId",
table: "Jobs");
migrationBuilder.DropForeignKey(
name: "FK_Jobs_Mangas_RetrieveChaptersJob_MangaId",
table: "Jobs");
migrationBuilder.DropForeignKey(
name: "FK_Jobs_Mangas_UpdateFilesDownloadedJob_MangaId",
table: "Jobs");
migrationBuilder.DropIndex(
name: "IX_Jobs_ChapterId",
table: "Jobs");
migrationBuilder.DropIndex(
name: "IX_Jobs_DownloadAvailableChaptersJob_MangaId",
table: "Jobs");
migrationBuilder.DropIndex(
name: "IX_Jobs_MangaId",
table: "Jobs");
migrationBuilder.DropIndex(
name: "IX_Jobs_RetrieveChaptersJob_MangaId",
table: "Jobs");
migrationBuilder.DropIndex(
name: "IX_Jobs_UpdateFilesDownloadedJob_MangaId",
table: "Jobs");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
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_RetrieveChaptersJob_MangaId",
table: "Jobs",
column: "RetrieveChaptersJob_MangaId");
migrationBuilder.CreateIndex(
name: "IX_Jobs_UpdateFilesDownloadedJob_MangaId",
table: "Jobs",
column: "UpdateFilesDownloadedJob_MangaId");
migrationBuilder.AddForeignKey(
name: "FK_Jobs_Chapters_ChapterId",
table: "Jobs",
column: "ChapterId",
principalTable: "Chapters",
principalColumn: "ChapterId",
onDelete: ReferentialAction.Cascade);
migrationBuilder.AddForeignKey(
name: "FK_Jobs_Mangas_DownloadAvailableChaptersJob_MangaId",
table: "Jobs",
column: "DownloadAvailableChaptersJob_MangaId",
principalTable: "Mangas",
principalColumn: "MangaId",
onDelete: ReferentialAction.Cascade);
migrationBuilder.AddForeignKey(
name: "FK_Jobs_Mangas_MangaId",
table: "Jobs",
column: "MangaId",
principalTable: "Mangas",
principalColumn: "MangaId",
onDelete: ReferentialAction.Cascade);
migrationBuilder.AddForeignKey(
name: "FK_Jobs_Mangas_RetrieveChaptersJob_MangaId",
table: "Jobs",
column: "RetrieveChaptersJob_MangaId",
principalTable: "Mangas",
principalColumn: "MangaId",
onDelete: ReferentialAction.Cascade);
migrationBuilder.AddForeignKey(
name: "FK_Jobs_Mangas_UpdateFilesDownloadedJob_MangaId",
table: "Jobs",
column: "UpdateFilesDownloadedJob_MangaId",
principalTable: "Mangas",
principalColumn: "MangaId",
onDelete: ReferentialAction.Cascade);
}
}
}

View File

@ -13,8 +13,8 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
namespace API.Migrations namespace API.Migrations
{ {
[DbContext(typeof(PgsqlContext))] [DbContext(typeof(PgsqlContext))]
[Migration("20250401162026_dev-010425-2-Longer_Var_Chars")] [Migration("20250509033915_Initial")]
partial class dev0104252Longer_Var_Chars partial class Initial
{ {
/// <inheritdoc /> /// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder) protected override void BuildTargetModel(ModelBuilder modelBuilder)
@ -64,7 +64,6 @@ namespace API.Migrations
b.Property<string>("ParentMangaId") b.Property<string>("ParentMangaId")
.IsRequired() .IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)"); .HasColumnType("character varying(64)");
b.Property<string>("Title") b.Property<string>("Title")
@ -92,10 +91,6 @@ namespace API.Migrations
.HasMaxLength(64) .HasMaxLength(64)
.HasColumnType("character varying(64)"); .HasColumnType("character varying(64)");
b.PrimitiveCollection<string[]>("DependsOnJobsIds")
.HasMaxLength(64)
.HasColumnType("text[]");
b.Property<bool>("Enabled") b.Property<bool>("Enabled")
.HasColumnType("boolean"); .HasColumnType("boolean");
@ -106,6 +101,7 @@ namespace API.Migrations
.HasColumnType("timestamp with time zone"); .HasColumnType("timestamp with time zone");
b.Property<string>("ParentJobId") b.Property<string>("ParentJobId")
.IsRequired()
.HasMaxLength(64) .HasMaxLength(64)
.HasColumnType("character varying(64)"); .HasColumnType("character varying(64)");
@ -154,32 +150,6 @@ namespace API.Migrations
b.UseTphMappingStrategy(); b.UseTphMappingStrategy();
}); });
modelBuilder.Entity("API.Schema.Link", b =>
{
b.Property<string>("LinkId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("LinkProvider")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("LinkUrl")
.IsRequired()
.HasMaxLength(2048)
.HasColumnType("character varying(2048)");
b.Property<string>("MangaId")
.HasColumnType("character varying(64)");
b.HasKey("LinkId");
b.HasIndex("MangaId");
b.ToTable("Links");
});
modelBuilder.Entity("API.Schema.LocalLibrary", b => modelBuilder.Entity("API.Schema.LocalLibrary", b =>
{ {
b.Property<string>("LocalLibraryId") b.Property<string>("LocalLibraryId")
@ -208,11 +178,13 @@ namespace API.Migrations
.HasColumnType("character varying(64)"); .HasColumnType("character varying(64)");
b.Property<string>("CoverFileNameInCache") b.Property<string>("CoverFileNameInCache")
.HasColumnType("text"); .HasMaxLength(512)
.HasColumnType("character varying(512)");
b.Property<string>("CoverUrl") b.Property<string>("CoverUrl")
.IsRequired() .IsRequired()
.HasColumnType("text"); .HasMaxLength(512)
.HasColumnType("character varying(512)");
b.Property<string>("Description") b.Property<string>("Description")
.IsRequired() .IsRequired()
@ -228,17 +200,19 @@ namespace API.Migrations
.HasMaxLength(256) .HasMaxLength(256)
.HasColumnType("character varying(256)"); .HasColumnType("character varying(256)");
b.Property<float>("IgnoreChapterBefore") b.Property<float>("IgnoreChaptersBefore")
.HasColumnType("real"); .HasColumnType("real");
b.Property<string>("LibraryLocalLibraryId") b.Property<string>("LibraryId")
.HasColumnType("character varying(64)");
b.Property<string>("MangaConnectorId")
.IsRequired() .IsRequired()
.HasMaxLength(64) .HasMaxLength(64)
.HasColumnType("character varying(64)"); .HasColumnType("character varying(64)");
b.Property<string>("MangaConnectorName")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("character varying(32)");
b.Property<string>("Name") b.Property<string>("Name")
.IsRequired() .IsRequired()
.HasMaxLength(512) .HasMaxLength(512)
@ -261,39 +235,13 @@ namespace API.Migrations
b.HasKey("MangaId"); b.HasKey("MangaId");
b.HasIndex("LibraryLocalLibraryId"); b.HasIndex("LibraryId");
b.HasIndex("MangaConnectorId"); b.HasIndex("MangaConnectorName");
b.ToTable("Mangas"); b.ToTable("Mangas");
}); });
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()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.HasKey("AltTitleId");
b.HasIndex("MangaId");
b.ToTable("AltTitles");
});
modelBuilder.Entity("API.Schema.MangaConnectors.MangaConnector", b => modelBuilder.Entity("API.Schema.MangaConnectors.MangaConnector", b =>
{ {
b.Property<string>("Name") b.Property<string>("Name")
@ -395,19 +343,19 @@ namespace API.Migrations
b.ToTable("NotificationConnectors"); b.ToTable("NotificationConnectors");
}); });
modelBuilder.Entity("AuthorManga", b => modelBuilder.Entity("AuthorToManga", b =>
{ {
b.Property<string>("AuthorsAuthorId") b.Property<string>("AuthorIds")
.HasColumnType("character varying(64)"); .HasColumnType("character varying(64)");
b.Property<string>("MangaId") b.Property<string>("MangaIds")
.HasColumnType("character varying(64)"); .HasColumnType("character varying(64)");
b.HasKey("AuthorsAuthorId", "MangaId"); b.HasKey("AuthorIds", "MangaIds");
b.HasIndex("MangaId"); b.HasIndex("MangaIds");
b.ToTable("AuthorManga"); b.ToTable("AuthorToManga");
}); });
modelBuilder.Entity("JobJob", b => modelBuilder.Entity("JobJob", b =>
@ -425,19 +373,19 @@ namespace API.Migrations
b.ToTable("JobJob"); b.ToTable("JobJob");
}); });
modelBuilder.Entity("MangaMangaTag", b => modelBuilder.Entity("MangaTagToManga", b =>
{ {
b.Property<string>("MangaId") b.Property<string>("MangaTagIds")
.HasColumnType("character varying(64)"); .HasColumnType("character varying(64)");
b.Property<string>("MangaTagsTag") b.Property<string>("MangaIds")
.HasColumnType("character varying(64)"); .HasColumnType("character varying(64)");
b.HasKey("MangaId", "MangaTagsTag"); b.HasKey("MangaTagIds", "MangaIds");
b.HasIndex("MangaTagsTag"); b.HasIndex("MangaIds");
b.ToTable("MangaMangaTag"); b.ToTable("MangaTagToManga");
}); });
modelBuilder.Entity("API.Schema.Jobs.DownloadAvailableChaptersJob", b => modelBuilder.Entity("API.Schema.Jobs.DownloadAvailableChaptersJob", b =>
@ -505,10 +453,42 @@ namespace API.Migrations
b.HasDiscriminator().HasValue((byte)3); 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 => modelBuilder.Entity("API.Schema.Jobs.RetrieveChaptersJob", b =>
{ {
b.HasBaseType("API.Schema.Jobs.Job"); b.HasBaseType("API.Schema.Jobs.Job");
b.Property<string>("Language")
.IsRequired()
.HasMaxLength(8)
.HasColumnType("character varying(8)");
b.Property<string>("MangaId") b.Property<string>("MangaId")
.IsRequired() .IsRequired()
.HasMaxLength(64) .HasMaxLength(64)
@ -545,26 +525,6 @@ namespace API.Migrations
b.HasDiscriminator().HasValue((byte)6); b.HasDiscriminator().HasValue((byte)6);
}); });
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 => modelBuilder.Entity("API.Schema.LibraryConnectors.Kavita", b =>
{ {
b.HasBaseType("API.Schema.LibraryConnectors.LibraryConnector"); b.HasBaseType("API.Schema.LibraryConnectors.LibraryConnector");
@ -579,20 +539,6 @@ namespace API.Migrations
b.HasDiscriminator().HasValue((byte)0); 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.Global", b => modelBuilder.Entity("API.Schema.MangaConnectors.Global", b =>
{ {
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector"); b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
@ -607,52 +553,10 @@ namespace API.Migrations
b.HasDiscriminator().HasValue("MangaDex"); 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.Manganato", b =>
{
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
b.HasDiscriminator().HasValue("Manganato");
});
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 => modelBuilder.Entity("API.Schema.Chapter", b =>
{ {
b.HasOne("API.Schema.Manga", "ParentManga") b.HasOne("API.Schema.Manga", "ParentManga")
.WithMany() .WithMany("Chapters")
.HasForeignKey("ParentMangaId") .HasForeignKey("ParentMangaId")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
@ -670,51 +574,100 @@ namespace API.Migrations
b.Navigation("ParentJob"); b.Navigation("ParentJob");
}); });
modelBuilder.Entity("API.Schema.Link", b =>
{
b.HasOne("API.Schema.Manga", null)
.WithMany("Links")
.HasForeignKey("MangaId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("API.Schema.Manga", b => modelBuilder.Entity("API.Schema.Manga", b =>
{ {
b.HasOne("API.Schema.LocalLibrary", "Library") b.HasOne("API.Schema.LocalLibrary", "Library")
.WithMany() .WithMany()
.HasForeignKey("LibraryLocalLibraryId") .HasForeignKey("LibraryId")
.OnDelete(DeleteBehavior.Restrict); .OnDelete(DeleteBehavior.SetNull)
.IsRequired();
b.HasOne("API.Schema.MangaConnectors.MangaConnector", "MangaConnector") b.HasOne("API.Schema.MangaConnectors.MangaConnector", "MangaConnector")
.WithMany() .WithMany()
.HasForeignKey("MangaConnectorId") .HasForeignKey("MangaConnectorName")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .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("Links");
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("AltTitles");
b1.WithOwner()
.HasForeignKey("MangaId");
});
b.Navigation("AltTitles");
b.Navigation("Library"); b.Navigation("Library");
b.Navigation("Links");
b.Navigation("MangaConnector"); b.Navigation("MangaConnector");
}); });
modelBuilder.Entity("API.Schema.MangaAltTitle", b => modelBuilder.Entity("AuthorToManga", b =>
{
b.HasOne("API.Schema.Manga", null)
.WithMany("AltTitles")
.HasForeignKey("MangaId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("AuthorManga", b =>
{ {
b.HasOne("API.Schema.Author", null) b.HasOne("API.Schema.Author", null)
.WithMany() .WithMany()
.HasForeignKey("AuthorsAuthorId") .HasForeignKey("AuthorIds")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
b.HasOne("API.Schema.Manga", null) b.HasOne("API.Schema.Manga", null)
.WithMany() .WithMany()
.HasForeignKey("MangaId") .HasForeignKey("MangaIds")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
}); });
@ -734,17 +687,17 @@ namespace API.Migrations
.IsRequired(); .IsRequired();
}); });
modelBuilder.Entity("MangaMangaTag", b => modelBuilder.Entity("MangaTagToManga", b =>
{ {
b.HasOne("API.Schema.Manga", null) b.HasOne("API.Schema.Manga", null)
.WithMany() .WithMany()
.HasForeignKey("MangaId") .HasForeignKey("MangaIds")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
b.HasOne("API.Schema.MangaTag", null) b.HasOne("API.Schema.MangaTag", null)
.WithMany() .WithMany()
.HasForeignKey("MangaTagsTag") .HasForeignKey("MangaTagIds")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
}); });
@ -782,6 +735,25 @@ namespace API.Migrations
b.Navigation("Chapter"); 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 => modelBuilder.Entity("API.Schema.Jobs.RetrieveChaptersJob", b =>
{ {
b.HasOne("API.Schema.Manga", "Manga") b.HasOne("API.Schema.Manga", "Manga")
@ -804,22 +776,9 @@ namespace API.Migrations
b.Navigation("Manga"); b.Navigation("Manga");
}); });
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.Manga", b => modelBuilder.Entity("API.Schema.Manga", b =>
{ {
b.Navigation("AltTitles"); b.Navigation("Chapters");
b.Navigation("Links");
}); });
#pragma warning restore 612, 618 #pragma warning restore 612, 618
} }

View File

@ -7,7 +7,7 @@ using Microsoft.EntityFrameworkCore.Migrations;
namespace API.Migrations namespace API.Migrations
{ {
/// <inheritdoc /> /// <inheritdoc />
public partial class dev160325Initial : Migration public partial class Initial : Migration
{ {
/// <inheritdoc /> /// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder) protected override void Up(MigrationBuilder migrationBuilder)
@ -115,32 +115,32 @@ namespace API.Migrations
columns: table => new columns: table => new
{ {
MangaId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false), MangaId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
IdOnConnectorSite = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: false), IdOnConnectorSite = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
Name = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false), Name = table.Column<string>(type: "character varying(512)", maxLength: 512, nullable: false),
Description = table.Column<string>(type: "text", nullable: false), Description = table.Column<string>(type: "text", nullable: false),
WebsiteUrl = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false), WebsiteUrl = table.Column<string>(type: "character varying(512)", maxLength: 512, nullable: false),
CoverUrl = table.Column<string>(type: "text", nullable: false), CoverUrl = table.Column<string>(type: "character varying(512)", maxLength: 512, nullable: false),
CoverFileNameInCache = table.Column<string>(type: "text", nullable: true),
Year = table.Column<long>(type: "bigint", nullable: false),
OriginalLanguage = table.Column<string>(type: "character varying(8)", maxLength: 8, nullable: false),
ReleaseStatus = table.Column<byte>(type: "smallint", nullable: false), ReleaseStatus = table.Column<byte>(type: "smallint", nullable: false),
DirectoryName = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false), LibraryId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
LibraryLocalLibraryId = table.Column<string>(type: "character varying(64)", nullable: true), MangaConnectorName = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: false),
IgnoreChapterBefore = table.Column<float>(type: "real", nullable: false), IgnoreChaptersBefore = table.Column<float>(type: "real", nullable: false),
MangaConnectorId = table.Column<string>(type: "character varying(64)", maxLength: 64, 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),
OriginalLanguage = table.Column<string>(type: "character varying(8)", maxLength: 8, nullable: true)
}, },
constraints: table => constraints: table =>
{ {
table.PrimaryKey("PK_Mangas", x => x.MangaId); table.PrimaryKey("PK_Mangas", x => x.MangaId);
table.ForeignKey( table.ForeignKey(
name: "FK_Mangas_LocalLibraries_LibraryLocalLibraryId", name: "FK_Mangas_LocalLibraries_LibraryId",
column: x => x.LibraryLocalLibraryId, column: x => x.LibraryId,
principalTable: "LocalLibraries", principalTable: "LocalLibraries",
principalColumn: "LocalLibraryId", principalColumn: "LocalLibraryId",
onDelete: ReferentialAction.Cascade); onDelete: ReferentialAction.SetNull);
table.ForeignKey( table.ForeignKey(
name: "FK_Mangas_MangaConnectors_MangaConnectorId", name: "FK_Mangas_MangaConnectors_MangaConnectorName",
column: x => x.MangaConnectorId, column: x => x.MangaConnectorName,
principalTable: "MangaConnectors", principalTable: "MangaConnectors",
principalColumn: "Name", principalColumn: "Name",
onDelete: ReferentialAction.Cascade); onDelete: ReferentialAction.Cascade);
@ -153,7 +153,7 @@ namespace API.Migrations
AltTitleId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false), AltTitleId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
Language = table.Column<string>(type: "character varying(8)", maxLength: 8, 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), Title = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
MangaId = table.Column<string>(type: "character varying(64)", nullable: true) MangaId = table.Column<string>(type: "character varying(64)", nullable: false)
}, },
constraints: table => constraints: table =>
{ {
@ -167,24 +167,24 @@ namespace API.Migrations
}); });
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
name: "AuthorManga", name: "AuthorToManga",
columns: table => new columns: table => new
{ {
AuthorsAuthorId = table.Column<string>(type: "character varying(64)", nullable: false), AuthorIds = table.Column<string>(type: "character varying(64)", nullable: false),
MangaId = table.Column<string>(type: "character varying(64)", nullable: false) MangaIds = table.Column<string>(type: "character varying(64)", nullable: false)
}, },
constraints: table => constraints: table =>
{ {
table.PrimaryKey("PK_AuthorManga", x => new { x.AuthorsAuthorId, x.MangaId }); table.PrimaryKey("PK_AuthorToManga", x => new { x.AuthorIds, x.MangaIds });
table.ForeignKey( table.ForeignKey(
name: "FK_AuthorManga_Authors_AuthorsAuthorId", name: "FK_AuthorToManga_Authors_AuthorIds",
column: x => x.AuthorsAuthorId, column: x => x.AuthorIds,
principalTable: "Authors", principalTable: "Authors",
principalColumn: "AuthorId", principalColumn: "AuthorId",
onDelete: ReferentialAction.Cascade); onDelete: ReferentialAction.Cascade);
table.ForeignKey( table.ForeignKey(
name: "FK_AuthorManga_Mangas_MangaId", name: "FK_AuthorToManga_Mangas_MangaIds",
column: x => x.MangaId, column: x => x.MangaIds,
principalTable: "Mangas", principalTable: "Mangas",
principalColumn: "MangaId", principalColumn: "MangaId",
onDelete: ReferentialAction.Cascade); onDelete: ReferentialAction.Cascade);
@ -195,13 +195,13 @@ namespace API.Migrations
columns: table => new columns: table => new
{ {
ChapterId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false), ChapterId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
ParentMangaId = table.Column<string>(type: "character varying(64)", nullable: false),
VolumeNumber = table.Column<int>(type: "integer", nullable: true), VolumeNumber = table.Column<int>(type: "integer", nullable: true),
ChapterNumber = table.Column<string>(type: "character varying(10)", maxLength: 10, nullable: false), ChapterNumber = table.Column<string>(type: "character varying(10)", maxLength: 10, nullable: false),
Url = table.Column<string>(type: "character varying(2048)", maxLength: 2048, nullable: false), Url = table.Column<string>(type: "character varying(2048)", maxLength: 2048, nullable: false),
Title = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true), Title = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
FileName = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false), FileName = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
Downloaded = table.Column<bool>(type: "boolean", nullable: false), Downloaded = table.Column<bool>(type: "boolean", nullable: false)
ParentMangaId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false)
}, },
constraints: table => constraints: table =>
{ {
@ -221,7 +221,7 @@ namespace API.Migrations
LinkId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false), LinkId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
LinkProvider = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false), LinkProvider = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
LinkUrl = table.Column<string>(type: "character varying(2048)", maxLength: 2048, nullable: false), LinkUrl = table.Column<string>(type: "character varying(2048)", maxLength: 2048, nullable: false),
MangaId = table.Column<string>(type: "character varying(64)", nullable: true) MangaId = table.Column<string>(type: "character varying(64)", nullable: false)
}, },
constraints: table => constraints: table =>
{ {
@ -235,24 +235,24 @@ namespace API.Migrations
}); });
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
name: "MangaMangaTag", name: "MangaTagToManga",
columns: table => new columns: table => new
{ {
MangaId = table.Column<string>(type: "character varying(64)", nullable: false), MangaTagIds = table.Column<string>(type: "character varying(64)", nullable: false),
MangaTagsTag = table.Column<string>(type: "character varying(64)", nullable: false) MangaIds = table.Column<string>(type: "character varying(64)", nullable: false)
}, },
constraints: table => constraints: table =>
{ {
table.PrimaryKey("PK_MangaMangaTag", x => new { x.MangaId, x.MangaTagsTag }); table.PrimaryKey("PK_MangaTagToManga", x => new { x.MangaTagIds, x.MangaIds });
table.ForeignKey( table.ForeignKey(
name: "FK_MangaMangaTag_Mangas_MangaId", name: "FK_MangaTagToManga_Mangas_MangaIds",
column: x => x.MangaId, column: x => x.MangaIds,
principalTable: "Mangas", principalTable: "Mangas",
principalColumn: "MangaId", principalColumn: "MangaId",
onDelete: ReferentialAction.Cascade); onDelete: ReferentialAction.Cascade);
table.ForeignKey( table.ForeignKey(
name: "FK_MangaMangaTag_Tags_MangaTagsTag", name: "FK_MangaTagToManga_Tags_MangaTagIds",
column: x => x.MangaTagsTag, column: x => x.MangaTagIds,
principalTable: "Tags", principalTable: "Tags",
principalColumn: "Tag", principalColumn: "Tag",
onDelete: ReferentialAction.Cascade); onDelete: ReferentialAction.Cascade);
@ -263,8 +263,7 @@ namespace API.Migrations
columns: table => new columns: table => new
{ {
JobId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false), JobId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
ParentJobId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: true), ParentJobId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
DependsOnJobsIds = table.Column<string[]>(type: "text[]", maxLength: 64, nullable: true),
JobType = table.Column<byte>(type: "smallint", nullable: false), JobType = table.Column<byte>(type: "smallint", nullable: false),
RecurrenceMs = table.Column<decimal>(type: "numeric(20,0)", nullable: false), RecurrenceMs = table.Column<decimal>(type: "numeric(20,0)", nullable: false),
LastExecution = table.Column<DateTime>(type: "timestamp with time zone", nullable: false), LastExecution = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
@ -275,9 +274,11 @@ namespace API.Migrations
ChapterId = 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), FromLocation = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
ToLocation = 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), RetrieveChaptersJob_MangaId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: true),
UpdateFilesDownloadedJob_MangaId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: true), Language = table.Column<string>(type: "character varying(8)", maxLength: 8, nullable: true),
UpdateMetadataJob_MangaId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: true) UpdateFilesDownloadedJob_MangaId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: true)
}, },
constraints: table => constraints: table =>
{ {
@ -294,6 +295,12 @@ namespace API.Migrations
principalTable: "Jobs", principalTable: "Jobs",
principalColumn: "JobId", principalColumn: "JobId",
onDelete: ReferentialAction.Cascade); onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_Jobs_LocalLibraries_ToLibraryId",
column: x => x.ToLibraryId,
principalTable: "LocalLibraries",
principalColumn: "LocalLibraryId",
onDelete: ReferentialAction.Cascade);
table.ForeignKey( table.ForeignKey(
name: "FK_Jobs_Mangas_DownloadAvailableChaptersJob_MangaId", name: "FK_Jobs_Mangas_DownloadAvailableChaptersJob_MangaId",
column: x => x.DownloadAvailableChaptersJob_MangaId, column: x => x.DownloadAvailableChaptersJob_MangaId,
@ -306,6 +313,12 @@ namespace API.Migrations
principalTable: "Mangas", principalTable: "Mangas",
principalColumn: "MangaId", principalColumn: "MangaId",
onDelete: ReferentialAction.Cascade); 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( table.ForeignKey(
name: "FK_Jobs_Mangas_RetrieveChaptersJob_MangaId", name: "FK_Jobs_Mangas_RetrieveChaptersJob_MangaId",
column: x => x.RetrieveChaptersJob_MangaId, column: x => x.RetrieveChaptersJob_MangaId,
@ -318,12 +331,6 @@ namespace API.Migrations
principalTable: "Mangas", principalTable: "Mangas",
principalColumn: "MangaId", principalColumn: "MangaId",
onDelete: ReferentialAction.Cascade); onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_Jobs_Mangas_UpdateMetadataJob_MangaId",
column: x => x.UpdateMetadataJob_MangaId,
principalTable: "Mangas",
principalColumn: "MangaId",
onDelete: ReferentialAction.Cascade);
}); });
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
@ -356,9 +363,9 @@ namespace API.Migrations
column: "MangaId"); column: "MangaId");
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_AuthorManga_MangaId", name: "IX_AuthorToManga_MangaIds",
table: "AuthorManga", table: "AuthorToManga",
column: "MangaId"); column: "MangaIds");
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_Chapters_ParentMangaId", name: "IX_Chapters_ParentMangaId",
@ -385,6 +392,11 @@ namespace API.Migrations
table: "Jobs", table: "Jobs",
column: "MangaId"); column: "MangaId");
migrationBuilder.CreateIndex(
name: "IX_Jobs_MoveMangaLibraryJob_MangaId",
table: "Jobs",
column: "MoveMangaLibraryJob_MangaId");
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_Jobs_ParentJobId", name: "IX_Jobs_ParentJobId",
table: "Jobs", table: "Jobs",
@ -395,35 +407,35 @@ namespace API.Migrations
table: "Jobs", table: "Jobs",
column: "RetrieveChaptersJob_MangaId"); column: "RetrieveChaptersJob_MangaId");
migrationBuilder.CreateIndex(
name: "IX_Jobs_ToLibraryId",
table: "Jobs",
column: "ToLibraryId");
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_Jobs_UpdateFilesDownloadedJob_MangaId", name: "IX_Jobs_UpdateFilesDownloadedJob_MangaId",
table: "Jobs", table: "Jobs",
column: "UpdateFilesDownloadedJob_MangaId"); column: "UpdateFilesDownloadedJob_MangaId");
migrationBuilder.CreateIndex(
name: "IX_Jobs_UpdateMetadataJob_MangaId",
table: "Jobs",
column: "UpdateMetadataJob_MangaId");
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_Links_MangaId", name: "IX_Links_MangaId",
table: "Links", table: "Links",
column: "MangaId"); column: "MangaId");
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_MangaMangaTag_MangaTagsTag", name: "IX_Mangas_LibraryId",
table: "MangaMangaTag", table: "Mangas",
column: "MangaTagsTag"); column: "LibraryId");
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_Mangas_LibraryLocalLibraryId", name: "IX_Mangas_MangaConnectorName",
table: "Mangas", table: "Mangas",
column: "LibraryLocalLibraryId"); column: "MangaConnectorName");
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_Mangas_MangaConnectorId", name: "IX_MangaTagToManga_MangaIds",
table: "Mangas", table: "MangaTagToManga",
column: "MangaConnectorId"); column: "MangaIds");
} }
/// <inheritdoc /> /// <inheritdoc />
@ -433,7 +445,7 @@ namespace API.Migrations
name: "AltTitles"); name: "AltTitles");
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "AuthorManga"); name: "AuthorToManga");
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "JobJob"); name: "JobJob");
@ -445,7 +457,7 @@ namespace API.Migrations
name: "Links"); name: "Links");
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "MangaMangaTag"); name: "MangaTagToManga");
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "NotificationConnectors"); name: "NotificationConnectors");

View File

@ -13,8 +13,8 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
namespace API.Migrations namespace API.Migrations
{ {
[DbContext(typeof(PgsqlContext))] [DbContext(typeof(PgsqlContext))]
[Migration("20250401234456_dev-010425-3-ParentJobOwnership")] [Migration("20250509034207_Initial-2")]
partial class dev0104253ParentJobOwnership partial class Initial2
{ {
/// <inheritdoc /> /// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder) protected override void BuildTargetModel(ModelBuilder modelBuilder)
@ -64,7 +64,6 @@ namespace API.Migrations
b.Property<string>("ParentMangaId") b.Property<string>("ParentMangaId")
.IsRequired() .IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)"); .HasColumnType("character varying(64)");
b.Property<string>("Title") b.Property<string>("Title")
@ -92,10 +91,6 @@ namespace API.Migrations
.HasMaxLength(64) .HasMaxLength(64)
.HasColumnType("character varying(64)"); .HasColumnType("character varying(64)");
b.PrimitiveCollection<string[]>("DependsOnJobsIds")
.HasMaxLength(64)
.HasColumnType("text[]");
b.Property<bool>("Enabled") b.Property<bool>("Enabled")
.HasColumnType("boolean"); .HasColumnType("boolean");
@ -106,6 +101,7 @@ namespace API.Migrations
.HasColumnType("timestamp with time zone"); .HasColumnType("timestamp with time zone");
b.Property<string>("ParentJobId") b.Property<string>("ParentJobId")
.IsRequired()
.HasMaxLength(64) .HasMaxLength(64)
.HasColumnType("character varying(64)"); .HasColumnType("character varying(64)");
@ -154,32 +150,6 @@ namespace API.Migrations
b.UseTphMappingStrategy(); b.UseTphMappingStrategy();
}); });
modelBuilder.Entity("API.Schema.Link", b =>
{
b.Property<string>("LinkId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("LinkProvider")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("LinkUrl")
.IsRequired()
.HasMaxLength(2048)
.HasColumnType("character varying(2048)");
b.Property<string>("MangaId")
.HasColumnType("character varying(64)");
b.HasKey("LinkId");
b.HasIndex("MangaId");
b.ToTable("Links");
});
modelBuilder.Entity("API.Schema.LocalLibrary", b => modelBuilder.Entity("API.Schema.LocalLibrary", b =>
{ {
b.Property<string>("LocalLibraryId") b.Property<string>("LocalLibraryId")
@ -208,11 +178,13 @@ namespace API.Migrations
.HasColumnType("character varying(64)"); .HasColumnType("character varying(64)");
b.Property<string>("CoverFileNameInCache") b.Property<string>("CoverFileNameInCache")
.HasColumnType("text"); .HasMaxLength(512)
.HasColumnType("character varying(512)");
b.Property<string>("CoverUrl") b.Property<string>("CoverUrl")
.IsRequired() .IsRequired()
.HasColumnType("text"); .HasMaxLength(512)
.HasColumnType("character varying(512)");
b.Property<string>("Description") b.Property<string>("Description")
.IsRequired() .IsRequired()
@ -228,17 +200,19 @@ namespace API.Migrations
.HasMaxLength(256) .HasMaxLength(256)
.HasColumnType("character varying(256)"); .HasColumnType("character varying(256)");
b.Property<float>("IgnoreChapterBefore") b.Property<float>("IgnoreChaptersBefore")
.HasColumnType("real"); .HasColumnType("real");
b.Property<string>("LibraryLocalLibraryId") b.Property<string>("LibraryId")
.HasColumnType("character varying(64)");
b.Property<string>("MangaConnectorId")
.IsRequired() .IsRequired()
.HasMaxLength(64) .HasMaxLength(64)
.HasColumnType("character varying(64)"); .HasColumnType("character varying(64)");
b.Property<string>("MangaConnectorName")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("character varying(32)");
b.Property<string>("Name") b.Property<string>("Name")
.IsRequired() .IsRequired()
.HasMaxLength(512) .HasMaxLength(512)
@ -261,39 +235,13 @@ namespace API.Migrations
b.HasKey("MangaId"); b.HasKey("MangaId");
b.HasIndex("LibraryLocalLibraryId"); b.HasIndex("LibraryId");
b.HasIndex("MangaConnectorId"); b.HasIndex("MangaConnectorName");
b.ToTable("Mangas"); b.ToTable("Mangas");
}); });
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()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.HasKey("AltTitleId");
b.HasIndex("MangaId");
b.ToTable("AltTitles");
});
modelBuilder.Entity("API.Schema.MangaConnectors.MangaConnector", b => modelBuilder.Entity("API.Schema.MangaConnectors.MangaConnector", b =>
{ {
b.Property<string>("Name") b.Property<string>("Name")
@ -395,19 +343,19 @@ namespace API.Migrations
b.ToTable("NotificationConnectors"); b.ToTable("NotificationConnectors");
}); });
modelBuilder.Entity("AuthorManga", b => modelBuilder.Entity("AuthorToManga", b =>
{ {
b.Property<string>("AuthorsAuthorId") b.Property<string>("AuthorIds")
.HasColumnType("character varying(64)"); .HasColumnType("character varying(64)");
b.Property<string>("MangaId") b.Property<string>("MangaIds")
.HasColumnType("character varying(64)"); .HasColumnType("character varying(64)");
b.HasKey("AuthorsAuthorId", "MangaId"); b.HasKey("AuthorIds", "MangaIds");
b.HasIndex("MangaId"); b.HasIndex("MangaIds");
b.ToTable("AuthorManga"); b.ToTable("AuthorToManga");
}); });
modelBuilder.Entity("JobJob", b => modelBuilder.Entity("JobJob", b =>
@ -425,19 +373,19 @@ namespace API.Migrations
b.ToTable("JobJob"); b.ToTable("JobJob");
}); });
modelBuilder.Entity("MangaMangaTag", b => modelBuilder.Entity("MangaTagToManga", b =>
{ {
b.Property<string>("MangaId") b.Property<string>("MangaTagIds")
.HasColumnType("character varying(64)"); .HasColumnType("character varying(64)");
b.Property<string>("MangaTagsTag") b.Property<string>("MangaIds")
.HasColumnType("character varying(64)"); .HasColumnType("character varying(64)");
b.HasKey("MangaId", "MangaTagsTag"); b.HasKey("MangaTagIds", "MangaIds");
b.HasIndex("MangaTagsTag"); b.HasIndex("MangaIds");
b.ToTable("MangaMangaTag"); b.ToTable("MangaTagToManga");
}); });
modelBuilder.Entity("API.Schema.Jobs.DownloadAvailableChaptersJob", b => modelBuilder.Entity("API.Schema.Jobs.DownloadAvailableChaptersJob", b =>
@ -505,10 +453,42 @@ namespace API.Migrations
b.HasDiscriminator().HasValue((byte)3); 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 => modelBuilder.Entity("API.Schema.Jobs.RetrieveChaptersJob", b =>
{ {
b.HasBaseType("API.Schema.Jobs.Job"); b.HasBaseType("API.Schema.Jobs.Job");
b.Property<string>("Language")
.IsRequired()
.HasMaxLength(8)
.HasColumnType("character varying(8)");
b.Property<string>("MangaId") b.Property<string>("MangaId")
.IsRequired() .IsRequired()
.HasMaxLength(64) .HasMaxLength(64)
@ -545,26 +525,6 @@ namespace API.Migrations
b.HasDiscriminator().HasValue((byte)6); b.HasDiscriminator().HasValue((byte)6);
}); });
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 => modelBuilder.Entity("API.Schema.LibraryConnectors.Kavita", b =>
{ {
b.HasBaseType("API.Schema.LibraryConnectors.LibraryConnector"); b.HasBaseType("API.Schema.LibraryConnectors.LibraryConnector");
@ -579,20 +539,6 @@ namespace API.Migrations
b.HasDiscriminator().HasValue((byte)0); 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.Global", b => modelBuilder.Entity("API.Schema.MangaConnectors.Global", b =>
{ {
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector"); b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
@ -607,52 +553,10 @@ namespace API.Migrations
b.HasDiscriminator().HasValue("MangaDex"); 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.Manganato", b =>
{
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
b.HasDiscriminator().HasValue("Manganato");
});
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 => modelBuilder.Entity("API.Schema.Chapter", b =>
{ {
b.HasOne("API.Schema.Manga", "ParentManga") b.HasOne("API.Schema.Manga", "ParentManga")
.WithMany() .WithMany("Chapters")
.HasForeignKey("ParentMangaId") .HasForeignKey("ParentMangaId")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
@ -670,51 +574,100 @@ namespace API.Migrations
b.Navigation("ParentJob"); b.Navigation("ParentJob");
}); });
modelBuilder.Entity("API.Schema.Link", b =>
{
b.HasOne("API.Schema.Manga", null)
.WithMany("Links")
.HasForeignKey("MangaId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("API.Schema.Manga", b => modelBuilder.Entity("API.Schema.Manga", b =>
{ {
b.HasOne("API.Schema.LocalLibrary", "Library") b.HasOne("API.Schema.LocalLibrary", "Library")
.WithMany() .WithMany()
.HasForeignKey("LibraryLocalLibraryId") .HasForeignKey("LibraryId")
.OnDelete(DeleteBehavior.Restrict); .OnDelete(DeleteBehavior.SetNull)
.IsRequired();
b.HasOne("API.Schema.MangaConnectors.MangaConnector", "MangaConnector") b.HasOne("API.Schema.MangaConnectors.MangaConnector", "MangaConnector")
.WithMany() .WithMany()
.HasForeignKey("MangaConnectorId") .HasForeignKey("MangaConnectorName")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .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("Links");
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("AltTitles");
b1.WithOwner()
.HasForeignKey("MangaId");
});
b.Navigation("AltTitles");
b.Navigation("Library"); b.Navigation("Library");
b.Navigation("Links");
b.Navigation("MangaConnector"); b.Navigation("MangaConnector");
}); });
modelBuilder.Entity("API.Schema.MangaAltTitle", b => modelBuilder.Entity("AuthorToManga", b =>
{
b.HasOne("API.Schema.Manga", null)
.WithMany("AltTitles")
.HasForeignKey("MangaId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("AuthorManga", b =>
{ {
b.HasOne("API.Schema.Author", null) b.HasOne("API.Schema.Author", null)
.WithMany() .WithMany()
.HasForeignKey("AuthorsAuthorId") .HasForeignKey("AuthorIds")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
b.HasOne("API.Schema.Manga", null) b.HasOne("API.Schema.Manga", null)
.WithMany() .WithMany()
.HasForeignKey("MangaId") .HasForeignKey("MangaIds")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
}); });
@ -734,17 +687,17 @@ namespace API.Migrations
.IsRequired(); .IsRequired();
}); });
modelBuilder.Entity("MangaMangaTag", b => modelBuilder.Entity("MangaTagToManga", b =>
{ {
b.HasOne("API.Schema.Manga", null) b.HasOne("API.Schema.Manga", null)
.WithMany() .WithMany()
.HasForeignKey("MangaId") .HasForeignKey("MangaIds")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
b.HasOne("API.Schema.MangaTag", null) b.HasOne("API.Schema.MangaTag", null)
.WithMany() .WithMany()
.HasForeignKey("MangaTagsTag") .HasForeignKey("MangaTagIds")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
}); });
@ -782,6 +735,25 @@ namespace API.Migrations
b.Navigation("Chapter"); 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 => modelBuilder.Entity("API.Schema.Jobs.RetrieveChaptersJob", b =>
{ {
b.HasOne("API.Schema.Manga", "Manga") b.HasOne("API.Schema.Manga", "Manga")
@ -804,22 +776,9 @@ namespace API.Migrations
b.Navigation("Manga"); b.Navigation("Manga");
}); });
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.Manga", b => modelBuilder.Entity("API.Schema.Manga", b =>
{ {
b.Navigation("AltTitles"); b.Navigation("Chapters");
b.Navigation("Links");
}); });
#pragma warning restore 612, 618 #pragma warning restore 612, 618
} }

View File

@ -5,7 +5,7 @@
namespace API.Migrations namespace API.Migrations
{ {
/// <inheritdoc /> /// <inheritdoc />
public partial class dev0104253ParentJobOwnership : Migration public partial class Initial2 : Migration
{ {
/// <inheritdoc /> /// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder) protected override void Up(MigrationBuilder migrationBuilder)

View File

@ -13,8 +13,8 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
namespace API.Migrations namespace API.Migrations
{ {
[DbContext(typeof(PgsqlContext))] [DbContext(typeof(PgsqlContext))]
[Migration("20250316150158_dev-160325-2")] [Migration("20250509035413_Initial-3")]
partial class dev1603252 partial class Initial3
{ {
/// <inheritdoc /> /// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder) protected override void BuildTargetModel(ModelBuilder modelBuilder)
@ -64,7 +64,6 @@ namespace API.Migrations
b.Property<string>("ParentMangaId") b.Property<string>("ParentMangaId")
.IsRequired() .IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)"); .HasColumnType("character varying(64)");
b.Property<string>("Title") b.Property<string>("Title")
@ -92,10 +91,6 @@ namespace API.Migrations
.HasMaxLength(64) .HasMaxLength(64)
.HasColumnType("character varying(64)"); .HasColumnType("character varying(64)");
b.PrimitiveCollection<string[]>("DependsOnJobsIds")
.HasMaxLength(64)
.HasColumnType("text[]");
b.Property<bool>("Enabled") b.Property<bool>("Enabled")
.HasColumnType("boolean"); .HasColumnType("boolean");
@ -106,6 +101,7 @@ namespace API.Migrations
.HasColumnType("timestamp with time zone"); .HasColumnType("timestamp with time zone");
b.Property<string>("ParentJobId") b.Property<string>("ParentJobId")
.IsRequired()
.HasMaxLength(64) .HasMaxLength(64)
.HasColumnType("character varying(64)"); .HasColumnType("character varying(64)");
@ -154,32 +150,6 @@ namespace API.Migrations
b.UseTphMappingStrategy(); b.UseTphMappingStrategy();
}); });
modelBuilder.Entity("API.Schema.Link", b =>
{
b.Property<string>("LinkId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("LinkProvider")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("LinkUrl")
.IsRequired()
.HasMaxLength(2048)
.HasColumnType("character varying(2048)");
b.Property<string>("MangaId")
.HasColumnType("character varying(64)");
b.HasKey("LinkId");
b.HasIndex("MangaId");
b.ToTable("Links");
});
modelBuilder.Entity("API.Schema.LocalLibrary", b => modelBuilder.Entity("API.Schema.LocalLibrary", b =>
{ {
b.Property<string>("LocalLibraryId") b.Property<string>("LocalLibraryId")
@ -208,11 +178,13 @@ namespace API.Migrations
.HasColumnType("character varying(64)"); .HasColumnType("character varying(64)");
b.Property<string>("CoverFileNameInCache") b.Property<string>("CoverFileNameInCache")
.HasColumnType("text"); .HasMaxLength(512)
.HasColumnType("character varying(512)");
b.Property<string>("CoverUrl") b.Property<string>("CoverUrl")
.IsRequired() .IsRequired()
.HasColumnType("text"); .HasMaxLength(512)
.HasColumnType("character varying(512)");
b.Property<string>("Description") b.Property<string>("Description")
.IsRequired() .IsRequired()
@ -220,32 +192,33 @@ namespace API.Migrations
b.Property<string>("DirectoryName") b.Property<string>("DirectoryName")
.IsRequired() .IsRequired()
.HasMaxLength(256) .HasMaxLength(1024)
.HasColumnType("character varying(256)"); .HasColumnType("character varying(1024)");
b.Property<string>("IdOnConnectorSite") b.Property<string>("IdOnConnectorSite")
.IsRequired() .IsRequired()
.HasMaxLength(128) .HasMaxLength(256)
.HasColumnType("character varying(128)"); .HasColumnType("character varying(256)");
b.Property<float>("IgnoreChapterBefore") b.Property<float>("IgnoreChaptersBefore")
.HasColumnType("real"); .HasColumnType("real");
b.Property<string>("LibraryLocalLibraryId") b.Property<string>("LibraryId")
.HasColumnType("character varying(64)");
b.Property<string>("MangaConnectorId")
.IsRequired() .IsRequired()
.HasMaxLength(64) .HasMaxLength(64)
.HasColumnType("character varying(64)"); .HasColumnType("character varying(64)");
b.Property<string>("MangaConnectorName")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("character varying(32)");
b.Property<string>("Name") b.Property<string>("Name")
.IsRequired() .IsRequired()
.HasMaxLength(256) .HasMaxLength(512)
.HasColumnType("character varying(256)"); .HasColumnType("character varying(512)");
b.Property<string>("OriginalLanguage") b.Property<string>("OriginalLanguage")
.IsRequired()
.HasMaxLength(8) .HasMaxLength(8)
.HasColumnType("character varying(8)"); .HasColumnType("character varying(8)");
@ -254,47 +227,21 @@ namespace API.Migrations
b.Property<string>("WebsiteUrl") b.Property<string>("WebsiteUrl")
.IsRequired() .IsRequired()
.HasMaxLength(256) .HasMaxLength(512)
.HasColumnType("character varying(256)"); .HasColumnType("character varying(512)");
b.Property<long>("Year") b.Property<long>("Year")
.HasColumnType("bigint"); .HasColumnType("bigint");
b.HasKey("MangaId"); b.HasKey("MangaId");
b.HasIndex("LibraryLocalLibraryId"); b.HasIndex("LibraryId");
b.HasIndex("MangaConnectorId"); b.HasIndex("MangaConnectorName");
b.ToTable("Mangas"); b.ToTable("Mangas");
}); });
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()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.HasKey("AltTitleId");
b.HasIndex("MangaId");
b.ToTable("AltTitles");
});
modelBuilder.Entity("API.Schema.MangaConnectors.MangaConnector", b => modelBuilder.Entity("API.Schema.MangaConnectors.MangaConnector", b =>
{ {
b.Property<string>("Name") b.Property<string>("Name")
@ -396,19 +343,19 @@ namespace API.Migrations
b.ToTable("NotificationConnectors"); b.ToTable("NotificationConnectors");
}); });
modelBuilder.Entity("AuthorManga", b => modelBuilder.Entity("AuthorToManga", b =>
{ {
b.Property<string>("AuthorsAuthorId") b.Property<string>("AuthorIds")
.HasColumnType("character varying(64)"); .HasColumnType("character varying(64)");
b.Property<string>("MangaId") b.Property<string>("MangaIds")
.HasColumnType("character varying(64)"); .HasColumnType("character varying(64)");
b.HasKey("AuthorsAuthorId", "MangaId"); b.HasKey("AuthorIds", "MangaIds");
b.HasIndex("MangaId"); b.HasIndex("MangaIds");
b.ToTable("AuthorManga"); b.ToTable("AuthorToManga");
}); });
modelBuilder.Entity("JobJob", b => modelBuilder.Entity("JobJob", b =>
@ -426,19 +373,19 @@ namespace API.Migrations
b.ToTable("JobJob"); b.ToTable("JobJob");
}); });
modelBuilder.Entity("MangaMangaTag", b => modelBuilder.Entity("MangaTagToManga", b =>
{ {
b.Property<string>("MangaId") b.Property<string>("MangaTagIds")
.HasColumnType("character varying(64)"); .HasColumnType("character varying(64)");
b.Property<string>("MangaTagsTag") b.Property<string>("MangaIds")
.HasColumnType("character varying(64)"); .HasColumnType("character varying(64)");
b.HasKey("MangaId", "MangaTagsTag"); b.HasKey("MangaTagIds", "MangaIds");
b.HasIndex("MangaTagsTag"); b.HasIndex("MangaIds");
b.ToTable("MangaMangaTag"); b.ToTable("MangaTagToManga");
}); });
modelBuilder.Entity("API.Schema.Jobs.DownloadAvailableChaptersJob", b => modelBuilder.Entity("API.Schema.Jobs.DownloadAvailableChaptersJob", b =>
@ -506,10 +453,42 @@ namespace API.Migrations
b.HasDiscriminator().HasValue((byte)3); 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 => modelBuilder.Entity("API.Schema.Jobs.RetrieveChaptersJob", b =>
{ {
b.HasBaseType("API.Schema.Jobs.Job"); b.HasBaseType("API.Schema.Jobs.Job");
b.Property<string>("Language")
.IsRequired()
.HasMaxLength(8)
.HasColumnType("character varying(8)");
b.Property<string>("MangaId") b.Property<string>("MangaId")
.IsRequired() .IsRequired()
.HasMaxLength(64) .HasMaxLength(64)
@ -546,26 +525,6 @@ namespace API.Migrations
b.HasDiscriminator().HasValue((byte)6); b.HasDiscriminator().HasValue((byte)6);
}); });
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 => modelBuilder.Entity("API.Schema.LibraryConnectors.Kavita", b =>
{ {
b.HasBaseType("API.Schema.LibraryConnectors.LibraryConnector"); b.HasBaseType("API.Schema.LibraryConnectors.LibraryConnector");
@ -580,18 +539,11 @@ namespace API.Migrations
b.HasDiscriminator().HasValue((byte)0); b.HasDiscriminator().HasValue((byte)0);
}); });
modelBuilder.Entity("API.Schema.MangaConnectors.AsuraToon", b => modelBuilder.Entity("API.Schema.MangaConnectors.Global", b =>
{ {
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector"); b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
b.HasDiscriminator().HasValue("AsuraToon"); b.HasDiscriminator().HasValue("Global");
});
modelBuilder.Entity("API.Schema.MangaConnectors.Bato", b =>
{
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
b.HasDiscriminator().HasValue("Bato");
}); });
modelBuilder.Entity("API.Schema.MangaConnectors.MangaDex", b => modelBuilder.Entity("API.Schema.MangaConnectors.MangaDex", b =>
@ -601,52 +553,10 @@ namespace API.Migrations
b.HasDiscriminator().HasValue("MangaDex"); 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.Manganato", b =>
{
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
b.HasDiscriminator().HasValue("Manganato");
});
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 => modelBuilder.Entity("API.Schema.Chapter", b =>
{ {
b.HasOne("API.Schema.Manga", "ParentManga") b.HasOne("API.Schema.Manga", "ParentManga")
.WithMany() .WithMany("Chapters")
.HasForeignKey("ParentMangaId") .HasForeignKey("ParentMangaId")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
@ -664,51 +574,100 @@ namespace API.Migrations
b.Navigation("ParentJob"); b.Navigation("ParentJob");
}); });
modelBuilder.Entity("API.Schema.Link", b =>
{
b.HasOne("API.Schema.Manga", null)
.WithMany("Links")
.HasForeignKey("MangaId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("API.Schema.Manga", b => modelBuilder.Entity("API.Schema.Manga", b =>
{ {
b.HasOne("API.Schema.LocalLibrary", "Library") b.HasOne("API.Schema.LocalLibrary", "Library")
.WithMany() .WithMany()
.HasForeignKey("LibraryLocalLibraryId") .HasForeignKey("LibraryId")
.OnDelete(DeleteBehavior.Restrict); .OnDelete(DeleteBehavior.SetNull)
.IsRequired();
b.HasOne("API.Schema.MangaConnectors.MangaConnector", "MangaConnector") b.HasOne("API.Schema.MangaConnectors.MangaConnector", "MangaConnector")
.WithMany() .WithMany()
.HasForeignKey("MangaConnectorId") .HasForeignKey("MangaConnectorName")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .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("Library");
b.Navigation("Links");
b.Navigation("MangaConnector"); b.Navigation("MangaConnector");
}); });
modelBuilder.Entity("API.Schema.MangaAltTitle", b => modelBuilder.Entity("AuthorToManga", b =>
{
b.HasOne("API.Schema.Manga", null)
.WithMany("AltTitles")
.HasForeignKey("MangaId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("AuthorManga", b =>
{ {
b.HasOne("API.Schema.Author", null) b.HasOne("API.Schema.Author", null)
.WithMany() .WithMany()
.HasForeignKey("AuthorsAuthorId") .HasForeignKey("AuthorIds")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
b.HasOne("API.Schema.Manga", null) b.HasOne("API.Schema.Manga", null)
.WithMany() .WithMany()
.HasForeignKey("MangaId") .HasForeignKey("MangaIds")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
}); });
@ -728,17 +687,17 @@ namespace API.Migrations
.IsRequired(); .IsRequired();
}); });
modelBuilder.Entity("MangaMangaTag", b => modelBuilder.Entity("MangaTagToManga", b =>
{ {
b.HasOne("API.Schema.Manga", null) b.HasOne("API.Schema.Manga", null)
.WithMany() .WithMany()
.HasForeignKey("MangaId") .HasForeignKey("MangaIds")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
b.HasOne("API.Schema.MangaTag", null) b.HasOne("API.Schema.MangaTag", null)
.WithMany() .WithMany()
.HasForeignKey("MangaTagsTag") .HasForeignKey("MangaTagIds")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
}); });
@ -776,6 +735,25 @@ namespace API.Migrations
b.Navigation("Chapter"); 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 => modelBuilder.Entity("API.Schema.Jobs.RetrieveChaptersJob", b =>
{ {
b.HasOne("API.Schema.Manga", "Manga") b.HasOne("API.Schema.Manga", "Manga")
@ -798,22 +776,9 @@ namespace API.Migrations
b.Navigation("Manga"); b.Navigation("Manga");
}); });
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.Manga", b => modelBuilder.Entity("API.Schema.Manga", b =>
{ {
b.Navigation("AltTitles"); b.Navigation("Chapters");
b.Navigation("Links");
}); });
#pragma warning restore 612, 618 #pragma warning restore 612, 618
} }

View File

@ -0,0 +1,130 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace API.Migrations
{
/// <inheritdoc />
public partial class Initial3 : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "AltTitles");
migrationBuilder.DropTable(
name: "Links");
migrationBuilder.CreateTable(
name: "Link",
columns: table => new
{
LinkId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
LinkProvider = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
LinkUrl = table.Column<string>(type: "character varying(2048)", maxLength: 2048, nullable: false),
MangaId = table.Column<string>(type: "character varying(64)", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Link", x => x.LinkId);
table.ForeignKey(
name: "FK_Link_Mangas_MangaId",
column: x => x.MangaId,
principalTable: "Mangas",
principalColumn: "MangaId",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "MangaAltTitle",
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)
},
constraints: table =>
{
table.PrimaryKey("PK_MangaAltTitle", x => x.AltTitleId);
table.ForeignKey(
name: "FK_MangaAltTitle_Mangas_MangaId",
column: x => x.MangaId,
principalTable: "Mangas",
principalColumn: "MangaId",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_Link_MangaId",
table: "Link",
column: "MangaId");
migrationBuilder.CreateIndex(
name: "IX_MangaAltTitle_MangaId",
table: "MangaAltTitle",
column: "MangaId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Link");
migrationBuilder.DropTable(
name: "MangaAltTitle");
migrationBuilder.CreateTable(
name: "AltTitles",
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),
MangaId = table.Column<string>(type: "character varying(64)", nullable: false),
Title = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_AltTitles", x => x.AltTitleId);
table.ForeignKey(
name: "FK_AltTitles_Mangas_MangaId",
column: x => x.MangaId,
principalTable: "Mangas",
principalColumn: "MangaId",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "Links",
columns: table => new
{
LinkId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
LinkProvider = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
LinkUrl = table.Column<string>(type: "character varying(2048)", maxLength: 2048, nullable: false),
MangaId = table.Column<string>(type: "character varying(64)", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Links", x => x.LinkId);
table.ForeignKey(
name: "FK_Links_Mangas_MangaId",
column: x => x.MangaId,
principalTable: "Mangas",
principalColumn: "MangaId",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_AltTitles_MangaId",
table: "AltTitles",
column: "MangaId");
migrationBuilder.CreateIndex(
name: "IX_Links_MangaId",
table: "Links",
column: "MangaId");
}
}
}

View File

@ -13,8 +13,8 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
namespace API.Migrations namespace API.Migrations
{ {
[DbContext(typeof(PgsqlContext))] [DbContext(typeof(PgsqlContext))]
[Migration("20250316143014_dev-160325-Initial")] [Migration("20250509035606_Initial-4")]
partial class dev160325Initial partial class Initial4
{ {
/// <inheritdoc /> /// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder) protected override void BuildTargetModel(ModelBuilder modelBuilder)
@ -64,7 +64,6 @@ namespace API.Migrations
b.Property<string>("ParentMangaId") b.Property<string>("ParentMangaId")
.IsRequired() .IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)"); .HasColumnType("character varying(64)");
b.Property<string>("Title") b.Property<string>("Title")
@ -92,10 +91,6 @@ namespace API.Migrations
.HasMaxLength(64) .HasMaxLength(64)
.HasColumnType("character varying(64)"); .HasColumnType("character varying(64)");
b.PrimitiveCollection<string[]>("DependsOnJobsIds")
.HasMaxLength(64)
.HasColumnType("text[]");
b.Property<bool>("Enabled") b.Property<bool>("Enabled")
.HasColumnType("boolean"); .HasColumnType("boolean");
@ -106,6 +101,7 @@ namespace API.Migrations
.HasColumnType("timestamp with time zone"); .HasColumnType("timestamp with time zone");
b.Property<string>("ParentJobId") b.Property<string>("ParentJobId")
.IsRequired()
.HasMaxLength(64) .HasMaxLength(64)
.HasColumnType("character varying(64)"); .HasColumnType("character varying(64)");
@ -154,32 +150,6 @@ namespace API.Migrations
b.UseTphMappingStrategy(); b.UseTphMappingStrategy();
}); });
modelBuilder.Entity("API.Schema.Link", b =>
{
b.Property<string>("LinkId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("LinkProvider")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("LinkUrl")
.IsRequired()
.HasMaxLength(2048)
.HasColumnType("character varying(2048)");
b.Property<string>("MangaId")
.HasColumnType("character varying(64)");
b.HasKey("LinkId");
b.HasIndex("MangaId");
b.ToTable("Links");
});
modelBuilder.Entity("API.Schema.LocalLibrary", b => modelBuilder.Entity("API.Schema.LocalLibrary", b =>
{ {
b.Property<string>("LocalLibraryId") b.Property<string>("LocalLibraryId")
@ -208,11 +178,13 @@ namespace API.Migrations
.HasColumnType("character varying(64)"); .HasColumnType("character varying(64)");
b.Property<string>("CoverFileNameInCache") b.Property<string>("CoverFileNameInCache")
.HasColumnType("text"); .HasMaxLength(512)
.HasColumnType("character varying(512)");
b.Property<string>("CoverUrl") b.Property<string>("CoverUrl")
.IsRequired() .IsRequired()
.HasColumnType("text"); .HasMaxLength(512)
.HasColumnType("character varying(512)");
b.Property<string>("Description") b.Property<string>("Description")
.IsRequired() .IsRequired()
@ -220,32 +192,32 @@ namespace API.Migrations
b.Property<string>("DirectoryName") b.Property<string>("DirectoryName")
.IsRequired() .IsRequired()
.HasMaxLength(256) .HasMaxLength(1024)
.HasColumnType("character varying(256)"); .HasColumnType("character varying(1024)");
b.Property<string>("IdOnConnectorSite") b.Property<string>("IdOnConnectorSite")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("character varying(128)");
b.Property<float>("IgnoreChapterBefore")
.HasColumnType("real");
b.Property<string>("LibraryLocalLibraryId")
.HasColumnType("character varying(64)");
b.Property<string>("MangaConnectorId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("Name")
.IsRequired() .IsRequired()
.HasMaxLength(256) .HasMaxLength(256)
.HasColumnType("character varying(256)"); .HasColumnType("character varying(256)");
b.Property<string>("OriginalLanguage") b.Property<float>("IgnoreChaptersBefore")
.HasColumnType("real");
b.Property<string>("LibraryId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("MangaConnectorName")
.IsRequired() .IsRequired()
.HasMaxLength(32)
.HasColumnType("character varying(32)");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(512)
.HasColumnType("character varying(512)");
b.Property<string>("OriginalLanguage")
.HasMaxLength(8) .HasMaxLength(8)
.HasColumnType("character varying(8)"); .HasColumnType("character varying(8)");
@ -254,47 +226,21 @@ namespace API.Migrations
b.Property<string>("WebsiteUrl") b.Property<string>("WebsiteUrl")
.IsRequired() .IsRequired()
.HasMaxLength(256) .HasMaxLength(512)
.HasColumnType("character varying(256)"); .HasColumnType("character varying(512)");
b.Property<long>("Year") b.Property<long>("Year")
.HasColumnType("bigint"); .HasColumnType("bigint");
b.HasKey("MangaId"); b.HasKey("MangaId");
b.HasIndex("LibraryLocalLibraryId"); b.HasIndex("LibraryId");
b.HasIndex("MangaConnectorId"); b.HasIndex("MangaConnectorName");
b.ToTable("Mangas"); b.ToTable("Mangas");
}); });
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()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.HasKey("AltTitleId");
b.HasIndex("MangaId");
b.ToTable("AltTitles");
});
modelBuilder.Entity("API.Schema.MangaConnectors.MangaConnector", b => modelBuilder.Entity("API.Schema.MangaConnectors.MangaConnector", b =>
{ {
b.Property<string>("Name") b.Property<string>("Name")
@ -396,19 +342,19 @@ namespace API.Migrations
b.ToTable("NotificationConnectors"); b.ToTable("NotificationConnectors");
}); });
modelBuilder.Entity("AuthorManga", b => modelBuilder.Entity("AuthorToManga", b =>
{ {
b.Property<string>("AuthorsAuthorId") b.Property<string>("AuthorIds")
.HasColumnType("character varying(64)"); .HasColumnType("character varying(64)");
b.Property<string>("MangaId") b.Property<string>("MangaIds")
.HasColumnType("character varying(64)"); .HasColumnType("character varying(64)");
b.HasKey("AuthorsAuthorId", "MangaId"); b.HasKey("AuthorIds", "MangaIds");
b.HasIndex("MangaId"); b.HasIndex("MangaIds");
b.ToTable("AuthorManga"); b.ToTable("AuthorToManga");
}); });
modelBuilder.Entity("JobJob", b => modelBuilder.Entity("JobJob", b =>
@ -426,19 +372,19 @@ namespace API.Migrations
b.ToTable("JobJob"); b.ToTable("JobJob");
}); });
modelBuilder.Entity("MangaMangaTag", b => modelBuilder.Entity("MangaTagToManga", b =>
{ {
b.Property<string>("MangaId") b.Property<string>("MangaTagIds")
.HasColumnType("character varying(64)"); .HasColumnType("character varying(64)");
b.Property<string>("MangaTagsTag") b.Property<string>("MangaIds")
.HasColumnType("character varying(64)"); .HasColumnType("character varying(64)");
b.HasKey("MangaId", "MangaTagsTag"); b.HasKey("MangaTagIds", "MangaIds");
b.HasIndex("MangaTagsTag"); b.HasIndex("MangaIds");
b.ToTable("MangaMangaTag"); b.ToTable("MangaTagToManga");
}); });
modelBuilder.Entity("API.Schema.Jobs.DownloadAvailableChaptersJob", b => modelBuilder.Entity("API.Schema.Jobs.DownloadAvailableChaptersJob", b =>
@ -506,10 +452,42 @@ namespace API.Migrations
b.HasDiscriminator().HasValue((byte)3); 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 => modelBuilder.Entity("API.Schema.Jobs.RetrieveChaptersJob", b =>
{ {
b.HasBaseType("API.Schema.Jobs.Job"); b.HasBaseType("API.Schema.Jobs.Job");
b.Property<string>("Language")
.IsRequired()
.HasMaxLength(8)
.HasColumnType("character varying(8)");
b.Property<string>("MangaId") b.Property<string>("MangaId")
.IsRequired() .IsRequired()
.HasMaxLength(64) .HasMaxLength(64)
@ -546,26 +524,6 @@ namespace API.Migrations
b.HasDiscriminator().HasValue((byte)6); b.HasDiscriminator().HasValue((byte)6);
}); });
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 => modelBuilder.Entity("API.Schema.LibraryConnectors.Kavita", b =>
{ {
b.HasBaseType("API.Schema.LibraryConnectors.LibraryConnector"); b.HasBaseType("API.Schema.LibraryConnectors.LibraryConnector");
@ -580,18 +538,11 @@ namespace API.Migrations
b.HasDiscriminator().HasValue((byte)0); b.HasDiscriminator().HasValue((byte)0);
}); });
modelBuilder.Entity("API.Schema.MangaConnectors.AsuraToon", b => modelBuilder.Entity("API.Schema.MangaConnectors.Global", b =>
{ {
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector"); b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
b.HasDiscriminator().HasValue("AsuraToon"); b.HasDiscriminator().HasValue("Global");
});
modelBuilder.Entity("API.Schema.MangaConnectors.Bato", b =>
{
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
b.HasDiscriminator().HasValue("Bato");
}); });
modelBuilder.Entity("API.Schema.MangaConnectors.MangaDex", b => modelBuilder.Entity("API.Schema.MangaConnectors.MangaDex", b =>
@ -601,52 +552,10 @@ namespace API.Migrations
b.HasDiscriminator().HasValue("MangaDex"); 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.Manganato", b =>
{
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
b.HasDiscriminator().HasValue("Manganato");
});
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 => modelBuilder.Entity("API.Schema.Chapter", b =>
{ {
b.HasOne("API.Schema.Manga", "ParentManga") b.HasOne("API.Schema.Manga", "ParentManga")
.WithMany() .WithMany("Chapters")
.HasForeignKey("ParentMangaId") .HasForeignKey("ParentMangaId")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
@ -664,51 +573,99 @@ namespace API.Migrations
b.Navigation("ParentJob"); b.Navigation("ParentJob");
}); });
modelBuilder.Entity("API.Schema.Link", b =>
{
b.HasOne("API.Schema.Manga", null)
.WithMany("Links")
.HasForeignKey("MangaId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("API.Schema.Manga", b => modelBuilder.Entity("API.Schema.Manga", b =>
{ {
b.HasOne("API.Schema.LocalLibrary", "Library") b.HasOne("API.Schema.LocalLibrary", "Library")
.WithMany() .WithMany()
.HasForeignKey("LibraryLocalLibraryId") .HasForeignKey("LibraryId")
.OnDelete(DeleteBehavior.Cascade); .OnDelete(DeleteBehavior.SetNull);
b.HasOne("API.Schema.MangaConnectors.MangaConnector", "MangaConnector") b.HasOne("API.Schema.MangaConnectors.MangaConnector", "MangaConnector")
.WithMany() .WithMany()
.HasForeignKey("MangaConnectorId") .HasForeignKey("MangaConnectorName")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .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("Library");
b.Navigation("Links");
b.Navigation("MangaConnector"); b.Navigation("MangaConnector");
}); });
modelBuilder.Entity("API.Schema.MangaAltTitle", b => modelBuilder.Entity("AuthorToManga", b =>
{
b.HasOne("API.Schema.Manga", null)
.WithMany("AltTitles")
.HasForeignKey("MangaId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("AuthorManga", b =>
{ {
b.HasOne("API.Schema.Author", null) b.HasOne("API.Schema.Author", null)
.WithMany() .WithMany()
.HasForeignKey("AuthorsAuthorId") .HasForeignKey("AuthorIds")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
b.HasOne("API.Schema.Manga", null) b.HasOne("API.Schema.Manga", null)
.WithMany() .WithMany()
.HasForeignKey("MangaId") .HasForeignKey("MangaIds")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
}); });
@ -728,17 +685,17 @@ namespace API.Migrations
.IsRequired(); .IsRequired();
}); });
modelBuilder.Entity("MangaMangaTag", b => modelBuilder.Entity("MangaTagToManga", b =>
{ {
b.HasOne("API.Schema.Manga", null) b.HasOne("API.Schema.Manga", null)
.WithMany() .WithMany()
.HasForeignKey("MangaId") .HasForeignKey("MangaIds")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
b.HasOne("API.Schema.MangaTag", null) b.HasOne("API.Schema.MangaTag", null)
.WithMany() .WithMany()
.HasForeignKey("MangaTagsTag") .HasForeignKey("MangaTagIds")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
}); });
@ -776,6 +733,25 @@ namespace API.Migrations
b.Navigation("Chapter"); 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 => modelBuilder.Entity("API.Schema.Jobs.RetrieveChaptersJob", b =>
{ {
b.HasOne("API.Schema.Manga", "Manga") b.HasOne("API.Schema.Manga", "Manga")
@ -798,22 +774,9 @@ namespace API.Migrations
b.Navigation("Manga"); b.Navigation("Manga");
}); });
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.Manga", b => modelBuilder.Entity("API.Schema.Manga", b =>
{ {
b.Navigation("AltTitles"); b.Navigation("Chapters");
b.Navigation("Links");
}); });
#pragma warning restore 612, 618 #pragma warning restore 612, 618
} }

View File

@ -5,35 +5,35 @@
namespace API.Migrations namespace API.Migrations
{ {
/// <inheritdoc /> /// <inheritdoc />
public partial class dev0104251 : Migration public partial class Initial4 : Migration
{ {
/// <inheritdoc /> /// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder) protected override void Up(MigrationBuilder migrationBuilder)
{ {
migrationBuilder.AlterColumn<string>( migrationBuilder.AlterColumn<string>(
name: "OriginalLanguage", name: "LibraryId",
table: "Mangas", table: "Mangas",
type: "character varying(8)", type: "character varying(64)",
maxLength: 8, maxLength: 64,
nullable: true, nullable: true,
oldClrType: typeof(string), oldClrType: typeof(string),
oldType: "character varying(8)", oldType: "character varying(64)",
oldMaxLength: 8); oldMaxLength: 64);
} }
/// <inheritdoc /> /// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder) protected override void Down(MigrationBuilder migrationBuilder)
{ {
migrationBuilder.AlterColumn<string>( migrationBuilder.AlterColumn<string>(
name: "OriginalLanguage", name: "LibraryId",
table: "Mangas", table: "Mangas",
type: "character varying(8)", type: "character varying(64)",
maxLength: 8, maxLength: 64,
nullable: false, nullable: false,
defaultValue: "", defaultValue: "",
oldClrType: typeof(string), oldClrType: typeof(string),
oldType: "character varying(8)", oldType: "character varying(64)",
oldMaxLength: 8, oldMaxLength: 64,
oldNullable: true); oldNullable: true);
} }
} }

View File

@ -0,0 +1,783 @@
// <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("20250509035754_Initial-5")]
partial class Initial5
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "9.0.3")
.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()
.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.LibraryConnectors.LibraryConnector", b =>
{
b.Property<string>("LibraryConnectorId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("Auth")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("BaseUrl")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<byte>("LibraryType")
.HasColumnType("smallint");
b.HasKey("LibraryConnectorId");
b.ToTable("LibraryConnectors");
b.HasDiscriminator<byte>("LibraryType");
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.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()
.HasMaxLength(512)
.HasColumnType("character varying(512)");
b.Property<string>("Title")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("character varying(128)");
b.Property<byte>("Urgency")
.HasColumnType("smallint");
b.HasKey("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()
.HasMaxLength(4096)
.HasColumnType("character varying(4096)");
b.Property<Dictionary<string, string>>("Headers")
.IsRequired()
.HasColumnType("hstore");
b.Property<string>("HttpMethod")
.IsRequired()
.HasMaxLength(8)
.HasColumnType("character varying(8)");
b.Property<string>("Url")
.IsRequired()
.HasMaxLength(2048)
.HasColumnType("character varying(2048)");
b.HasKey("Name");
b.ToTable("NotificationConnectors");
});
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.UpdateFilesDownloadedJob", 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.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.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.UpdateFilesDownloadedJob", 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

@ -0,0 +1,40 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace API.Migrations
{
/// <inheritdoc />
public partial class Initial5 : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<string>(
name: "ParentJobId",
table: "Jobs",
type: "character varying(64)",
maxLength: 64,
nullable: true,
oldClrType: typeof(string),
oldType: "character varying(64)",
oldMaxLength: 64);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<string>(
name: "ParentJobId",
table: "Jobs",
type: "character varying(64)",
maxLength: 64,
nullable: false,
defaultValue: "",
oldClrType: typeof(string),
oldType: "character varying(64)",
oldMaxLength: 64,
oldNullable: true);
}
}
}

View File

@ -61,7 +61,6 @@ namespace API.Migrations
b.Property<string>("ParentMangaId") b.Property<string>("ParentMangaId")
.IsRequired() .IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)"); .HasColumnType("character varying(64)");
b.Property<string>("Title") b.Property<string>("Title")
@ -89,10 +88,6 @@ namespace API.Migrations
.HasMaxLength(64) .HasMaxLength(64)
.HasColumnType("character varying(64)"); .HasColumnType("character varying(64)");
b.PrimitiveCollection<string[]>("DependsOnJobsIds")
.HasMaxLength(64)
.HasColumnType("text[]");
b.Property<bool>("Enabled") b.Property<bool>("Enabled")
.HasColumnType("boolean"); .HasColumnType("boolean");
@ -151,32 +146,6 @@ namespace API.Migrations
b.UseTphMappingStrategy(); b.UseTphMappingStrategy();
}); });
modelBuilder.Entity("API.Schema.Link", b =>
{
b.Property<string>("LinkId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("LinkProvider")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("LinkUrl")
.IsRequired()
.HasMaxLength(2048)
.HasColumnType("character varying(2048)");
b.Property<string>("MangaId")
.HasColumnType("character varying(64)");
b.HasKey("LinkId");
b.HasIndex("MangaId");
b.ToTable("Links");
});
modelBuilder.Entity("API.Schema.LocalLibrary", b => modelBuilder.Entity("API.Schema.LocalLibrary", b =>
{ {
b.Property<string>("LocalLibraryId") b.Property<string>("LocalLibraryId")
@ -205,11 +174,13 @@ namespace API.Migrations
.HasColumnType("character varying(64)"); .HasColumnType("character varying(64)");
b.Property<string>("CoverFileNameInCache") b.Property<string>("CoverFileNameInCache")
.HasColumnType("text"); .HasMaxLength(512)
.HasColumnType("character varying(512)");
b.Property<string>("CoverUrl") b.Property<string>("CoverUrl")
.IsRequired() .IsRequired()
.HasColumnType("text"); .HasMaxLength(512)
.HasColumnType("character varying(512)");
b.Property<string>("Description") b.Property<string>("Description")
.IsRequired() .IsRequired()
@ -225,17 +196,18 @@ namespace API.Migrations
.HasMaxLength(256) .HasMaxLength(256)
.HasColumnType("character varying(256)"); .HasColumnType("character varying(256)");
b.Property<float>("IgnoreChapterBefore") b.Property<float>("IgnoreChaptersBefore")
.HasColumnType("real"); .HasColumnType("real");
b.Property<string>("LibraryLocalLibraryId") b.Property<string>("LibraryId")
.HasColumnType("character varying(64)");
b.Property<string>("MangaConnectorId")
.IsRequired()
.HasMaxLength(64) .HasMaxLength(64)
.HasColumnType("character varying(64)"); .HasColumnType("character varying(64)");
b.Property<string>("MangaConnectorName")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("character varying(32)");
b.Property<string>("Name") b.Property<string>("Name")
.IsRequired() .IsRequired()
.HasMaxLength(512) .HasMaxLength(512)
@ -258,39 +230,13 @@ namespace API.Migrations
b.HasKey("MangaId"); b.HasKey("MangaId");
b.HasIndex("LibraryLocalLibraryId"); b.HasIndex("LibraryId");
b.HasIndex("MangaConnectorId"); b.HasIndex("MangaConnectorName");
b.ToTable("Mangas"); b.ToTable("Mangas");
}); });
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()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.HasKey("AltTitleId");
b.HasIndex("MangaId");
b.ToTable("AltTitles");
});
modelBuilder.Entity("API.Schema.MangaConnectors.MangaConnector", b => modelBuilder.Entity("API.Schema.MangaConnectors.MangaConnector", b =>
{ {
b.Property<string>("Name") b.Property<string>("Name")
@ -392,19 +338,19 @@ namespace API.Migrations
b.ToTable("NotificationConnectors"); b.ToTable("NotificationConnectors");
}); });
modelBuilder.Entity("AuthorManga", b => modelBuilder.Entity("AuthorToManga", b =>
{ {
b.Property<string>("AuthorsAuthorId") b.Property<string>("AuthorIds")
.HasColumnType("character varying(64)"); .HasColumnType("character varying(64)");
b.Property<string>("MangaId") b.Property<string>("MangaIds")
.HasColumnType("character varying(64)"); .HasColumnType("character varying(64)");
b.HasKey("AuthorsAuthorId", "MangaId"); b.HasKey("AuthorIds", "MangaIds");
b.HasIndex("MangaId"); b.HasIndex("MangaIds");
b.ToTable("AuthorManga"); b.ToTable("AuthorToManga");
}); });
modelBuilder.Entity("JobJob", b => modelBuilder.Entity("JobJob", b =>
@ -422,19 +368,19 @@ namespace API.Migrations
b.ToTable("JobJob"); b.ToTable("JobJob");
}); });
modelBuilder.Entity("MangaMangaTag", b => modelBuilder.Entity("MangaTagToManga", b =>
{ {
b.Property<string>("MangaId") b.Property<string>("MangaTagIds")
.HasColumnType("character varying(64)"); .HasColumnType("character varying(64)");
b.Property<string>("MangaTagsTag") b.Property<string>("MangaIds")
.HasColumnType("character varying(64)"); .HasColumnType("character varying(64)");
b.HasKey("MangaId", "MangaTagsTag"); b.HasKey("MangaTagIds", "MangaIds");
b.HasIndex("MangaTagsTag"); b.HasIndex("MangaIds");
b.ToTable("MangaMangaTag"); b.ToTable("MangaTagToManga");
}); });
modelBuilder.Entity("API.Schema.Jobs.DownloadAvailableChaptersJob", b => modelBuilder.Entity("API.Schema.Jobs.DownloadAvailableChaptersJob", b =>
@ -446,6 +392,8 @@ namespace API.Migrations
.HasMaxLength(64) .HasMaxLength(64)
.HasColumnType("character varying(64)"); .HasColumnType("character varying(64)");
b.HasIndex("MangaId");
b.ToTable("Jobs", t => b.ToTable("Jobs", t =>
{ {
t.Property("MangaId") t.Property("MangaId")
@ -464,6 +412,8 @@ namespace API.Migrations
.HasMaxLength(64) .HasMaxLength(64)
.HasColumnType("character varying(64)"); .HasColumnType("character varying(64)");
b.HasIndex("MangaId");
b.HasDiscriminator().HasValue((byte)4); b.HasDiscriminator().HasValue((byte)4);
}); });
@ -476,6 +426,8 @@ namespace API.Migrations
.HasMaxLength(64) .HasMaxLength(64)
.HasColumnType("character varying(64)"); .HasColumnType("character varying(64)");
b.HasIndex("ChapterId");
b.HasDiscriminator().HasValue((byte)0); b.HasDiscriminator().HasValue((byte)0);
}); });
@ -496,7 +448,7 @@ namespace API.Migrations
b.HasDiscriminator().HasValue((byte)3); b.HasDiscriminator().HasValue((byte)3);
}); });
modelBuilder.Entity("API.Schema.Jobs.RetrieveChaptersJob", b => modelBuilder.Entity("API.Schema.Jobs.MoveMangaLibraryJob", b =>
{ {
b.HasBaseType("API.Schema.Jobs.Job"); b.HasBaseType("API.Schema.Jobs.Job");
@ -505,6 +457,40 @@ namespace API.Migrations
.HasMaxLength(64) .HasMaxLength(64)
.HasColumnType("character varying(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 => b.ToTable("Jobs", t =>
{ {
t.Property("MangaId") t.Property("MangaId")
@ -523,6 +509,8 @@ namespace API.Migrations
.HasMaxLength(64) .HasMaxLength(64)
.HasColumnType("character varying(64)"); .HasColumnType("character varying(64)");
b.HasIndex("MangaId");
b.ToTable("Jobs", t => b.ToTable("Jobs", t =>
{ {
t.Property("MangaId") t.Property("MangaId")
@ -532,26 +520,6 @@ namespace API.Migrations
b.HasDiscriminator().HasValue((byte)6); b.HasDiscriminator().HasValue((byte)6);
}); });
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 => modelBuilder.Entity("API.Schema.LibraryConnectors.Kavita", b =>
{ {
b.HasBaseType("API.Schema.LibraryConnectors.LibraryConnector"); b.HasBaseType("API.Schema.LibraryConnectors.LibraryConnector");
@ -566,20 +534,6 @@ namespace API.Migrations
b.HasDiscriminator().HasValue((byte)0); 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.Global", b => modelBuilder.Entity("API.Schema.MangaConnectors.Global", b =>
{ {
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector"); b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
@ -594,52 +548,10 @@ namespace API.Migrations
b.HasDiscriminator().HasValue("MangaDex"); 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.Manganato", b =>
{
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
b.HasDiscriminator().HasValue("Manganato");
});
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 => modelBuilder.Entity("API.Schema.Chapter", b =>
{ {
b.HasOne("API.Schema.Manga", "ParentManga") b.HasOne("API.Schema.Manga", "ParentManga")
.WithMany() .WithMany("Chapters")
.HasForeignKey("ParentMangaId") .HasForeignKey("ParentMangaId")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
@ -657,51 +569,99 @@ namespace API.Migrations
b.Navigation("ParentJob"); b.Navigation("ParentJob");
}); });
modelBuilder.Entity("API.Schema.Link", b =>
{
b.HasOne("API.Schema.Manga", null)
.WithMany("Links")
.HasForeignKey("MangaId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("API.Schema.Manga", b => modelBuilder.Entity("API.Schema.Manga", b =>
{ {
b.HasOne("API.Schema.LocalLibrary", "Library") b.HasOne("API.Schema.LocalLibrary", "Library")
.WithMany() .WithMany()
.HasForeignKey("LibraryLocalLibraryId") .HasForeignKey("LibraryId")
.OnDelete(DeleteBehavior.Restrict); .OnDelete(DeleteBehavior.SetNull);
b.HasOne("API.Schema.MangaConnectors.MangaConnector", "MangaConnector") b.HasOne("API.Schema.MangaConnectors.MangaConnector", "MangaConnector")
.WithMany() .WithMany()
.HasForeignKey("MangaConnectorId") .HasForeignKey("MangaConnectorName")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .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("Library");
b.Navigation("Links");
b.Navigation("MangaConnector"); b.Navigation("MangaConnector");
}); });
modelBuilder.Entity("API.Schema.MangaAltTitle", b => modelBuilder.Entity("AuthorToManga", b =>
{
b.HasOne("API.Schema.Manga", null)
.WithMany("AltTitles")
.HasForeignKey("MangaId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("AuthorManga", b =>
{ {
b.HasOne("API.Schema.Author", null) b.HasOne("API.Schema.Author", null)
.WithMany() .WithMany()
.HasForeignKey("AuthorsAuthorId") .HasForeignKey("AuthorIds")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
b.HasOne("API.Schema.Manga", null) b.HasOne("API.Schema.Manga", null)
.WithMany() .WithMany()
.HasForeignKey("MangaId") .HasForeignKey("MangaIds")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
}); });
@ -721,22 +681,85 @@ namespace API.Migrations
.IsRequired(); .IsRequired();
}); });
modelBuilder.Entity("MangaMangaTag", b => modelBuilder.Entity("MangaTagToManga", b =>
{ {
b.HasOne("API.Schema.Manga", null) b.HasOne("API.Schema.Manga", null)
.WithMany() .WithMany()
.HasForeignKey("MangaId") .HasForeignKey("MangaIds")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
b.HasOne("API.Schema.MangaTag", null) b.HasOne("API.Schema.MangaTag", null)
.WithMany() .WithMany()
.HasForeignKey("MangaTagsTag") .HasForeignKey("MangaTagIds")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
}); });
modelBuilder.Entity("API.Schema.Jobs.UpdateMetadataJob", b => 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.UpdateFilesDownloadedJob", b =>
{ {
b.HasOne("API.Schema.Manga", "Manga") b.HasOne("API.Schema.Manga", "Manga")
.WithMany() .WithMany()
@ -749,9 +772,7 @@ namespace API.Migrations
modelBuilder.Entity("API.Schema.Manga", b => modelBuilder.Entity("API.Schema.Manga", b =>
{ {
b.Navigation("AltTitles"); b.Navigation("Chapters");
b.Navigation("Links");
}); });
#pragma warning restore 612, 618 #pragma warning restore 612, 618
} }

View File

@ -7,6 +7,7 @@ using API.Schema.MangaConnectors;
using Asp.Versioning; using Asp.Versioning;
using Asp.Versioning.Builder; using Asp.Versioning.Builder;
using Asp.Versioning.Conventions; using Asp.Versioning.Conventions;
using log4net;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Converters; using Newtonsoft.Json.Converters;
@ -69,6 +70,7 @@ builder.Services.AddControllers().AddNewtonsoftJson(opts =>
opts.SerializerSettings.Converters.Add(new StringEnumConverter()); opts.SerializerSettings.Converters.Add(new StringEnumConverter());
opts.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore; opts.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore;
}); });
builder.Services.AddScoped<ILog>(opts => LogManager.GetLogger("API"));
builder.WebHost.UseUrls("http://*:6531"); builder.WebHost.UseUrls("http://*:6531");
@ -109,26 +111,18 @@ using (var scope = app.Services.CreateScope())
MangaConnector[] connectors = MangaConnector[] connectors =
[ [
new AsuraToon(),
new Bato(),
new MangaDex(), new MangaDex(),
new MangaHere(),
new MangaKatana(),
new Mangaworld(),
new ManhuaPlus(),
new Weebcentral(),
//new Manganato(),
new Global(scope.ServiceProvider.GetService<PgsqlContext>()!) new Global(scope.ServiceProvider.GetService<PgsqlContext>()!)
]; ];
MangaConnector[] newConnectors = connectors.Where(c => !context.MangaConnectors.Contains(c)).ToArray(); MangaConnector[] newConnectors = connectors.Where(c => !context.MangaConnectors.Contains(c)).ToArray();
context.MangaConnectors.AddRange(newConnectors); context.MangaConnectors.AddRange(newConnectors);
context.Jobs.AddRange(context.Mangas.AsEnumerable().Select(m => new UpdateFilesDownloadedJob(0, m.MangaId))); context.Jobs.AddRange(context.Mangas.AsEnumerable().Select(m => new UpdateFilesDownloadedJob(m, 0)));
context.Jobs.RemoveRange(context.Jobs.Where(j => j.state == JobState.Completed && j.RecurrenceMs < 1)); context.Jobs.RemoveRange(context.Jobs.Where(j => j.state == JobState.Completed && j.RecurrenceMs < 1));
if (!context.LocalLibraries.Any()) if (!context.LocalLibraries.Any())
context.LocalLibraries.Add(new LocalLibrary(TrangaSettings.downloadLocation, "Default Library")); context.LocalLibraries.Add(new LocalLibrary(TrangaSettings.downloadLocation, "Default ToLibrary"));
string[] emojis = { "(•‿•)", "(づ \u25d5‿\u25d5 )づ", "( \u02d8\u25bd\u02d8)っ\u2668", "=\uff3e\u25cf \u22cf \u25cf\uff3e=", "(ΦωΦ)", "(\u272a\u3268\u272a)", "( ノ・o・ )ノ", "(〜^\u2207^ )〜", "~(\u2267ω\u2266)~","૮ \u00b4• ﻌ \u00b4• ა", "(\u02c3ᆺ\u02c2)", "(=\ud83d\udf66 \u0f1d \ud83d\udf66=)"}; string[] emojis = { "(•‿•)", "(づ \u25d5‿\u25d5 )づ", "( \u02d8\u25bd\u02d8)っ\u2668", "=\uff3e\u25cf \u22cf \u25cf\uff3e=", "(ΦωΦ)", "(\u272a\u3268\u272a)", "( ノ・o・ )ノ", "(〜^\u2207^ )〜", "~(\u2267ω\u2266)~","૮ \u00b4• ﻌ \u00b4• ა", "(\u02c3ᆺ\u02c2)", "(=\ud83d\udf66 \u0f1d \ud83d\udf66=)"};
context.Notifications.Add(new Notification("Tranga Started", emojis[Random.Shared.Next(0, emojis.Length - 1)], NotificationUrgency.High)); context.Notifications.Add(new Notification("Tranga Started", emojis[Random.Shared.Next(0, emojis.Length - 1)], NotificationUrgency.High));

View File

@ -3,7 +3,6 @@ using System.ComponentModel.DataAnnotations.Schema;
using System.Text; using System.Text;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Xml.Linq; using System.Xml.Linq;
using API.Schema.Jobs;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Newtonsoft.Json; using Newtonsoft.Json;
@ -12,51 +11,50 @@ namespace API.Schema;
[PrimaryKey("ChapterId")] [PrimaryKey("ChapterId")]
public class Chapter : IComparable<Chapter> public class Chapter : IComparable<Chapter>
{ {
public Chapter(Manga parentManga, string url, string chapterNumber, int? volumeNumber = null, string? title = null) [StringLength(64)] [Required] public string ChapterId { get; init; }
: this(parentManga.MangaId, url, chapterNumber, volumeNumber, title)
{
ParentManga = parentManga;
FileName = GetArchiveFilePath(parentManga.Name);
}
public Chapter(string parentMangaId, string url, string chapterNumber, public string ParentMangaId { get; init; }
int? volumeNumber = null, string? title = null) [JsonIgnore] public Manga ParentManga { get; init; } = null!;
{
ChapterId = TokenGen.CreateToken(typeof(Chapter), parentMangaId, (volumeNumber ?? 0).ToString(), chapterNumber);
ParentMangaId = parentMangaId;
Url = url;
ChapterNumber = chapterNumber;
VolumeNumber = volumeNumber;
Title = title;
}
[StringLength(64)]
[Required]
public string ChapterId { get; init; }
public int? VolumeNumber { get; private set; } public int? VolumeNumber { get; private set; }
[StringLength(10)] [StringLength(10)] [Required] public string ChapterNumber { get; private set; }
[Required]
public string ChapterNumber { get; private set; }
[StringLength(2048)] [StringLength(2048)] [Required] [Url] public string Url { get; internal set; }
[Required]
[Url] [StringLength(256)] public string? Title { get; private set; }
public string Url { get; internal set; }
[StringLength(256)] [StringLength(256)] [Required] public string FileName { get; private set; }
public string? Title { get; private set; }
[StringLength(256)] [Required] public bool Downloaded { get; internal set; }
[Required] [JsonIgnore] [NotMapped] public string FullArchiveFilePath => Path.Join(ParentManga.FullDirectoryPath, FileName);
public string FileName { get; private set; }
[JsonIgnore] public Chapter(Manga parentManga, string url, string chapterNumber, int? volumeNumber = null, string? title = null)
[NotMapped] {
public string? FullArchiveFilePath => ParentManga is { } m ? Path.Join(m.FullDirectoryPath, FileName) : null; this.ChapterId = TokenGen.CreateToken(typeof(Chapter), parentManga.MangaId, chapterNumber);
[Required] this.ParentMangaId = parentManga.MangaId;
public bool Downloaded { get; internal set; } = false; this.ParentManga = parentManga;
[Required] this.VolumeNumber = volumeNumber;
[StringLength(64)] this.ChapterNumber = chapterNumber;
public string ParentMangaId { get; internal set; } this.Url = url;
[JsonIgnore] this.Title = title;
public Manga? ParentManga { get; init; } this.FileName = GetArchiveFilePath();
this.Downloaded = false;
}
/// <summary>
/// EF ONLY!!!
/// </summary>
internal Chapter(string chapterId, string parentMangaId, int? volumeNumber, string chapterNumber, string url, string? title, string fileName, bool downloaded)
{
this.ChapterId = chapterId;
this.ParentMangaId = parentMangaId;
this.VolumeNumber = volumeNumber;
this.ChapterNumber = chapterNumber;
this.Url = url;
this.Title = title;
this.FileName = fileName;
this.Downloaded = downloaded;
}
public int CompareTo(Chapter? other) public int CompareTo(Chapter? other)
{ {
@ -70,43 +68,11 @@ public class Chapter : IComparable<Chapter>
}; };
} }
public MoveFileOrFolderJob? UpdateChapterNumber(string chapterNumber)
{
ChapterNumber = chapterNumber;
return UpdateArchiveFileName();
}
public MoveFileOrFolderJob? UpdateVolumeNumber(int? volumeNumber)
{
VolumeNumber = volumeNumber;
return UpdateArchiveFileName();
}
public MoveFileOrFolderJob? UpdateTitle(string? title)
{
Title = title;
return UpdateArchiveFileName();
}
internal MoveFileOrFolderJob? UpdateArchiveFileName()
{
string? oldPath = FullArchiveFilePath;
if (oldPath is null)
return null;
string newPath = GetArchiveFilePath();
FileName = newPath;
return Downloaded ? new MoveFileOrFolderJob(oldPath, newPath) : null;
}
/// <summary> /// <summary>
/// Checks the filesystem if an archive at the ArchiveFilePath exists /// Checks the filesystem if an archive at the ArchiveFilePath exists
/// </summary> /// </summary>
/// <returns>True if archive exists on disk</returns> /// <returns>True if archive exists on disk</returns>
public bool IsDownloaded() public bool CheckDownloaded() => File.Exists(FullArchiveFilePath);
{
string path = GetArchiveFilePath();
return File.Exists(path);
}
/// Placeholders: /// Placeholders:
/// %M Manga Name /// %M Manga Name
@ -119,7 +85,7 @@ public class Chapter : IComparable<Chapter>
/// %Y Year (Manga) /// %Y Year (Manga)
private static readonly Regex NullableRex = new(@"\?([a-zA-Z])\(([^\)]*)\)|(.+?)"); private static readonly Regex NullableRex = new(@"\?([a-zA-Z])\(([^\)]*)\)|(.+?)");
private static readonly Regex ReplaceRexx = new(@"%([a-zA-Z])|(.+?)"); private static readonly Regex ReplaceRexx = new(@"%([a-zA-Z])|(.+?)");
private string GetArchiveFilePath(string? parentMangaName = null) private string GetArchiveFilePath()
{ {
string archiveNamingScheme = TrangaSettings.chapterNamingScheme; string archiveNamingScheme = TrangaSettings.chapterNamingScheme;
StringBuilder stringBuilder = new(); StringBuilder stringBuilder = new();
@ -134,13 +100,13 @@ public class Chapter : IComparable<Chapter>
char placeholder = nullable.Groups[1].Value[0]; char placeholder = nullable.Groups[1].Value[0];
bool isNull = placeholder switch bool isNull = placeholder switch
{ {
'M' => ParentManga?.Name is null && parentMangaName is null, 'M' => ParentManga?.Name is null,
'V' => VolumeNumber is null, 'V' => VolumeNumber is null,
'C' => ChapterNumber is null, 'C' => ChapterNumber is null,
'T' => Title is null, 'T' => Title is null,
'A' => ParentManga?.Authors?.FirstOrDefault()?.AuthorName is null, 'A' => ParentManga?.Authors?.FirstOrDefault()?.AuthorName is null,
'I' => ChapterId is null, 'I' => ChapterId is null,
'i' => ParentMangaId is null, 'i' => ParentManga?.MangaId is null,
'Y' => ParentManga?.Year is null, 'Y' => ParentManga?.Year is null,
_ => true _ => true
}; };
@ -162,13 +128,13 @@ public class Chapter : IComparable<Chapter>
char placeholder = replace.Groups[1].Value[0]; char placeholder = replace.Groups[1].Value[0];
string? value = placeholder switch string? value = placeholder switch
{ {
'M' => ParentManga?.Name ?? parentMangaName, 'M' => ParentManga?.Name,
'V' => VolumeNumber?.ToString(), 'V' => VolumeNumber?.ToString(),
'C' => ChapterNumber, 'C' => ChapterNumber,
'T' => Title, 'T' => Title,
'A' => ParentManga?.Authors?.FirstOrDefault()?.AuthorName, 'A' => ParentManga?.Authors?.FirstOrDefault()?.AuthorName,
'I' => ChapterId, 'I' => ChapterId,
'i' => ParentMangaId, 'i' => ParentManga?.MangaId,
'Y' => ParentManga?.Year.ToString(), 'Y' => ParentManga?.Year.ToString(),
_ => null _ => null
}; };

View File

@ -1,17 +1,31 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using Newtonsoft.Json;
namespace API.Schema.Jobs; namespace API.Schema.Jobs;
public class DownloadAvailableChaptersJob(ulong recurrenceMs, string mangaId, string? parentJobId = null, ICollection<string>? dependsOnJobsIds = null) public class DownloadAvailableChaptersJob : Job
: Job(TokenGen.CreateToken(typeof(DownloadAvailableChaptersJob)), JobType.DownloadAvailableChaptersJob, recurrenceMs, parentJobId, dependsOnJobsIds)
{ {
[StringLength(64)] [StringLength(64)] [Required] public string MangaId { get; init; }
[Required] [JsonIgnore] public Manga Manga { get; init; } = null!;
public string MangaId { get; init; } = mangaId;
public DownloadAvailableChaptersJob(Manga manga, ulong recurrenceMs, Job? parentJob = null, ICollection<Job>? dependsOnJobs = null)
: base(TokenGen.CreateToken(typeof(DownloadAvailableChaptersJob)), JobType.DownloadAvailableChaptersJob, recurrenceMs, parentJob, dependsOnJobs)
{
this.MangaId = manga.MangaId;
this.Manga = manga;
}
/// <summary>
/// EF ONLY!!!
/// </summary>
public DownloadAvailableChaptersJob(string mangaId, ulong recurrenceMs, string? parentJobId = null)
: base(TokenGen.CreateToken(typeof(DownloadAvailableChaptersJob)), JobType.DownloadAvailableChaptersJob, recurrenceMs, parentJobId)
{
this.MangaId = mangaId;
}
protected override IEnumerable<Job> RunInternal(PgsqlContext context) protected override IEnumerable<Job> RunInternal(PgsqlContext context)
{ {
return context.Chapters.Where(c => c.ParentMangaId == MangaId).AsEnumerable() return Manga.Chapters.Select(chapter => new DownloadSingleChapterJob(chapter, this));
.Select(chapter => new DownloadSingleChapterJob(chapter.ChapterId, this.JobId));
} }
} }

View File

@ -1,26 +1,41 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using Microsoft.EntityFrameworkCore;
using Newtonsoft.Json;
namespace API.Schema.Jobs; namespace API.Schema.Jobs;
public class DownloadMangaCoverJob(string mangaId, string? parentJobId = null, ICollection<string>? dependsOnJobsIds = null) public class DownloadMangaCoverJob : Job
: Job(TokenGen.CreateToken(typeof(DownloadMangaCoverJob)), JobType.DownloadMangaCoverJob, 0, parentJobId, dependsOnJobsIds)
{ {
[StringLength(64)] [StringLength(64)] [Required] public string MangaId { get; init; }
[Required] [JsonIgnore] public Manga Manga { get; init; } = null!;
public string MangaId { get; init; } = mangaId;
public DownloadMangaCoverJob(Manga manga, Job? parentJob = null, ICollection<Job>? dependsOnJobs = null)
: base(TokenGen.CreateToken(typeof(DownloadMangaCoverJob)), JobType.DownloadMangaCoverJob, 0, parentJob, dependsOnJobs)
{
this.MangaId = manga.MangaId;
this.Manga = manga;
}
/// <summary>
/// EF ONLY!!!
/// </summary>
public DownloadMangaCoverJob(string mangaId, string? parentJobId = null)
: base(TokenGen.CreateToken(typeof(DownloadMangaCoverJob)), JobType.DownloadMangaCoverJob, 0, parentJobId)
{
this.MangaId = mangaId;
}
protected override IEnumerable<Job> RunInternal(PgsqlContext context) protected override IEnumerable<Job> RunInternal(PgsqlContext context)
{ {
Manga? manga = context.Mangas.Find(this.MangaId); try
if (manga is null)
{ {
Log.Error($"Manga {this.MangaId} not found."); Manga.CoverFileNameInCache = Manga.MangaConnector.SaveCoverImageToCache(Manga);
return [];
}
manga.CoverFileNameInCache = manga.SaveCoverImageToCache();
context.SaveChanges(); context.SaveChanges();
Log.Info($"Saved cover for Manga {this.MangaId} to cache at {manga.CoverFileNameInCache}."); }
catch (DbUpdateException e)
{
Log.Error(e);
}
return []; return [];
} }
} }

View File

@ -2,7 +2,7 @@
using System.IO.Compression; using System.IO.Compression;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using API.MangaDownloadClients; using API.MangaDownloadClients;
using API.Schema.MangaConnectors; using Newtonsoft.Json;
using SixLabors.ImageSharp; using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Formats.Jpeg; using SixLabors.ImageSharp.Formats.Jpeg;
using SixLabors.ImageSharp.Processing; using SixLabors.ImageSharp.Processing;
@ -11,45 +11,37 @@ using static System.IO.UnixFileMode;
namespace API.Schema.Jobs; namespace API.Schema.Jobs;
public class DownloadSingleChapterJob(string chapterId, string? parentJobId = null, ICollection<string>? dependsOnJobsIds = null) public class DownloadSingleChapterJob : Job
: Job(TokenGen.CreateToken(typeof(DownloadSingleChapterJob)), JobType.DownloadSingleChapterJob, 0, parentJobId, dependsOnJobsIds)
{ {
[StringLength(64)] [StringLength(64)] [Required] public string ChapterId { get; init; }
[Required]
public string ChapterId { get; init; } = chapterId; [JsonIgnore] public Chapter Chapter { get; init; } = null!;
public DownloadSingleChapterJob(Chapter chapter, Job? parentJob = null, ICollection<Job>? dependsOnJobs = null)
: base(TokenGen.CreateToken(typeof(DownloadSingleChapterJob)), JobType.DownloadSingleChapterJob, 0, parentJob, dependsOnJobs)
{
this.ChapterId = chapter.ChapterId;
this.Chapter = chapter;
}
/// <summary>
/// EF ONLY!!!
/// </summary>
public DownloadSingleChapterJob(string chapterId, string? parentJobId = null)
: base(TokenGen.CreateToken(typeof(DownloadSingleChapterJob)), JobType.DownloadSingleChapterJob, 0)
{
this.ChapterId = chapterId;
}
protected override IEnumerable<Job> RunInternal(PgsqlContext context) protected override IEnumerable<Job> RunInternal(PgsqlContext context)
{ {
Chapter? chapter = context.Chapters.Find(ChapterId); string[] imageUrls = Chapter.ParentManga.MangaConnector.GetChapterImageUrls(Chapter);
if (chapter is null)
{
Log.Error("Chapter is null.");
return [];
}
Manga? manga = context.Mangas.Find(chapter.ParentMangaId) ?? chapter.ParentManga;
if (manga is null)
{
Log.Error("Manga is null.");
return [];
}
MangaConnector? connector = context.MangaConnectors.Find(manga.MangaConnectorId) ?? manga.MangaConnector;
if (connector is null)
{
Log.Error("Connector is null.");
return [];
}
string[] imageUrls = connector.GetChapterImageUrls(chapter);
if (imageUrls.Length < 1) if (imageUrls.Length < 1)
{ {
Log.Info($"No imageUrls for chapter {ChapterId}"); Log.Info($"No imageUrls for chapter {ChapterId}");
return []; return [];
} }
string? saveArchiveFilePath = chapter.FullArchiveFilePath; string saveArchiveFilePath = Chapter.FullArchiveFilePath;
if (saveArchiveFilePath is null)
{
Log.Error("saveArchiveFilePath is null.");
return [];
}
//Check if Publication Directory already exists //Check if Publication Directory already exists
string directoryPath = Path.GetDirectoryName(saveArchiveFilePath)!; string directoryPath = Path.GetDirectoryName(saveArchiveFilePath)!;
@ -88,10 +80,10 @@ public class DownloadSingleChapterJob(string chapterId, string? parentJobId = nu
} }
} }
CopyCoverFromCacheToDownloadLocation(manga); CopyCoverFromCacheToDownloadLocation(Chapter.ParentManga);
Log.Debug($"Creating ComicInfo.xml {ChapterId}"); Log.Debug($"Creating ComicInfo.xml {ChapterId}");
File.WriteAllText(Path.Join(tempFolder, "ComicInfo.xml"), chapter.GetComicInfoXmlString()); File.WriteAllText(Path.Join(tempFolder, "ComicInfo.xml"), Chapter.GetComicInfoXmlString());
Log.Debug($"Packaging images to archive {ChapterId}"); Log.Debug($"Packaging images to archive {ChapterId}");
//ZIP-it and ship-it //ZIP-it and ship-it
@ -100,10 +92,10 @@ public class DownloadSingleChapterJob(string chapterId, string? parentJobId = nu
File.SetUnixFileMode(saveArchiveFilePath, UserRead | UserWrite | UserExecute | GroupRead | GroupWrite | GroupExecute | OtherRead | OtherExecute); File.SetUnixFileMode(saveArchiveFilePath, UserRead | UserWrite | UserExecute | GroupRead | GroupWrite | GroupExecute | OtherRead | OtherExecute);
Directory.Delete(tempFolder, true); //Cleanup Directory.Delete(tempFolder, true); //Cleanup
chapter.Downloaded = true; Chapter.Downloaded = true;
context.SaveChanges(); context.SaveChanges();
return [new UpdateFilesDownloadedJob(0, manga.MangaId, this.JobId)]; return [new UpdateFilesDownloadedJob(Chapter.ParentManga, 0, this)];
} }
private void ProcessImage(string imagePath) private void ProcessImage(string imagePath)
@ -138,7 +130,7 @@ public class DownloadSingleChapterJob(string chapterId, string? parentJobId = nu
} }
Log.Info($"Copying cover to {publicationFolder}"); Log.Info($"Copying cover to {publicationFolder}");
string? fileInCache = manga.CoverFileNameInCache ?? manga.SaveCoverImageToCache(); string? fileInCache = manga.CoverFileNameInCache ?? manga.MangaConnector.SaveCoverImageToCache(manga);
if (fileInCache is null) if (fileInCache is null)
{ {
Log.Error($"File {fileInCache} does not exist"); Log.Error($"File {fileInCache} does not exist");

View File

@ -12,49 +12,50 @@ public abstract class Job
[StringLength(64)] [StringLength(64)]
[Required] [Required]
public string JobId { get; init; } public string JobId { get; init; }
[StringLength(64)]
public string? ParentJobId { get; init; }
[JsonIgnore]
public Job? ParentJob { get; init; }
[StringLength(64)]
public ICollection<string>? DependsOnJobsIds { get; init; }
[JsonIgnore]
public ICollection<Job>? DependsOnJobs { get; init; }
[Required] [StringLength(64)] public string? ParentJobId { get; init; }
public JobType JobType { get; init; } [JsonIgnore] public Job? ParentJob { get; init; }
[Required] [JsonIgnore] public ICollection<Job> DependsOnJobs { get; init; }
public ulong RecurrenceMs { get; set; }
[Required]
public DateTime LastExecution { get; internal set; } = DateTime.UnixEpoch;
[NotMapped] [Required] public JobType JobType { get; init; }
[Required]
public DateTime NextExecution => LastExecution.AddMilliseconds(RecurrenceMs);
[Required]
public JobState state { get; internal set; } = JobState.Waiting;
[Required]
public bool Enabled { get; internal set; } = true;
[NotMapped] [Required] public ulong RecurrenceMs { get; set; }
[JsonIgnore]
protected ILog Log { get; init; }
public Job(string jobId, JobType jobType, ulong recurrenceMs, Job? parentJob = null, ICollection<Job>? dependsOnJobs = null) [Required] public DateTime LastExecution { get; internal set; } = DateTime.UnixEpoch;
: this(jobId, jobType, recurrenceMs, parentJob?.JobId, dependsOnJobs?.Select(j => j.JobId).ToList())
[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;
[JsonIgnore] [NotMapped] internal bool DependenciesFulfilled => DependsOnJobs.All(j => j.IsCompleted);
[NotMapped] [JsonIgnore] protected ILog Log { get; init; }
protected Job(string jobId, JobType jobType, ulong recurrenceMs, Job? parentJob = null, ICollection<Job>? dependsOnJobs = null)
{ {
this.JobId = jobId;
this.JobType = jobType;
this.RecurrenceMs = recurrenceMs;
this.ParentJobId = parentJob?.JobId;
this.ParentJob = parentJob; this.ParentJob = parentJob;
this.DependsOnJobs = dependsOnJobs; this.DependsOnJobs = dependsOnJobs ?? [];
this.Log = LogManager.GetLogger(this.GetType());
} }
public Job(string jobId, JobType jobType, ulong recurrenceMs, string? parentJobId = null, ICollection<string>? dependsOnJobsIds = null) /// <summary>
/// EF ONLY!!!
/// </summary>
protected Job(string jobId, JobType jobType, ulong recurrenceMs, string? parentJobId)
{ {
Log = LogManager.GetLogger(GetType()); this.JobId = jobId;
JobId = jobId; this.JobType = jobType;
ParentJobId = parentJobId; this.RecurrenceMs = recurrenceMs;
DependsOnJobsIds = dependsOnJobsIds; this.ParentJobId = parentJobId;
JobType = jobType; this.DependsOnJobs = [];
RecurrenceMs = recurrenceMs;
this.Log = LogManager.GetLogger(this.GetType());
} }
public IEnumerable<Job> Run(IServiceProvider serviceProvider) public IEnumerable<Job> Run(IServiceProvider serviceProvider)

View File

@ -3,11 +3,12 @@
public enum JobState : byte public enum JobState : byte
{ {
//Values 0-63 Preparation Stages //Values 0-63 Preparation Stages
Waiting = 0, FirstExecution = 0,
//64-127 Running Stages //64-127 Running Stages
Running = 64, Running = 64,
//128-191 Completion Stages //128-191 Completion Stages
Completed = 128, Completed = 128,
CompletedWaiting = 159,
//192-255 Error stages //192-255 Error stages
Failed = 192 Failed = 192
} }

View File

@ -2,15 +2,31 @@
namespace API.Schema.Jobs; namespace API.Schema.Jobs;
public class MoveFileOrFolderJob(string fromLocation, string toLocation, string? parentJobId = null, ICollection<string>? dependsOnJobsIds = null) public class MoveFileOrFolderJob : Job
: Job(TokenGen.CreateToken(typeof(MoveFileOrFolderJob)), JobType.MoveFileOrFolderJob, 0, parentJobId, dependsOnJobsIds)
{ {
[StringLength(256)] [StringLength(256)]
[Required] [Required]
public string FromLocation { get; init; } = fromLocation; public string FromLocation { get; init; }
[StringLength(256)] [StringLength(256)]
[Required] [Required]
public string ToLocation { get; init; } = toLocation; 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>
public MoveFileOrFolderJob(string jobId, string fromLocation, string toLocation, string? parentJobId = null)
: base(jobId, JobType.MoveFileOrFolderJob, 0, parentJobId)
{
this.FromLocation = fromLocation;
this.ToLocation = toLocation;
}
protected override IEnumerable<Job> RunInternal(PgsqlContext context) protected override IEnumerable<Job> RunInternal(PgsqlContext context)
{ {

View File

@ -1,35 +1,39 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Newtonsoft.Json;
namespace API.Schema.Jobs; namespace API.Schema.Jobs;
public class MoveMangaLibraryJob(string mangaId, string toLibraryId, string? parentJobId = null, ICollection<string>? dependsOnJobsIds = null) public class MoveMangaLibraryJob : Job
: Job(TokenGen.CreateToken(typeof(MoveMangaLibraryJob)), JobType.MoveMangaLibraryJob, 0, parentJobId, dependsOnJobsIds)
{ {
[StringLength(64)] [StringLength(64)] [Required] public string MangaId { get; init; }
[Required] [JsonIgnore] public Manga Manga { get; init; } = null!;
public string MangaId { get; init; } = mangaId; [StringLength(64)] [Required] public string ToLibraryId { get; init; }
[StringLength(64)] public LocalLibrary ToLibrary { get; init; } = null!;
[Required]
public string ToLibraryId { get; init; } = toLibraryId; public MoveMangaLibraryJob(Manga manga, LocalLibrary toLibrary, Job? parentJob = null, ICollection<Job>? dependsOnJobs = null)
: base(TokenGen.CreateToken(typeof(MoveMangaLibraryJob)), JobType.MoveMangaLibraryJob, 0, parentJob, dependsOnJobs)
{
this.MangaId = manga.MangaId;
this.Manga = manga;
this.ToLibraryId = toLibrary.LocalLibraryId;
this.ToLibrary = toLibrary;
}
/// <summary>
/// EF ONLY!!!
/// </summary>
public MoveMangaLibraryJob(string mangaId, string toLibraryId, string? parentJobId = null)
: base(TokenGen.CreateToken(typeof(MoveMangaLibraryJob)), JobType.MoveMangaLibraryJob, 0, parentJobId)
{
this.MangaId = mangaId;
this.ToLibraryId = toLibraryId;
}
protected override IEnumerable<Job> RunInternal(PgsqlContext context) protected override IEnumerable<Job> RunInternal(PgsqlContext context)
{ {
Manga? manga = context.Mangas.Find(MangaId); Dictionary<Chapter, string> oldPath = Manga.Chapters.ToDictionary(c => c, c => c.FullArchiveFilePath);
if (manga is null) Manga.Library = ToLibrary;
{
Log.Error("Manga not found");
return [];
}
LocalLibrary? library = context.LocalLibraries.Find(ToLibraryId);
if (library is null)
{
Log.Error("LocalLibrary not found");
return [];
}
Chapter[] chapters = context.Chapters.Where(c => c.ParentMangaId == MangaId).ToArray();
Dictionary<Chapter, string> oldPath = chapters.ToDictionary(c => c, c => c.FullArchiveFilePath!);
manga.Library = library;
try try
{ {
context.SaveChanges(); context.SaveChanges();
@ -40,6 +44,6 @@ public class MoveMangaLibraryJob(string mangaId, string toLibraryId, string? par
return []; return [];
} }
return chapters.Select(c => new MoveFileOrFolderJob(oldPath[c], c.FullArchiveFilePath!)); return Manga.Chapters.Select(c => new MoveFileOrFolderJob(oldPath[c], c.FullArchiveFilePath));
} }
} }

View File

@ -1,40 +1,43 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using API.Schema.MangaConnectors; using API.Schema.MangaConnectors;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Newtonsoft.Json;
namespace API.Schema.Jobs; namespace API.Schema.Jobs;
public class RetrieveChaptersJob(ulong recurrenceMs, string mangaId, string? parentJobId = null, ICollection<string>? dependsOnJobsIds = null) public class RetrieveChaptersJob : Job
: Job(TokenGen.CreateToken(typeof(RetrieveChaptersJob)), JobType.RetrieveChaptersJob, recurrenceMs, parentJobId, dependsOnJobsIds)
{ {
[StringLength(64)] [StringLength(64)] [Required] public string MangaId { get; init; }
[Required] [JsonIgnore] public Manga Manga { get; init; } = null!;
public string MangaId { get; init; } = mangaId; [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.MangaId = manga.MangaId;
this.Manga = manga;
this.Language = language;
}
/// <summary>
/// EF ONLY!!!
/// </summary>
public RetrieveChaptersJob(string mangaId, string language, ulong recurrenceMs, string? parentJobId = null)
: base(TokenGen.CreateToken(typeof(RetrieveChaptersJob)), JobType.RetrieveChaptersJob, recurrenceMs, parentJobId)
{
this.MangaId = mangaId;
this.Language = language;
}
protected override IEnumerable<Job> RunInternal(PgsqlContext context) protected override IEnumerable<Job> RunInternal(PgsqlContext context)
{ {
Manga? manga = context.Mangas.Find(MangaId);
if (manga is null)
{
Log.Error("Manga is null.");
return [];
}
MangaConnector? connector = manga.MangaConnector ?? context.MangaConnectors.Find(manga.MangaConnectorId);
if (connector is null)
{
Log.Error("Connector is null.");
return [];
}
// This gets all chapters that are not downloaded // This gets all chapters that are not downloaded
Chapter[] allNewChapters = connector.GetNewChapters(manga).DistinctBy(c => c.ChapterId).ToArray(); Chapter[] allChapters = Manga.MangaConnector.GetChapters(Manga, Language);
Log.Info($"{allNewChapters.Length} new chapters."); Chapter[] newChapters = allChapters.Where(chapter => context.Chapters.Contains(chapter) == false).ToArray();
Log.Info($"{newChapters.Length} new chapters.");
try try
{ {
// This filters out chapters that are not downloaded but already exist in the DB
string[] chapterIds = context.Chapters.Where(chapter => chapter.ParentMangaId == manga.MangaId)
.Select(chapter => chapter.ChapterId).ToArray();
Chapter[] newChapters = allNewChapters.Where(chapter => !chapterIds.Contains(chapter.ChapterId)).ToArray();
context.Chapters.AddRange(newChapters); context.Chapters.AddRange(newChapters);
context.SaveChanges(); context.SaveChanges();
} }

View File

@ -1,22 +1,43 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using Microsoft.EntityFrameworkCore;
using Newtonsoft.Json; using Newtonsoft.Json;
namespace API.Schema.Jobs; namespace API.Schema.Jobs;
public class UpdateFilesDownloadedJob(ulong recurrenceMs, string mangaId, string? parentJobId = null, ICollection<string>? dependsOnJobsIds = null) public class UpdateFilesDownloadedJob : Job
: Job(TokenGen.CreateToken(typeof(UpdateFilesDownloadedJob)), JobType.UpdateFilesDownloadedJob, recurrenceMs, parentJobId, dependsOnJobsIds)
{ {
[StringLength(64)] [StringLength(64)] [Required] public string MangaId { get; init; }
[Required] [JsonIgnore] public Manga Manga { get; init; } = null!;
public string MangaId { get; init; } = mangaId;
public UpdateFilesDownloadedJob(Manga manga, ulong recurrenceMs, Job? parentJob = null, ICollection<Job>? dependsOnJobs = null)
: base(TokenGen.CreateToken(typeof(UpdateFilesDownloadedJob)), JobType.UpdateFilesDownloadedJob, recurrenceMs, parentJob, dependsOnJobs)
{
this.MangaId = manga.MangaId;
this.Manga = manga;
}
/// <summary>
/// EF ONLY!!!
/// </summary>
public UpdateFilesDownloadedJob(string mangaId, ulong recurrenceMs, string? parentJobId = null)
: base(TokenGen.CreateToken(typeof(UpdateFilesDownloadedJob)), JobType.UpdateFilesDownloadedJob, recurrenceMs, parentJobId)
{
this.MangaId = mangaId;
}
protected override IEnumerable<Job> RunInternal(PgsqlContext context) protected override IEnumerable<Job> RunInternal(PgsqlContext context)
{ {
IQueryable<Chapter> chapters = context.Chapters.Where(c => c.ParentMangaId == MangaId); foreach (Chapter chapter in Manga.Chapters)
foreach (Chapter chapter in chapters) chapter.Downloaded = chapter.CheckDownloaded();
chapter.Downloaded = chapter.IsDownloaded();
try
{
context.SaveChanges(); context.SaveChanges();
}
catch (DbUpdateException e)
{
Log.Error(e);
}
return []; return [];
} }
} }

View File

@ -1,27 +0,0 @@
using System.ComponentModel.DataAnnotations;
using Newtonsoft.Json;
namespace API.Schema.Jobs;
public class UpdateMetadataJob(ulong recurrenceMs, string mangaId, string? parentJobId = null, ICollection<string>? dependsOnJobsIds = null)
: Job(TokenGen.CreateToken(typeof(UpdateMetadataJob)), JobType.UpdateMetaDataJob, recurrenceMs, parentJobId, dependsOnJobsIds)
{
[StringLength(64)]
[Required]
public string MangaId { get; init; } = mangaId;
[JsonIgnore]
public virtual Manga? Manga { get; init; }
/// <summary>
/// Updates all data related to Manga.
/// Retrieves data from Mangaconnector
/// Updates Chapter-info
/// </summary>
/// <param name="context"></param>
protected override IEnumerable<Job> RunInternal(PgsqlContext context)
{
Log.Warn("NOT IMPLEMENTED.");
return [];//TODO
}
}

View File

@ -53,13 +53,13 @@ public class Kavita : LibraryConnector
protected override void UpdateLibraryInternal() protected override void UpdateLibraryInternal()
{ {
foreach (KavitaLibrary lib in GetLibraries()) foreach (KavitaLibrary lib in GetLibraries())
NetClient.MakePost($"{BaseUrl}/api/Library/scan?libraryId={lib.id}", "Bearer", Auth); NetClient.MakePost($"{BaseUrl}/api/ToLibrary/scan?libraryId={lib.id}", "Bearer", Auth);
} }
internal override bool Test() internal override bool Test()
{ {
foreach (KavitaLibrary lib in GetLibraries()) foreach (KavitaLibrary lib in GetLibraries())
if (NetClient.MakePost($"{BaseUrl}/api/Library/scan?libraryId={lib.id}", "Bearer", Auth)) if (NetClient.MakePost($"{BaseUrl}/api/ToLibrary/scan?libraryId={lib.id}", "Bearer", Auth))
return true; return true;
return false; return false;
} }
@ -70,7 +70,7 @@ public class Kavita : LibraryConnector
/// <returns>Array of KavitaLibrary</returns> /// <returns>Array of KavitaLibrary</returns>
private IEnumerable<KavitaLibrary> GetLibraries() private IEnumerable<KavitaLibrary> GetLibraries()
{ {
Stream data = NetClient.MakeRequest($"{BaseUrl}/api/Library/libraries", "Bearer", Auth); Stream data = NetClient.MakeRequest($"{BaseUrl}/api/ToLibrary/libraries", "Bearer", Auth);
if (data == Stream.Null) if (data == Stream.Null)
{ {
Log.Info("No libraries found"); Log.Info("No libraries found");

View File

@ -1,12 +1,7 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema; using System.ComponentModel.DataAnnotations.Schema;
using System.Diagnostics.CodeAnalysis;
using System.Net;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Text.RegularExpressions; using System.Text;
using API.MangaDownloadClients;
using API.Schema.Jobs;
using API.Schema.MangaConnectors; using API.Schema.MangaConnectors;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Newtonsoft.Json; using Newtonsoft.Json;
@ -20,160 +15,89 @@ public class Manga
[StringLength(64)] [StringLength(64)]
[Required] [Required]
public string MangaId { get; init; } public string MangaId { get; init; }
[StringLength(256)] [StringLength(256)] [Required] public string IdOnConnectorSite { get; init; }
[StringLength(512)] [Required] public string Name { get; internal set; }
[Required] public string Description { get; internal set; }
[Url] [StringLength(512)] [Required] public string WebsiteUrl { get; internal init; }
[JsonIgnore] [Url] [StringLength(512)] public string CoverUrl { get; internal set; }
[Required] public MangaReleaseStatus ReleaseStatus { get; internal set; }
[StringLength(64)]
public string? LibraryId { get; init; }
[JsonIgnore] public LocalLibrary? Library { get; internal set; }
[StringLength(32)]
[Required] [Required]
public string IdOnConnectorSite { get; init; } public string MangaConnectorName { get; init; }
[StringLength(512)] [JsonIgnore] public MangaConnector MangaConnector { get; init; } = null!;
[Required]
public string Name { get; internal set; } public ICollection<Author> Authors { get; internal set; }= null!;
[Required] public ICollection<MangaTag> MangaTags { get; internal set; }= null!;
public string Description { get; internal set; } public ICollection<Link> Links { get; internal set; }= null!;
[Url] public ICollection<MangaAltTitle> AltTitles { get; internal set; } = null!;
[StringLength(512)] [Required] public float IgnoreChaptersBefore { get; internal set; }
[Required] [StringLength(1024)] [Required] public string DirectoryName { get; private set; }
public string WebsiteUrl { get; internal set; }
[JsonIgnore] [JsonIgnore] [StringLength(512)] public string? CoverFileNameInCache { get; internal set; } = null;
[Url] [Required] public uint? Year { get; internal init; }
public string CoverUrl { get; internal set; } [StringLength(8)] public string? OriginalLanguage { get; internal init; }
[JsonIgnore]
public string? CoverFileNameInCache { get; internal set; }
[Required]
public uint Year { get; internal set; }
[StringLength(8)]
public string? OriginalLanguage { get; internal set; }
[Required]
public MangaReleaseStatus ReleaseStatus { get; internal set; }
[StringLength(1024)]
[Required]
public string DirectoryName { get; private set; }
public LocalLibrary? Library { get; internal set; }
[JsonIgnore] [JsonIgnore]
[NotMapped] [NotMapped]
public string LibraryPath => Library is null ? TrangaSettings.downloadLocation : Library.BasePath; public string? FullDirectoryPath => Library is not null ? Path.Join(Library.BasePath, DirectoryName) : null;
[JsonIgnore]
[NotMapped]
public string FullDirectoryPath => Path.Join(LibraryPath, DirectoryName);
[Required]
public float IgnoreChapterBefore { get; internal set; }
[StringLength(64)]
[Required]
public string MangaConnectorId { get; private set; }
[JsonIgnore] public MangaConnector? MangaConnector { get; private set; }
[JsonIgnore] public ICollection<Author>? Authors { get; internal set; } [JsonIgnore] public ICollection<Chapter> Chapters { get; internal set; } = [];
[NotMapped]
[StringLength(64)]
[Required]
public IEnumerable<string> AuthorIds => Authors?.Select(a => a.AuthorId) ?? [];
[JsonIgnore] public ICollection<MangaTag>? MangaTags { get; internal set; } public Manga(string idOnConnector, string name, string description, string websiteUrl, string coverUrl, MangaReleaseStatus releaseStatus,
[NotMapped] MangaConnector mangaConnector, ICollection<Author> authors, ICollection<MangaTag> mangaTags, ICollection<Link> links, ICollection<MangaAltTitle> altTitles,
[StringLength(64)] LocalLibrary? library = null, float ignoreChaptersBefore = 0f, uint? year = null, string? originalLanguage = null)
[Required]
public IEnumerable<string> Tags => MangaTags?.Select(t => t.Tag) ?? [];
[JsonIgnore] public ICollection<Link>? Links { get; internal set; }
[NotMapped]
[StringLength(64)]
[Required]
public IEnumerable<string> LinkIds => Links?.Select(l => l.LinkId) ?? [];
[JsonIgnore] public ICollection<MangaAltTitle>? AltTitles { get; internal set; }
[NotMapped]
[StringLength(64)]
[Required]
public IEnumerable<string> AltTitleIds => AltTitles?.Select(a => a.AltTitleId) ?? [];
public Manga(string idOnConnectorSite, string name, string description, string websiteUrl, string coverUrl,
string? coverFileNameInCache, uint year, string? originalLanguage, MangaReleaseStatus releaseStatus,
float ignoreChapterBefore, MangaConnector mangaConnector, ICollection<Author> authors,
ICollection<MangaTag> mangaTags, ICollection<Link> links, ICollection<MangaAltTitle> altTitles,
LocalLibrary? library = null)
: this(idOnConnectorSite, name, description, websiteUrl, coverUrl, coverFileNameInCache, year, originalLanguage,
releaseStatus, ignoreChapterBefore, mangaConnector.Name)
{ {
this.MangaId = TokenGen.CreateToken(typeof(Manga), mangaConnector.Name, idOnConnector);
this.IdOnConnectorSite = idOnConnector;
this.Name = name;
this.Description = description;
this.WebsiteUrl = websiteUrl;
this.CoverUrl = coverUrl;
this.ReleaseStatus = releaseStatus;
this.LibraryId = library?.LocalLibraryId;
this.Library = library;
this.MangaConnectorName = mangaConnector.Name;
this.MangaConnector = mangaConnector;
this.Authors = authors; this.Authors = authors;
this.MangaTags = mangaTags; this.MangaTags = mangaTags;
this.Links = links; this.Links = links;
this.AltTitles = altTitles; this.AltTitles = altTitles;
this.Library = library; this.IgnoreChaptersBefore = ignoreChaptersBefore;
this.DirectoryName = CleanDirectoryName(name);
this.Year = year;
this.OriginalLanguage = originalLanguage;
} }
public Manga(string idOnConnectorSite, string name, string description, string websiteUrl, string coverUrl, /// <summary>
string? coverFileNameInCache, uint year, string? originalLanguage, MangaReleaseStatus releaseStatus, /// EF ONLY!!!
float ignoreChapterBefore, string mangaConnectorId) /// </summary>
public Manga(string mangaId, string idOnConnectorSite, string name, string description, string websiteUrl, string coverUrl, MangaReleaseStatus releaseStatus,
string mangaConnectorName, string directoryName, float ignoreChaptersBefore, string? libraryId, uint? year, string? originalLanguage)
{ {
MangaId = TokenGen.CreateToken(typeof(Manga), mangaConnectorId, idOnConnectorSite); this.MangaId = mangaId;
IdOnConnectorSite = idOnConnectorSite; this.IdOnConnectorSite = idOnConnectorSite;
Name = name; this.Name = name;
Description = description; this.Description = description;
WebsiteUrl = websiteUrl; this.WebsiteUrl = websiteUrl;
CoverUrl = coverUrl; this.CoverUrl = coverUrl;
CoverFileNameInCache = coverFileNameInCache; this.ReleaseStatus = releaseStatus;
Year = year; this.MangaConnectorName = mangaConnectorName;
OriginalLanguage = originalLanguage; this.DirectoryName = directoryName;
ReleaseStatus = releaseStatus; this.LibraryId = libraryId;
IgnoreChapterBefore = ignoreChapterBefore; this.IgnoreChaptersBefore = ignoreChaptersBefore;
MangaConnectorId = mangaConnectorId; this.Year = year;
DirectoryName = BuildFolderName(name); this.OriginalLanguage = originalLanguage;
} }
public MoveFileOrFolderJob UpdateFolderName(string downloadLocation, string newName)
{
string oldName = this.DirectoryName;
this.DirectoryName = newName;
return new MoveFileOrFolderJob(Path.Join(downloadLocation, oldName), Path.Join(downloadLocation, this.DirectoryName));
}
internal void UpdateWithInfo(Manga other)
{
this.Name = other.Name;
this.Year = other.Year;
this.Description = other.Description;
this.CoverUrl = other.CoverUrl;
this.OriginalLanguage = other.OriginalLanguage;
this.Authors = other.Authors;
this.Links = other.Links;
this.MangaTags = other.MangaTags;
this.AltTitles = other.AltTitles;
this.ReleaseStatus = other.ReleaseStatus;
}
private static string BuildFolderName(string mangaName)
{
return mangaName;
}
internal string? SaveCoverImageToCache(int retries = 3)
{
if(retries < 0)
return null;
Regex urlRex = new (@"https?:\/\/((?:[a-zA-Z0-9-]+\.)+[a-zA-Z0-9]+)\/(?:.+\/)*(.+\.([a-zA-Z]+))");
//https?:\/\/[a-zA-Z0-9-]+\.([a-zA-Z0-9-]+\.[a-zA-Z0-9]+)\/(?:.+\/)*(.+\.([a-zA-Z]+)) for only second level domains
Match match = urlRex.Match(CoverUrl);
string filename = $"{match.Groups[1].Value}-{MangaId}.{match.Groups[3].Value}";
string saveImagePath = Path.Join(TrangaSettings.coverImageCache, filename);
if (File.Exists(saveImagePath))
return saveImagePath;
RequestResult coverResult = new HttpDownloadClient().MakeRequest(CoverUrl, RequestType.MangaCover, $"https://{match.Groups[1].Value}");
if (coverResult.statusCode is < HttpStatusCode.OK or >= HttpStatusCode.Ambiguous)
return SaveCoverImageToCache(--retries);
using MemoryStream ms = new();
coverResult.result.CopyTo(ms);
Directory.CreateDirectory(TrangaSettings.coverImageCache);
File.WriteAllBytes(saveImagePath, ms.ToArray());
return saveImagePath;
}
public string CreatePublicationFolder() public string CreatePublicationFolder()
{ {
string publicationFolder = Path.Join(LibraryPath, this.DirectoryName); string publicationFolder = FullDirectoryPath;
if(!Directory.Exists(publicationFolder)) if(!Directory.Exists(publicationFolder))
Directory.CreateDirectory(publicationFolder); Directory.CreateDirectory(publicationFolder);
if(RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) if(RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
@ -181,5 +105,39 @@ public class Manga
return publicationFolder; return publicationFolder;
} }
//TODO onchanges create job to update metadata files in archives, etc. //https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file
//less than 32 is control *forbidden*
//34 is " *forbidden*
//42 is * *forbidden*
//47 is / *forbidden*
//58 is : *forbidden*
//60 is < *forbidden*
//62 is > *forbidden*
//63 is ? *forbidden*
//92 is \ *forbidden*
//124 is | *forbidden*
//127 is delete *forbidden*
//Below 127 all except *******
private static readonly int[] ForbiddenCharsBelow127 = [34, 42, 47, 58, 60, 62, 63, 92, 124, 127];
//Above 127 none except *******
private static readonly int[] IncludeCharsAbove127 = [128, 138, 142];
//128 is € include
//138 is Š include
//142 is Ž include
//152 through 255 looks fine except 157, 172, 173, 175 *******
private static readonly int[] ForbiddenCharsAbove152 = [157, 172, 173, 175];
private static string CleanDirectoryName(string name)
{
StringBuilder sb = new ();
foreach (char c in name)
{
if (c > 32 && c < 127 && ForbiddenCharsBelow127.Contains(c) == false)
sb.Append(c);
else if (c > 127 && c < 152 && IncludeCharsAbove127.Contains(c))
sb.Append(c);
else if(c >= 152 && c <= 255 && ForbiddenCharsAbove152.Contains(c) == false)
sb.Append(c);
}
return sb.ToString();
}
} }

View File

@ -1,5 +1,4 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
namespace API.Schema; namespace API.Schema;

View File

@ -1,191 +0,0 @@
using System.Text.RegularExpressions;
using API.MangaDownloadClients;
using HtmlAgilityPack;
using log4net;
namespace API.Schema.MangaConnectors;
public class AsuraToon : MangaConnector
{
public AsuraToon() : base("AsuraToon", ["en"], ["asuracomic.net"], "https://asuracomic.net/images/logo.webp")
{
this.downloadClient = new ChromiumDownloadClient();
}
public override (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)[] GetManga(string publicationTitle = "")
{
string sanitizedTitle = string.Join(' ', Regex.Matches(publicationTitle, "[A-z]*").Where(m => m.Value.Length > 0)).ToLower();
string requestUrl = $"https://asuracomic.net/series?name={sanitizedTitle}";
RequestResult requestResult =
downloadClient.MakeRequest(requestUrl, RequestType.Default);
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
return [];
if (requestResult.htmlDocument is null)
{
return [];
}
(Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)[] publications = ParsePublicationsFromHtml(requestResult.htmlDocument);
return publications;
}
public override (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)? GetMangaFromId(string publicationId)
{
return GetMangaFromUrl($"https://asuracomic.net/series/{publicationId}");
}
public override (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)? GetMangaFromUrl(string url)
{
RequestResult requestResult = downloadClient.MakeRequest(url, RequestType.MangaInfo);
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
return null;
if (requestResult.htmlDocument is null)
{
return null;
}
return ParseSinglePublicationFromHtml(requestResult.htmlDocument, url.Split('/')[^1], url);
}
private (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)[] ParsePublicationsFromHtml(HtmlDocument document)
{
HtmlNodeCollection mangaList = document.DocumentNode.SelectNodes("//a[starts-with(@href,'series')]");
if (mangaList is null || mangaList.Count < 1)
return [];
IEnumerable<string> urls = mangaList.Select(a => $"https://asuracomic.net/{a.GetAttributeValue("href", "")}");
List<(Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)> ret = new();
foreach (string url in urls)
{
(Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)? manga = GetMangaFromUrl(url);
if (manga is { } x)
ret.Add(x);
}
return ret.ToArray();
}
private (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?) ParseSinglePublicationFromHtml(HtmlDocument document, string publicationId, string websiteUrl)
{
string? originalLanguage = null;
Dictionary<string, string> altTitles = new(), links = new();
HtmlNodeCollection genreNodes = document.DocumentNode.SelectNodes("//h3[text()='Genres']/../div/button");
string[] tags = genreNodes.Select(b => b.InnerText).ToArray();
List<MangaTag> mangaTags = tags.Select(t => new MangaTag(t)).ToList();
HtmlNode statusNode = document.DocumentNode.SelectSingleNode("//h3[text()='Status']/../h3[2]");
MangaReleaseStatus releaseStatus = statusNode.InnerText.ToLower() switch
{
"ongoing" => MangaReleaseStatus.Continuing,
"hiatus" => MangaReleaseStatus.OnHiatus,
"completed" => MangaReleaseStatus.Completed,
"dropped" => MangaReleaseStatus.Cancelled,
"season end" => MangaReleaseStatus.Continuing,
"coming soon" => MangaReleaseStatus.Unreleased,
_ => MangaReleaseStatus.Unreleased
};
HtmlNode coverNode =
document.DocumentNode.SelectSingleNode("//img[@alt='poster']");
string coverUrl = coverNode.GetAttributeValue("src", "");
HtmlNode titleNode =
document.DocumentNode.SelectSingleNode("//title");
string sortName = Regex.Match(titleNode.InnerText, @"(.*) - Asura Scans").Groups[1].Value;
HtmlNode descriptionNode =
document.DocumentNode.SelectSingleNode("//h3[starts-with(text(),'Synopsis')]/../span");
string description = descriptionNode?.InnerText??"";
HtmlNodeCollection authorNodes = document.DocumentNode.SelectNodes("//h3[text()='Author']/../h3[not(text()='Author' or text()='_')]");
HtmlNodeCollection artistNodes = document.DocumentNode.SelectNodes("//h3[text()='Artist']/../h3[not(text()='Artist' or text()='_')]");
IEnumerable<string> authorNames = authorNodes is null ? [] : authorNodes.Select(a => a.InnerText);
IEnumerable<string> artistNames = artistNodes is null ? [] : artistNodes.Select(a => a.InnerText);
List<string> authorStrings = authorNames.Concat(artistNames).ToList();
List<Author> authors = authorStrings.Select(author => new Author(author)).ToList();
HtmlNode? firstChapterNode = document.DocumentNode.SelectSingleNode("//a[contains(@href, 'chapter/1')]/../following-sibling::h3");
uint year = uint.Parse(firstChapterNode?.InnerText.Split(' ')[^1] ?? "2000");
Manga manga = new (publicationId, sortName, description, websiteUrl, coverUrl, null, year,
originalLanguage, releaseStatus, -1,
this,
authors,
mangaTags,
[],
[]);
return (manga, authors, mangaTags, [], []);
}
public override Chapter[] GetChapters(Manga manga, string language="en")
{
string requestUrl = $"https://asuracomic.net/series/{manga.MangaId}";
// Leaving this in for verification if the page exists
RequestResult requestResult =
downloadClient.MakeRequest(requestUrl, RequestType.Default);
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
return [];
//Return Chapters ordered by Chapter-Number
List<Chapter> chapters = ParseChaptersFromHtml(manga, requestUrl);
return chapters.Order().ToArray();
}
private List<Chapter> ParseChaptersFromHtml(Manga manga, string mangaUrl)
{
RequestResult result = downloadClient.MakeRequest(mangaUrl, RequestType.Default);
if ((int)result.statusCode < 200 || (int)result.statusCode >= 300 || result.htmlDocument is null)
{
return new List<Chapter>();
}
List<Chapter> ret = new();
HtmlNodeCollection chapterURLNodes = result.htmlDocument.DocumentNode.SelectNodes("//a[contains(@href, '/chapter/')]");
Regex infoRex = new(@"Chapter ([0-9]+)(.*)?");
foreach (HtmlNode chapterInfo in chapterURLNodes)
{
string chapterUrl = chapterInfo.GetAttributeValue("href", "");
Match match = infoRex.Match(chapterInfo.InnerText);
string chapterNumber = new(match.Groups[1].Value);
string? chapterName = match.Groups[2].Success && match.Groups[2].Length > 1 ? match.Groups[2].Value : null;
string url = $"https://asuracomic.net/series/{chapterUrl}";
try
{
ret.Add(new Chapter(manga, url, chapterNumber, null, chapterName));
}
catch (Exception e)
{
}
}
return ret;
}
internal override string[] GetChapterImageUrls(Chapter chapter)
{
string requestUrl = chapter.Url;
// Leaving this in to check if the page exists
RequestResult requestResult =
downloadClient.MakeRequest(requestUrl, RequestType.Default);
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300 || requestResult.htmlDocument is null)
{
return [];
}
string[] imageUrls = ParseImageUrlsFromHtml(requestResult.htmlDocument);
return imageUrls;
}
private string[] ParseImageUrlsFromHtml(HtmlDocument document)
{
HtmlNodeCollection images = document.DocumentNode.SelectNodes("//img[contains(@alt, 'chapter page')]");
return images.Select(i => i.GetAttributeValue("src", "")).ToArray();
}
}

View File

@ -1,203 +0,0 @@
using System.Net;
using System.Text.RegularExpressions;
using API.MangaDownloadClients;
using HtmlAgilityPack;
namespace API.Schema.MangaConnectors;
public class Bato : MangaConnector
{
public Bato() : base("Bato", ["en"], ["bato.to"], "https://bato.to/amsta/img/batoto/favicon.ico")
{
this.downloadClient = new HttpDownloadClient();
}
public override (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)[] GetManga(string publicationTitle = "")
{
string sanitizedTitle = string.Join(' ', Regex.Matches(publicationTitle, "[A-z]*").Where(m => m.Value.Length > 0)).ToLower();
string requestUrl = $"https://bato.to/v3x-search?word={sanitizedTitle}&lang=en";
RequestResult requestResult =
downloadClient.MakeRequest(requestUrl, RequestType.Default);
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
return [];
if (requestResult.htmlDocument is null)
{
return [];
}
(Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)[] publications = ParsePublicationsFromHtml(requestResult.htmlDocument);
return publications;
}
public override (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)? GetMangaFromId(string publicationId)
{
return GetMangaFromUrl($"https://bato.to/title/{publicationId}");
}
public override (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)? GetMangaFromUrl(string url)
{
RequestResult requestResult = downloadClient.MakeRequest(url, RequestType.MangaInfo);
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
return null;
if (requestResult.htmlDocument is null)
{
return null;
}
return ParseSinglePublicationFromHtml(requestResult.htmlDocument, url.Split('/')[^1], url);
}
private (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)[] ParsePublicationsFromHtml(HtmlDocument document)
{
HtmlNode mangaList = document.DocumentNode.SelectSingleNode("//div[@data-hk='0-0-2']");
if (!mangaList.ChildNodes.Any(node => node.Name == "div"))
return [];
List<string> urls = mangaList.ChildNodes
.Select(node => $"https://bato.to{node.Descendants("div").First().FirstChild.GetAttributeValue("href", "")}").ToList();
HashSet<(Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)> ret = new();
foreach (string url in urls)
{
(Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)? manga = GetMangaFromUrl(url);
if (manga is { } x)
ret.Add(x);
}
return ret.ToArray();
}
private (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?) ParseSinglePublicationFromHtml(HtmlDocument document, string publicationId, string websiteUrl)
{
HtmlNode infoNode = document.DocumentNode.SelectSingleNode("/html/body/div/main/div[1]/div[2]");
string sortName = infoNode.Descendants("h3").First().InnerText;
string description = document.DocumentNode
.SelectSingleNode("//div[contains(concat(' ',normalize-space(@class),' '),'prose')]").InnerText;
string[] altTitlesList = infoNode.ChildNodes[1].ChildNodes[2].InnerText.Split('/');
int i = 0;
List<MangaAltTitle> altTitles = altTitlesList.Select(a => new MangaAltTitle(i++.ToString(), a)).ToList();
string coverUrl = document.DocumentNode.SelectNodes("//img")
.First(child => child.GetAttributeValue("data-hk", "") == "0-1-0").GetAttributeValue("src", "").Replace("&amp;", "&");
List<HtmlNode> genreNodes = document.DocumentNode.SelectSingleNode("//b[text()='Genres:']/..").SelectNodes("span").ToList();
string[] tags = genreNodes.Select(node => node.FirstChild.InnerText).ToArray();
List<MangaTag> mangaTags = tags.Select(s => new MangaTag(s)).ToList();
List<HtmlNode> authorsNodes = infoNode.ChildNodes[1].ChildNodes[3].Descendants("a").ToList();
List<string> authorNames = authorsNodes.Select(node => node.InnerText.Replace("amp;", "")).ToList();
List<Author> authors = authorNames.Select(n => new Author(n)).ToList();
HtmlNode? originalLanguageNode = document.DocumentNode.SelectSingleNode("//span[text()='Tr From']/..");
string originalLanguage = originalLanguageNode is not null ? originalLanguageNode.LastChild.InnerText : "";
if (!uint.TryParse(
document.DocumentNode.SelectSingleNode("//span[text()='Original Publication:']/..").LastChild.InnerText.Split('-')[0],
out uint year))
year = (uint)DateTime.UtcNow.Year;
string status = document.DocumentNode.SelectSingleNode("//span[text()='Original Publication:']/..")
.ChildNodes[2].InnerText;
MangaReleaseStatus releaseStatus = MangaReleaseStatus.Unreleased;
switch (status.ToLower())
{
case "ongoing": releaseStatus = MangaReleaseStatus.Continuing; break;
case "completed": releaseStatus = MangaReleaseStatus.Completed; break;
case "hiatus": releaseStatus = MangaReleaseStatus.OnHiatus; break;
case "cancelled": releaseStatus = MangaReleaseStatus.Cancelled; break;
case "pending": releaseStatus = MangaReleaseStatus.Unreleased; break;
}
Manga manga = new (publicationId, sortName, description, websiteUrl, coverUrl, null, year,
originalLanguage, releaseStatus, -1,
this,
authors,
mangaTags,
[],
altTitles);
return (manga, authors, mangaTags, [], altTitles);
}
public override Chapter[] GetChapters(Manga manga, string language="en")
{
string requestUrl = $"https://bato.to/title/{manga.MangaId}";
// Leaving this in for verification if the page exists
RequestResult requestResult =
downloadClient.MakeRequest(requestUrl, RequestType.Default);
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
return [];
//Return Chapters ordered by Chapter-Number
List<Chapter> chapters = ParseChaptersFromHtml(manga, requestUrl);
return chapters.Order().ToArray();
}
private List<Chapter> ParseChaptersFromHtml(Manga manga, string mangaUrl)
{
RequestResult result = downloadClient.MakeRequest(mangaUrl, RequestType.Default);
if ((int)result.statusCode < 200 || (int)result.statusCode >= 300 || result.htmlDocument is null)
{
return new List<Chapter>();
}
List<Chapter> ret = new();
HtmlNode chapterList =
result.htmlDocument.DocumentNode.SelectSingleNode("/html/body/div/main/div[3]/astro-island/div/div[2]/div/div/astro-slot");
Regex numberRex = new(@"\/title\/.+\/([0-9])+(?:-vol_([0-9]+))?-ch_([0-9\.]+)");
foreach (HtmlNode chapterInfo in chapterList.SelectNodes("div"))
{
HtmlNode infoNode = chapterInfo.FirstChild.FirstChild;
string chapterUrl = infoNode.GetAttributeValue("href", "");
Match match = numberRex.Match(chapterUrl);
string id = match.Groups[1].Value;
int? volumeNumber = match.Groups[2].Success ? int.Parse(match.Groups[2].Value) : null;
string chapterNumber = new(match.Groups[3].Value);
string url = $"https://bato.to{chapterUrl}?load=2";
try
{
ret.Add(new Chapter(manga, url, chapterNumber, volumeNumber, null));
}
catch (Exception e)
{
}
}
return ret;
}
internal override string[] GetChapterImageUrls(Chapter chapter)
{
string requestUrl = chapter.Url;
// Leaving this in to check if the page exists
RequestResult requestResult =
downloadClient.MakeRequest(requestUrl, RequestType.Default);
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300 || requestResult.htmlDocument is null)
{
return [];
}
string[] imageUrls = ParseImageUrlsFromHtml(requestResult.htmlDocument);
return imageUrls;
}
private string[] ParseImageUrlsFromHtml(HtmlDocument document)
{
HtmlNode images = document.DocumentNode.SelectNodes("//astro-island").First(node =>
node.GetAttributeValue("component-url", "").Contains("/_astro/ImageList."));
string weirdString = images.OuterHtml;
string weirdString2 = Regex.Match(weirdString, @"props=\""(.*)}\""").Groups[1].Value;
string[] urls = Regex.Matches(weirdString2, @"(https:\/\/[A-z\-0-9\.\?\&\;\=\/]+)\\")
.Select(match => match.Groups[1].Value.Replace("&amp;", "&")).ToArray();
return urls;
}
}

View File

@ -8,15 +8,14 @@ public class Global : MangaConnector
this.context = context; this.context = context;
} }
public override (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)[] GetManga(string publicationTitle = "") public override Manga[] SearchManga(string mangaSearchName)
{ {
//Get all enabled Connectors //Get all enabled Connectors
MangaConnector[] enabledConnectors = context.MangaConnectors.Where(c => c.Enabled && c.Name != "Global").ToArray(); MangaConnector[] enabledConnectors = context.MangaConnectors.Where(c => c.Enabled && c.Name != "Global").ToArray();
//Create Task for each MangaConnector to search simulatneously //Create Task for each MangaConnector to search simulatneously
Task<(Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)[]>[] tasks = Task<Manga[]>[] tasks =
enabledConnectors.Select(c => enabledConnectors.Select(c => new Task<Manga[]>(() => c.SearchManga(mangaSearchName))).ToArray();
new Task<(Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)[]>(() => c.GetManga(publicationTitle))).ToArray();
foreach (var task in tasks) foreach (var task in tasks)
task.Start(); task.Start();
@ -27,29 +26,28 @@ public class Global : MangaConnector
}while(tasks.Any(t => t.Status < TaskStatus.RanToCompletion)); }while(tasks.Any(t => t.Status < TaskStatus.RanToCompletion));
//Concatenate all results into one //Concatenate all results into one
(Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)[] ret = Manga[] ret = tasks.Select(t => t.IsCompletedSuccessfully ? t.Result : []).ToArray().SelectMany(i => i).ToArray();
tasks.Select(t => t.IsCompletedSuccessfully ? t.Result : []).ToArray().SelectMany(i => i).ToArray();
return ret; return ret;
} }
public override (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)? GetMangaFromUrl(string url) public override Manga? GetMangaFromUrl(string url)
{ {
MangaConnector? mc = context.MangaConnectors.ToArray().FirstOrDefault(c => c.ValidateUrl(url)); MangaConnector? mc = context.MangaConnectors.ToArray().FirstOrDefault(c => c.UrlMatchesConnector(url));
return mc?.GetMangaFromUrl(url) ?? null; return mc?.GetMangaFromUrl(url) ?? null;
} }
public override (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)? GetMangaFromId(string publicationId) public override Manga? GetMangaFromId(string mangaIdOnSite)
{ {
return null; return null;
} }
public override Chapter[] GetChapters(Manga manga, string language = "en") public override Chapter[] GetChapters(Manga manga, string? language = null)
{ {
return manga.MangaConnector?.GetChapters(manga) ?? []; return manga.MangaConnector.GetChapters(manga, language);
} }
internal override string[] GetChapterImageUrls(Chapter chapter) internal override string[] GetChapterImageUrls(Chapter chapter)
{ {
return chapter.ParentManga?.MangaConnector?.GetChapterImageUrls(chapter) ?? []; return chapter.ParentManga.MangaConnector.GetChapterImageUrls(chapter);
} }
} }

View File

@ -11,6 +11,14 @@ namespace API.Schema.MangaConnectors;
[PrimaryKey("Name")] [PrimaryKey("Name")]
public abstract class MangaConnector(string name, string[] supportedLanguages, string[] baseUris, string iconUrl) public abstract class MangaConnector(string name, string[] supportedLanguages, string[] baseUris, string iconUrl)
{ {
[JsonIgnore]
[NotMapped]
internal DownloadClient downloadClient { get; init; } = null!;
[JsonIgnore]
[NotMapped]
protected ILog Log { get; init; } = LogManager.GetLogger(name);
[StringLength(32)] [StringLength(32)]
[Required] [Required]
public string Name { get; init; } = name; public string Name { get; init; } = name;
@ -26,32 +34,41 @@ public abstract class MangaConnector(string name, string[] supportedLanguages, s
[Required] [Required]
public bool Enabled { get; internal set; } = true; public bool Enabled { get; internal set; } = true;
public abstract (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)[] GetManga(string publicationTitle = ""); public abstract Manga[] SearchManga(string mangaSearchName);
public abstract (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)? GetMangaFromUrl(string url); public abstract Manga? GetMangaFromUrl(string url);
public abstract (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)? GetMangaFromId(string publicationId); public abstract Manga? GetMangaFromId(string mangaIdOnSite);
public abstract Chapter[] GetChapters(Manga manga, string language="en"); public abstract Chapter[] GetChapters(Manga manga, string? language = null);
[JsonIgnore]
[NotMapped]
internal DownloadClient downloadClient { get; init; } = null!;
[JsonIgnore]
[NotMapped]
protected ILog Log { get; init; } = LogManager.GetLogger(name);
public Chapter[] GetNewChapters(Manga manga)
{
Chapter[] allChapters = GetChapters(manga);
if (allChapters.Length < 1)
return [];
return allChapters.Where(chapter => !chapter.IsDownloaded()).ToArray();
}
internal abstract string[] GetChapterImageUrls(Chapter chapter); internal abstract string[] GetChapterImageUrls(Chapter chapter);
public bool ValidateUrl(string url) => BaseUris.Any(baseUri => Regex.IsMatch(url, "https?://" + baseUri + "/.*")); public bool UrlMatchesConnector(string url) => BaseUris.Any(baseUri => Regex.IsMatch(url, "https?://" + baseUri + "/.*"));
internal string? SaveCoverImageToCache(Manga manga, int retries = 3)
{
if(retries < 0)
return null;
Regex urlRex = new (@"https?:\/\/((?:[a-zA-Z0-9-]+\.)+[a-zA-Z0-9]+)\/(?:.+\/)*(.+\.([a-zA-Z]+))");
//https?:\/\/[a-zA-Z0-9-]+\.([a-zA-Z0-9-]+\.[a-zA-Z0-9]+)\/(?:.+\/)*(.+\.([a-zA-Z]+)) for only second level domains
Match match = urlRex.Match(manga.CoverUrl);
string filename = $"{match.Groups[1].Value}-{manga.MangaId}.{match.Groups[3].Value}";
string saveImagePath = Path.Join(TrangaSettings.coverImageCache, filename);
if (File.Exists(saveImagePath))
return saveImagePath;
RequestResult coverResult = downloadClient.MakeRequest(manga.CoverUrl, RequestType.MangaCover, $"https://{match.Groups[1].Value}");
if ((int)coverResult.statusCode < 200 || (int)coverResult.statusCode >= 300)
return SaveCoverImageToCache(manga, --retries);
using MemoryStream ms = new();
coverResult.result.CopyTo(ms);
Directory.CreateDirectory(TrangaSettings.coverImageCache);
File.WriteAllBytes(saveImagePath, ms.ToArray());
return saveImagePath;
}
} }

View File

@ -1,8 +1,6 @@
using System.Net; using System.Text.RegularExpressions;
using System.Text.Json.Nodes;
using System.Text.RegularExpressions;
using API.MangaDownloadClients; using API.MangaDownloadClients;
using JsonSerializer = System.Text.Json.JsonSerializer; using Newtonsoft.Json.Linq;
namespace API.Schema.MangaConnectors; namespace API.Schema.MangaConnectors;
@ -11,313 +9,327 @@ public class MangaDex : MangaConnector
//https://api.mangadex.org/docs/3-enumerations/#language-codes--localization //https://api.mangadex.org/docs/3-enumerations/#language-codes--localization
//https://en.wikipedia.org/wiki/List_of_ISO_639_language_codes //https://en.wikipedia.org/wiki/List_of_ISO_639_language_codes
//https://gist.github.com/Josantonius/b455e315bc7f790d14b136d61d9ae469 //https://gist.github.com/Josantonius/b455e315bc7f790d14b136d61d9ae469
public MangaDex() : base("MangaDex", ["en","pt","pt-br","it","de","ru","aa","ab","ae","af","ak","am","an","ar-ae","ar-bh","ar-dz","ar-eg","ar-iq","ar-jo","ar-kw","ar-lb","ar-ly","ar-ma","ar-om","ar-qa","ar-sa","ar-sy","ar-tn","ar-ye","ar","as","av","ay","az","ba","be","bg","bh","bi","bm","bn","bo","br","bs","ca","ce","ch","co","cr","cs","cu","cv","cy","da","de-at","de-ch","de-de","de-li","de-lu","div","dv","dz","ee","el","en-au","en-bz","en-ca","en-cb","en-gb","en-ie","en-jm","en-nz","en-ph","en-tt","en-us","en-za","en-zw","eo","es-ar","es-bo","es-cl","es-co","es-cr","es-do","es-ec","es-es","es-gt","es-hn","es-la","es-mx","es-ni","es-pa","es-pe","es-pr","es-py","es-sv","es-us","es-uy","es-ve","es","et","eu","fa","ff","fi","fj","fo","fr-be","fr-ca","fr-ch","fr-fr","fr-lu","fr-mc","fr","fy","ga","gd","gl","gn","gu","gv","ha","he","hi","ho","hr-ba","hr-hr","hr","ht","hu","hy","hz","ia","id","ie","ig","ii","ik","in","io","is","it-ch","it-it","iu","iw","ja","ja-ro","ji","jv","jw","ka","kg","ki","kj","kk","kl","km","kn","ko","ko-ro","kr","ks","ku","kv","kw","ky","kz","la","lb","lg","li","ln","lo","ls","lt","lu","lv","mg","mh","mi","mk","ml","mn","mo","mr","ms-bn","ms-my","ms","mt","my","na","nb","nd","ne","ng","nl-be","nl-nl","nl","nn","no","nr","ns","nv","ny","oc","oj","om","or","os","pa","pi","pl","ps","pt-pt","qu-bo","qu-ec","qu-pe","qu","rm","rn","ro","rw","sa","sb","sc","sd","se-fi","se-no","se-se","se","sg","sh","si","sk","sl","sm","sn","so","sq","sr-ba","sr-sp","sr","ss","st","su","sv-fi","sv-se","sv","sw","sx","syr","ta","te","tg","th","ti","tk","tl","tn","to","tr","ts","tt","tw","ty","ug","uk","ur","us","uz","ve","vi","vo","wa","wo","xh","yi","yo","za","zh-cn","zh-hk","zh-mo","zh-ro","zh-sg","zh-tw","zh","zu"], ["mangadex.org"], "https://mangadex.org/favicon.ico") public MangaDex() : base("MangaDex",
["en","pt","pt-br","it","de","ru","aa","ab","ae","af","ak","am","an","ar-ae","ar-bh","ar-dz","ar-eg","ar-iq","ar-jo","ar-kw","ar-lb","ar-ly","ar-ma","ar-om","ar-qa","ar-sa","ar-sy","ar-tn","ar-ye","ar","as","av","ay","az","ba","be","bg","bh","bi","bm","bn","bo","br","bs","ca","ce","ch","co","cr","cs","cu","cv","cy","da","de-at","de-ch","de-de","de-li","de-lu","div","dv","dz","ee","el","en-au","en-bz","en-ca","en-cb","en-gb","en-ie","en-jm","en-nz","en-ph","en-tt","en-us","en-za","en-zw","eo","es-ar","es-bo","es-cl","es-co","es-cr","es-do","es-ec","es-es","es-gt","es-hn","es-la","es-mx","es-ni","es-pa","es-pe","es-pr","es-py","es-sv","es-us","es-uy","es-ve","es","et","eu","fa","ff","fi","fj","fo","fr-be","fr-ca","fr-ch","fr-fr","fr-lu","fr-mc","fr","fy","ga","gd","gl","gn","gu","gv","ha","he","hi","ho","hr-ba","hr-hr","hr","ht","hu","hy","hz","ia","id","ie","ig","ii","ik","in","io","is","it-ch","it-it","iu","iw","ja","ja-ro","ji","jv","jw","ka","kg","ki","kj","kk","kl","km","kn","ko","ko-ro","kr","ks","ku","kv","kw","ky","kz","la","lb","lg","li","ln","lo","ls","lt","lu","lv","mg","mh","mi","mk","ml","mn","mo","mr","ms-bn","ms-my","ms","mt","my","na","nb","nd","ne","ng","nl-be","nl-nl","nl","nn","no","nr","ns","nv","ny","oc","oj","om","or","os","pa","pi","pl","ps","pt-pt","qu-bo","qu-ec","qu-pe","qu","rm","rn","ro","rw","sa","sb","sc","sd","se-fi","se-no","se-se","se","sg","sh","si","sk","sl","sm","sn","so","sq","sr-ba","sr-sp","sr","ss","st","su","sv-fi","sv-se","sv","sw","sx","syr","ta","te","tg","th","ti","tk","tl","tn","to","tr","ts","tt","tw","ty","ug","uk","ur","us","uz","ve","vi","vo","wa","wo","xh","yi","yo","za","zh-cn","zh-hk","zh-mo","zh-ro","zh-sg","zh-tw","zh","zu"],
["mangadex.org"],
"https://mangadex.org/favicon.ico")
{ {
this.downloadClient = new HttpDownloadClient(); this.downloadClient = new HttpDownloadClient();
} }
public override (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)[] GetManga(string publicationTitle = "") private const int Limit = 100;
public override Manga[] SearchManga(string mangaSearchName)
{ {
const int limit = 100; //How many values we want returned at once Log.Info($"Searching Manga: {mangaSearchName}");
int offset = 0; //"Page" List<Manga> mangas = new ();
int total = int.MaxValue; //How many total results are there, is updated on first request
HashSet<(Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)> retManga = new();
List<JsonNode> results = new();
//Request all search-results int offset = 0;
while (offset < total) //As long as we haven't requested all "Pages" int total = int.MaxValue;
while(offset < total)
{ {
//Request next Page
string requestUrl = string requestUrl =
$"https://api.mangadex.org/manga?limit={limit}&title={publicationTitle}&offset={offset}" + $"https://api.mangadex.org/manga?limit={Limit}&offset={offset}&title={mangaSearchName}" +
$"&includes[]=manga&includes[]=cover_art&includes[]=author&includes[]=artist&includes[]=tag"; $"&contentRating%5B%5D=safe&contentRating%5B%5D=suggestive&contentRating%5B%5D=erotica" +
RequestResult requestResult = downloadClient.MakeRequest(requestUrl, RequestType.MangaInfo); $"&includes%5B%5D=manga&includes%5B%5D=cover_art&includes%5B%5D=author&includes%5B%5D=artist&includes%5B%5D=tag'";
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300) offset += Limit;
{
Log.Info($"{requestResult.statusCode}: {requestUrl}");
break;
}
JsonObject? result = JsonSerializer.Deserialize<JsonObject>(requestResult.result);
offset += limit; RequestResult result = downloadClient.MakeRequest(requestUrl, RequestType.MangaDexFeed);
if (result is null) if ((int)result.statusCode < 200 || (int)result.statusCode >= 300)
{ {
Log.Info($"result was null: {requestUrl}"); Log.Error("Request failed");
break; return [];
} }
if(result.ContainsKey("total")) using StreamReader sr = new (result.result);
total = result["total"]!.GetValue<int>(); //Update the total number of Publications JObject jObject = JObject.Parse(sr.ReadToEnd());
else continue;
if (result.ContainsKey("data")) if (jObject.Value<string>("result") != "ok")
results.AddRange(result["data"]!.AsArray()!);//Manga-data-Array {
JArray? errors = jObject["errors"] as JArray;
Log.Error($"Request failed: {string.Join(',', errors?.Select(e => e.Value<string>("title")) ?? [])}");
return [];
} }
foreach (JsonNode mangaNode in results) total = jObject.Value<int>("total");
JArray? data = jObject.Value<JArray>("data");
if (data is null)
{ {
if(MangaFromJsonObject(mangaNode.AsObject()) is { } manga) Log.Error("Data was null");
retManga.Add(manga); //Add Publication (Manga) to result return [];
}
return retManga.ToArray();
} }
public override (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)? GetMangaFromId(string publicationId) mangas.AddRange(data.Select(ParseMangaFromJToken));
{
string url = $"https://api.mangadex.org/manga/{publicationId}" +
$"?includes%5B%5D=manga&includes%5B%5D=cover_art&includes%5B%5D=author&includes%5B%5D=artist&includes%5B%5D=tag";
RequestResult requestResult = downloadClient.MakeRequest(url, RequestType.MangaInfo);
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
{
Log.Info($"{requestResult.statusCode}: {url}");
return null;
} }
JsonObject? result = JsonSerializer.Deserialize<JsonObject>(requestResult.result);
if(result is not null) Log.Info($"Search {mangaSearchName} yielded {mangas.Count} results.");
return MangaFromJsonObject(result["data"]!.AsObject()); return mangas.ToArray();
Log.Info($"result was null: {url}"); }
private static readonly Regex GetMangaIdFromUrl = new(@"https?:\/\/mangadex\.org\/title\/([a-z0-9-]+)\/?.*");
public override Manga? GetMangaFromUrl(string url)
{
Log.Info($"Getting Manga: {url}");
if (!UrlMatchesConnector(url))
{
Log.Debug($"Url is not for Connector. {url}");
return null; return null;
} }
public override (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)? GetMangaFromUrl(string url) Match match = GetMangaIdFromUrl.Match(url);
if (!match.Success || !match.Groups[1].Success)
{ {
Regex idRex = new (@"https:\/\/mangadex.org\/title\/([A-z0-9-]*)\/.*"); Log.Debug($"Url is not for Connector (Could not retrieve id). {url}");
string id = idRex.Match(url).Groups[1].Value; return null;
}
string id = match.Groups[1].Value;
return GetMangaFromId(id); return GetMangaFromId(id);
} }
private (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)? MangaFromJsonObject(JsonObject manga) public override Manga? GetMangaFromId(string mangaIdOnSite)
{ {
if (!manga.TryGetPropertyValue("id", out JsonNode? idNode)) Log.Info($"Getting Manga: {mangaIdOnSite}");
{ string requestUrl =
Log.Info("id was null"); $"https://api.mangadex.org/manga/{mangaIdOnSite}" +
return null; $"?includes%5B%5D=manga&includes%5B%5D=cover_art&includes%5B%5D=author&includes%5B%5D=artist&includes%5B%5D=tag'";
}
string publicationId = idNode!.GetValue<string>();
if (!manga.TryGetPropertyValue("attributes", out JsonNode? attributesNode)) RequestResult result = downloadClient.MakeRequest(requestUrl, RequestType.MangaDexFeed);
if ((int)result.statusCode < 200 || (int)result.statusCode >= 300)
{ {
Log.Info("attributes was null"); Log.Error("Request failed");
return null;
}
JsonObject attributes = attributesNode!.AsObject();
if (!attributes.TryGetPropertyValue("title", out JsonNode? titleNode))
{
Log.Info("title was null");
return null;
}
string sortName = titleNode!.AsObject().ContainsKey("en") switch
{
true => titleNode.AsObject()["en"]!.GetValue<string>(),
false => titleNode.AsObject().First().Value!.GetValue<string>()
};
Dictionary<string, string> altTitlesDict = new();
if (attributes.TryGetPropertyValue("altTitles", out JsonNode? altTitlesNode))
{
foreach (JsonNode? altTitleNode in altTitlesNode!.AsArray())
{
JsonObject altTitleNodeObject = altTitleNode!.AsObject();
altTitlesDict.TryAdd(altTitleNodeObject.First().Key, altTitleNodeObject.First().Value!.GetValue<string>());
}
}
List<MangaAltTitle> altTitles = altTitlesDict.Select(t => new MangaAltTitle(t.Key, t.Value)).ToList();
if (!attributes.TryGetPropertyValue("description", out JsonNode? descriptionNode))
{
Log.Info("description was null");
return null;
}
string description = descriptionNode!.AsObject().ContainsKey("en") switch
{
true => descriptionNode.AsObject()["en"]!.GetValue<string>(),
false => descriptionNode.AsObject().FirstOrDefault().Value?.GetValue<string>() ?? ""
};
Dictionary<string, string> linksDict = new();
if (attributes.TryGetPropertyValue("links", out JsonNode? linksNode) && linksNode is not null)
foreach (KeyValuePair<string, JsonNode?> linkKv in linksNode!.AsObject())
linksDict.TryAdd(linkKv.Key, linkKv.Value.GetValue<string>());
List<Link> links = linksDict.Select(x => new Link(x.Key, x.Value)).ToList();
string? originalLanguage =
attributes.TryGetPropertyValue("originalLanguage", out JsonNode? originalLanguageNode) switch
{
true => originalLanguageNode?.GetValue<string>(),
false => null
};
MangaReleaseStatus releaseStatus = MangaReleaseStatus.Unreleased;
if (attributes.TryGetPropertyValue("status", out JsonNode? statusNode))
{
releaseStatus = statusNode?.GetValue<string>().ToLower() switch
{
"ongoing" => MangaReleaseStatus.Continuing,
"completed" => MangaReleaseStatus.Completed,
"hiatus" => MangaReleaseStatus.OnHiatus,
"cancelled" => MangaReleaseStatus.Cancelled,
_ => MangaReleaseStatus.Unreleased
};
}
uint year = attributes.TryGetPropertyValue("year", out JsonNode? yearNode) switch
{
true => yearNode?.GetValue<uint>()??0,
false => 0
};
HashSet<string> tags = new(128);
if (attributes.TryGetPropertyValue("tags", out JsonNode? tagsNode))
foreach (JsonNode? tagNode in tagsNode!.AsArray())
tags.Add(tagNode!["attributes"]!["name"]!["en"]!.GetValue<string>());
List<MangaTag> mangaTags = tags.Select(t => new MangaTag(t)).ToList();
if (!manga.TryGetPropertyValue("relationships", out JsonNode? relationshipsNode))
{
Log.Info("relationships was null");
return null; return null;
} }
JsonNode? coverNode = relationshipsNode!.AsArray() using StreamReader sr = new (result.result);
.FirstOrDefault(rel => rel!["type"]!.GetValue<string>().Equals("cover_art")); JObject jObject = JObject.Parse(sr.ReadToEnd());
if (coverNode is null)
if (jObject.Value<string>("result") != "ok")
{ {
Log.Info("coverNode was null"); JArray? errors = jObject["errors"] as JArray;
Log.Error($"Request failed: {string.Join(',', errors?.Select(e => e.Value<string>("title")) ?? [])}");
return null; return null;
} }
string fileName = coverNode["attributes"]!["fileName"]!.GetValue<string>();
string coverUrl = $"https://uploads.mangadex.org/covers/{publicationId}/{fileName}";
List<string> authorNames = new(); JObject? data = jObject["data"] as JObject;
JsonNode?[] authorNodes = relationshipsNode.AsArray() if (data is null)
.Where(rel => rel!["type"]!.GetValue<string>().Equals("author") || rel!["type"]!.GetValue<string>().Equals("artist")).ToArray();
foreach (JsonNode? authorNode in authorNodes)
{ {
string authorName = authorNode!["attributes"]!["name"]!.GetValue<string>(); Log.Error("Data was null");
if(!authorNames.Contains(authorName)) return null;
authorNames.Add(authorName);
}
List<Author> authors = authorNames.Select(a => new Author(a)).ToList();
Manga pub = new (publicationId, sortName, description, $"https://mangadex.org/title/{publicationId}", coverUrl, null, year,
originalLanguage, releaseStatus, -1,
this,
authors,
mangaTags,
links,
altTitles);
return (pub, authors, mangaTags, links, altTitles);
} }
public override Chapter[] GetChapters(Manga manga, string language="en") Manga manga = ParseMangaFromJToken(data);
{ return manga;
const int limit = 100; //How many values we want returned at once
int offset = 0; //"Page"
int total = int.MaxValue; //How many total results are there, is updated on first request
List<Chapter> chapters = new();
//As long as we haven't requested all "Pages"
while (offset < total)
{
//Request next "Page"
string requestUrl = $"https://api.mangadex.org/manga/{manga.IdOnConnectorSite}/feed?limit={limit}&offset={offset}&translatedLanguage%5B%5D={language}" +
$"&contentRating%5B%5D=safe&contentRating%5B%5D=suggestive&contentRating%5B%5D=erotica&contentRating%5B%5D=pornographic";
RequestResult requestResult = downloadClient.MakeRequest(requestUrl, RequestType.MangaDexFeed);
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
{
Log.Info($"{requestResult.statusCode}: {requestUrl}");
break;
}
JsonObject? result = JsonSerializer.Deserialize<JsonObject>(requestResult.result);
offset += limit;
if (result is null)
{
Log.Info($"result was null: {requestUrl}");
break;
} }
total = result["total"]!.GetValue<int>(); public override Chapter[] GetChapters(Manga manga, string? language = null)
JsonArray chaptersInResult = result["data"]!.AsArray();
//Loop through all Chapters in result and extract information from JSON
foreach (JsonNode? jsonNode in chaptersInResult)
{ {
JsonObject chapter = (JsonObject)jsonNode!; Log.Info($"Getting Chapters: {manga.IdOnConnectorSite}");
JsonObject attributes = chapter["attributes"]!.AsObject(); List<Chapter> chapters = new ();
string chapterId = chapter["id"]!.GetValue<string>(); int offset = 0;
string url = $"https://mangadex.org/chapter/{chapterId}"; int total = int.MaxValue;
while(offset < total)
string? title = attributes.ContainsKey("title") && attributes["title"] is not null
? attributes["title"]!.GetValue<string>()
: null;
int? volume = attributes.ContainsKey("volume") && attributes["volume"] is not null
? int.Parse(attributes["volume"]!.GetValue<string>())
: null;
string? chapterNumStr = attributes.ContainsKey("chapter") && attributes["chapter"] is not null
? attributes["chapter"]!.GetValue<string>()
: null;
string chapterNumber = new(chapterNumStr);
if (attributes.ContainsKey("pages") && attributes["pages"] is not null &&
attributes["pages"]!.GetValue<int>() < 1)
{ {
Log.Info($"No pages: {chapterId}"); string requestUrl =
continue; $"https://api.mangadex.org/manga/{manga.IdOnConnectorSite}/feed?limit={Limit}&offset={offset}&" +
$"translatedLanguage%5B%5D={language}&" +
$"contentRating%5B%5D=safe&contentRating%5B%5D=suggestive&contentRating%5B%5D=erotica&includeFutureUpdates=0&includes%5B%5D=";
offset += Limit;
RequestResult result = downloadClient.MakeRequest(requestUrl, RequestType.MangaDexFeed);
if ((int)result.statusCode < 200 || (int)result.statusCode >= 300)
{
Log.Error("Request failed");
return [];
} }
try using StreamReader sr = new (result.result);
JObject jObject = JObject.Parse(sr.ReadToEnd());
if (jObject.Value<string>("result") != "ok")
{ {
Chapter newChapter = new(manga, url, chapterNumber, volume, title); JArray? errors = jObject["errors"] as JArray;
if(!chapters.Contains(newChapter)) Log.Error($"Request failed: {string.Join(',', errors?.Select(e => e.Value<string>("title")) ?? [])}");
chapters.Add(newChapter); return [];
}
catch (Exception e)
{
Log.Debug(e);
}
}
} }
//Return Chapters ordered by Chapter-Number total = jObject.Value<int>("total");
JArray? data = jObject.Value<JArray>("data");
if (data is null)
{
Log.Error("Data was null");
return [];
}
chapters.AddRange(data.Select(d => ParseChapterFromJToken(manga, d)));
}
Log.Info($"Request for chapters for {manga.Name} yielded {chapters.Count} results.");
return chapters.ToArray(); return chapters.ToArray();
} }
private static readonly Regex GetChapterIdFromUrl = new(@"https?:\/\/mangadex\.org\/chapter\/([a-z0-9-]+)\/?.*");
internal override string[] GetChapterImageUrls(Chapter chapter) internal override string[] GetChapterImageUrls(Chapter chapter)
{//Request URLs for Chapter-Images
Match m = Regex.Match(chapter.Url, @"https?:\/\/mangadex.org\/chapter\/([0-9\-a-z]+)");
if (!m.Success)
{ {
Log.Error($"Could not parse Chapter ID: {chapter.Url}"); Log.Info($"Getting Chapter Image-Urls: {chapter.Url}");
if (!UrlMatchesConnector(chapter.Url))
{
Log.Debug($"Url is not for Connector. {chapter.Url}");
return []; return [];
} }
string url = $"https://api.mangadex.org/at-home/server/{m.Groups[1].Value}?forcePort443=false"; Match match = GetChapterIdFromUrl.Match(chapter.Url);
RequestResult requestResult = if (!match.Success || !match.Groups[1].Success)
downloadClient.MakeRequest(url, RequestType.MangaDexImage);
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
{ {
Log.Info($"{requestResult.statusCode}: {url}"); Log.Debug($"Url is not for Connector (Could not retrieve id). {chapter.Url}");
return []; return [];
} }
JsonObject? result = JsonSerializer.Deserialize<JsonObject>(requestResult.result);
if (result is null) string id = match.Groups[1].Value;
string requestUrl = $"https://api.mangadex.org/at-home/server/{id}";
RequestResult result = downloadClient.MakeRequest(requestUrl, RequestType.Default);
if ((int)result.statusCode < 200 || (int)result.statusCode >= 300)
{ {
Log.Info($"Result was null: {url}"); Log.Error("Request failed");
return []; return [];
} }
string baseUrl = result["baseUrl"]!.GetValue<string>();
string hash = result["chapter"]!["hash"]!.GetValue<string>(); using StreamReader sr = new (result.result);
JsonArray imageFileNames = result["chapter"]!["data"]!.AsArray(); JObject jObject = JObject.Parse(sr.ReadToEnd());
//Loop through all imageNames and construct urls (imageUrl)
List<string> imageUrls = new(); if (jObject.Value<string>("result") != "ok")
foreach (JsonNode? image in imageFileNames) {
imageUrls.Add($"{baseUrl}/data/{hash}/{image!.GetValue<string>()}"); JArray? errors = jObject["errors"] as JArray;
return imageUrls.ToArray(); Log.Error($"Request failed: {string.Join(',', errors?.Select(e => e.Value<string>("title")) ?? [])}");
return [];
}
string? baseUrl = jObject.Value<string>("baseUrl");
JToken? chapterToken = jObject["chapter"];
string? hash = chapterToken?.Value<string>("hash");
JArray? data = chapterToken?["data"] as JArray;
if (baseUrl is null || hash is null || data is null)
{
Log.Error("Data was null");
return [];
}
IEnumerable<string> urls = data.Select(t => $"{baseUrl}/data/{hash}/{t.Value<string>()}");
return urls.ToArray();
}
private Manga ParseMangaFromJToken(JToken jToken)
{
string? id = jToken.Value<string>("id");
JObject? attributes = jToken["attributes"] as JObject;
string? name = attributes?["title"]?.Value<string>("en");
string? description = attributes?["description"]?.Value<string>("en");
string? status = attributes?["status"]?.Value<string>();
uint? year = attributes?["year"]?.Value<uint>();
string? originalLanguage = attributes?["originalLanguage"]?.Value<string>();
JArray? altTitlesJArray = attributes?["altTitles"] as JArray;
JArray? tagsJArray = attributes?["tags"] as JArray;
JArray? relationships = jToken["relationships"] as JArray;
string? coverFileName =
relationships?.FirstOrDefault(r => r["type"]?.Value<string>() == "cover_art")?["attributes"]?.Value<string>("fileName");
if (id is null || attributes is null || name is null || description is null || status is null ||
altTitlesJArray is null || tagsJArray is null || relationships is null || coverFileName is null)
throw new Exception("jToken was not in expected format");
List<Link> links = attributes["links"]?
.ToObject<Dictionary<string,string>>()?
.Select(kv =>
{
//https://api.mangadex.org/docs/3-enumerations/#manga-links-data
string url = kv.Key switch
{
"al" => $"https://anilist.co/manga/{kv.Value}",
"ap" => $"https://www.anime-planet.com/manga/{kv.Value}",
"bw" => $"https://bookwalker.jp/{kv.Value}",
"mu" => $"https://www.mangaupdates.com/series.html?id={kv.Value}",
"nu" => $"https://www.novelupdates.com/series/{kv.Value}",
"mal" => $"https://myanimelist.net/manga/{kv.Value}",
_ => kv.Value
};
string key = kv.Key switch
{
"al" => "AniList",
"ap" => "Anime Planet",
"bw" => "BookWalker",
"mu" => "Manga Updates",
"nu" => "Novel Updates",
"kt" => "Kitsu.io",
"amz" => "Amazon",
"ebj" => "eBookJapan",
"mal" => "MyAnimeList",
"cdj" => "CDJapan",
_ => kv.Key
};
return new Link(key, url);
}).ToList()!;
List<MangaAltTitle> altTitles = altTitlesJArray
.Select(t =>
{
JObject? j = t as JObject;
JProperty? p = j?.Properties().First();
if (p is null)
return null;
return new MangaAltTitle(p.Name, p.Value.ToString());
}).Where(x => x is not null).ToList()!;
List<MangaTag> tags = tagsJArray
.Where(t => t.Value<string>("type") == "tag")
.Select(t => t["attributes"]?["name"]?.Value<string>("en"))
.Select(str => str is not null ? new MangaTag(str) : null)
.Where(x => x is not null).ToList()!;
List<Author> authors = relationships
.Where(r => r["type"]?.Value<string>() == "author")
.Select(t => t["attributes"]?.Value<string>("name"))
.Select(str => str is not null ? new Author(str) : null)
.Where(x => x is not null).ToList()!;
MangaReleaseStatus releaseStatus = status switch
{
"completed" => MangaReleaseStatus.Completed,
"ongoing" => MangaReleaseStatus.Continuing,
"cancelled" => MangaReleaseStatus.Cancelled,
"hiatus" => MangaReleaseStatus.OnHiatus,
_ => MangaReleaseStatus.Unreleased
};
string websiteUrl = $"https://mangadex.org/title/{id}";
string coverUrl = $"https://uploads.mangadex.org/covers/{id}/{coverFileName}";
return new Manga(id, name, description, websiteUrl, coverUrl, releaseStatus, this,
authors, tags, links,altTitles,
null, 0f, year, originalLanguage);
}
private Chapter ParseChapterFromJToken(Manga parentManga, JToken jToken)
{
string? id = jToken.Value<string>("id");
JToken? attributes = jToken["attributes"];
string? chapter = attributes?.Value<string>("chapter");
string? volumeStr = attributes?.Value<string>("volume");
int? volume = null;
string? title = attributes?.Value<string>("title");
if(id is null || chapter is null)
throw new Exception("jToken was not in expected format");
if(volumeStr is not null)
volume = int.Parse(volumeStr);
string url = $"https://mangadex.org/chapter/{id}";
return new Chapter(parentManga, url, chapter, volume, title);
} }
} }

View File

@ -1,183 +0,0 @@
using System.Text.RegularExpressions;
using API.MangaDownloadClients;
using HtmlAgilityPack;
namespace API.Schema.MangaConnectors;
public class MangaHere : MangaConnector
{
public MangaHere() : base("MangaHere", ["en"], ["www.mangahere.cc"], "http://www.mangahere.cc/favicon.ico")
{
this.downloadClient = new ChromiumDownloadClient();
}
public override (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)[] GetManga(string publicationTitle = "")
{
string sanitizedTitle = string.Join('+', Regex.Matches(publicationTitle, "[A-z]*").Where(str => str.Length > 0)).ToLower();
string requestUrl = $"https://www.mangahere.cc/search?title={sanitizedTitle}";
RequestResult requestResult =
downloadClient.MakeRequest(requestUrl, RequestType.Default);
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300 || requestResult.htmlDocument is null)
return [];
(Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)[] publications = ParsePublicationsFromHtml(requestResult.htmlDocument);
return publications;
}
private (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)[] ParsePublicationsFromHtml(HtmlDocument document)
{
if (document.DocumentNode.SelectNodes("//div[contains(concat(' ',normalize-space(@class),' '),' container ')]").Any(node => node.ChildNodes.Any(cNode => cNode.HasClass("search-keywords"))))
return [];
List<string> urls = document.DocumentNode
.SelectNodes("//a[contains(@href, '/manga/') and not(contains(@href, '.html'))]")
.Select(thumb => $"https://www.mangahere.cc{thumb.GetAttributeValue("href", "")}").Distinct().ToList();
HashSet<(Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)> ret = new();
foreach (string url in urls)
{
(Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)? manga = GetMangaFromUrl(url);
if (manga is { } x)
ret.Add(x);
}
return ret.ToArray();
}
public override (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)? GetMangaFromId(string publicationId)
{
return GetMangaFromUrl($"https://www.mangahere.cc/manga/{publicationId}");
}
public override (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)? GetMangaFromUrl(string url)
{
RequestResult requestResult =
downloadClient.MakeRequest(url, RequestType.MangaInfo);
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300 || requestResult.htmlDocument is null)
return null;
Regex idRex = new (@"https:\/\/www\.mangahere\.[a-z]{0,63}\/manga\/([0-9A-z\-]+).*");
string id = idRex.Match(url).Groups[1].Value;
return ParseSinglePublicationFromHtml(requestResult.htmlDocument, id, url);
}
private (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?) ParseSinglePublicationFromHtml(HtmlDocument document, string publicationId, string websiteUrl)
{
string originalLanguage = "", status = "";
Dictionary<string, string> altTitles = new(), links = new();
MangaReleaseStatus releaseStatus = MangaReleaseStatus.Unreleased;
//We dont get posters, because same origin bs HtmlNode posterNode = document.DocumentNode.SelectSingleNode("//img[contains(concat(' ',normalize-space(@class),' '),' detail-info-cover-img ')]");
string coverUrl = "http://static.mangahere.cc/v20230914/mangahere/images/nopicture.jpg";
HtmlNode titleNode = document.DocumentNode.SelectSingleNode("//span[contains(concat(' ',normalize-space(@class),' '),' detail-info-right-title-font ')]");
string sortName = titleNode.InnerText;
List<string> authorNames = document.DocumentNode
.SelectNodes("//p[contains(concat(' ',normalize-space(@class),' '),' detail-info-right-say ')]/a")
.Select(node => node.InnerText)
.ToList();
List<Author> authors = authorNames.Select(n => new Author(n)).ToList();
HashSet<string> tags = document.DocumentNode
.SelectNodes("//p[contains(concat(' ',normalize-space(@class),' '),' detail-info-right-tag-list ')]/a")
.Select(node => node.InnerText)
.ToHashSet();
List<MangaTag> mangaTags = tags.Select(n => new MangaTag(n)).ToList();
status = document.DocumentNode.SelectSingleNode("//span[contains(concat(' ',normalize-space(@class),' '),' detail-info-right-title-tip ')]").InnerText;
switch (status.ToLower())
{
case "cancelled": releaseStatus = MangaReleaseStatus.Cancelled; break;
case "hiatus": releaseStatus = MangaReleaseStatus.OnHiatus; break;
case "discontinued": releaseStatus = MangaReleaseStatus.Cancelled; break;
case "complete": releaseStatus = MangaReleaseStatus.Completed; break;
case "ongoing": releaseStatus = MangaReleaseStatus.Continuing; break;
}
HtmlNode descriptionNode = document.DocumentNode
.SelectSingleNode("//p[contains(concat(' ',normalize-space(@class),' '),' fullcontent ')]");
string description = descriptionNode.InnerText;
Manga manga = new (publicationId, sortName, description, websiteUrl, coverUrl, null, 0,
originalLanguage, releaseStatus, -1,
this,
authors,
mangaTags,
[],
[]);
return (manga, authors, mangaTags, [], []);
}
public override Chapter[] GetChapters(Manga manga, string language="en")
{
string requestUrl = $"https://www.mangahere.cc/manga/{manga.MangaId}";
RequestResult requestResult =
downloadClient.MakeRequest(requestUrl, RequestType.Default);
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300 || requestResult.htmlDocument is null)
return Array.Empty<Chapter>();
List<string> urls = requestResult.htmlDocument.DocumentNode.SelectNodes("//div[@id='list-1']/ul//li//a[contains(@href, '/manga/')]")
.Select(node => node.GetAttributeValue("href", "")).ToList();
Regex chapterRex = new(@".*\/manga\/[a-zA-Z0-9\-\._\~\!\$\&\'\(\)\*\+\,\;\=\:\@]+\/v([0-9(TBD)]+)\/c([0-9\.]+)\/.*");
List<Chapter> chapters = new();
foreach (string url in urls)
{
Match rexMatch = chapterRex.Match(url);
int? volumeNumber = rexMatch.Groups[1].Value == "TBD" ? null : int.Parse(rexMatch.Groups[1].Value);
string chapterNumber = new(rexMatch.Groups[2].Value);
string fullUrl = $"https://www.mangahere.cc{url}";
try
{
chapters.Add(new Chapter(manga, fullUrl, chapterNumber, volumeNumber, null));
}
catch (Exception e)
{
}
}
//Return Chapters ordered by Chapter-Number
return chapters.Order().ToArray();
}
internal override string[] GetChapterImageUrls(Chapter chapter)
{
List<string> imageUrls = new();
int downloaded = 1;
int images = 1;
string url = string.Join('/', chapter.Url.Split('/')[..^1]);
do
{
RequestResult requestResult =
downloadClient.MakeRequest($"{url}/{downloaded}.html", RequestType.Default);
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300 || requestResult.htmlDocument is null)
{
return [];
}
imageUrls.AddRange(ParseImageUrlsFromHtml(requestResult.htmlDocument));
images = requestResult.htmlDocument.DocumentNode
.SelectNodes("//a[contains(@href, '/manga/')]")
.MaxBy(node => node.GetAttributeValue("data-page", 0))!.GetAttributeValue("data-page", 0);
} while (downloaded++ <= images);
return imageUrls.ToArray();
}
private string[] ParseImageUrlsFromHtml(HtmlDocument document)
{
return document.DocumentNode
.SelectNodes("//img[contains(concat(' ',normalize-space(@class),' '),' reader-main-img ')]")
.Select(node =>
{
string url = node.GetAttributeValue("src", "");
return url.StartsWith("//") ? $"https:{url}" : url;
})
.ToArray();
}
}

View File

@ -1,233 +0,0 @@
using System.Text.RegularExpressions;
using API.MangaDownloadClients;
using HtmlAgilityPack;
namespace API.Schema.MangaConnectors;
public class MangaKatana : MangaConnector
{
public MangaKatana() : base("MangaKatana", ["en"], ["mangakatana.com"], "https://mangakatana.com/static/img/fav.png")
{
this.downloadClient = new HttpDownloadClient();
}
public override (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)[] GetManga(string publicationTitle = "")
{
string sanitizedTitle = string.Join("%20", Regex.Matches(publicationTitle, "[A-z]*").Where(m => m.Value.Length > 0)).ToLower();
string requestUrl = $"https://mangakatana.com/?search={sanitizedTitle}&search_by=book_name";
RequestResult requestResult =
downloadClient.MakeRequest(requestUrl, RequestType.Default);
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
return [];
// ReSharper disable once MergeIntoPattern
// If a single result is found, the user will be redirected to the results directly instead of a result page
if(requestResult.hasBeenRedirected
&& requestResult.redirectedToUrl is not null
&& requestResult.redirectedToUrl.Contains("mangakatana.com/manga"))
{
return new [] { ParseSinglePublicationFromHtml(requestResult.result, requestResult.redirectedToUrl.Split('/')[^1], requestResult.redirectedToUrl) };
}
(Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)[] publications = ParsePublicationsFromHtml(requestResult.result);
return publications;
}
public override (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)? GetMangaFromId(string publicationId)
{
return GetMangaFromUrl($"https://mangakatana.com/manga/{publicationId}");
}
public override (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)? GetMangaFromUrl(string url)
{
RequestResult requestResult =
downloadClient.MakeRequest(url, RequestType.MangaInfo);
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
return null;
return ParseSinglePublicationFromHtml(requestResult.result, url.Split('/')[^1], url);
}
private (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)[] ParsePublicationsFromHtml(Stream html)
{
StreamReader reader = new(html);
string htmlString = reader.ReadToEnd();
HtmlDocument document = new();
document.LoadHtml(htmlString);
IEnumerable<HtmlNode> searchResults = document.DocumentNode.SelectNodes("//*[@id='book_list']/div");
if (searchResults is null || !searchResults.Any())
return [];
List<string> urls = new();
foreach (HtmlNode mangaResult in searchResults)
{
urls.Add(mangaResult.Descendants("a").First().GetAttributes()
.First(a => a.Name == "href").Value);
}
HashSet<(Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)> ret = new();
foreach (string url in urls)
{
(Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)? manga = GetMangaFromUrl(url);
if (manga is { } x)
ret.Add(x);
}
return ret.ToArray();
}
private (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?) ParseSinglePublicationFromHtml(Stream html, string publicationId, string websiteUrl)
{
StreamReader reader = new(html);
string htmlString = reader.ReadToEnd();
HtmlDocument document = new();
document.LoadHtml(htmlString);
Dictionary<string, string> altTitlesDict = new();
Dictionary<string, string>? links = null;
HashSet<string> tags = new();
string[] authorNames = [];
string originalLanguage = "";
MangaReleaseStatus releaseStatus = MangaReleaseStatus.Unreleased;
HtmlNode infoNode = document.DocumentNode.SelectSingleNode("//*[@id='single_book']");
string sortName = infoNode.Descendants("h1").First(n => n.HasClass("heading")).InnerText;
HtmlNode infoTable = infoNode.SelectSingleNode("//*[@id='single_book']/div[2]/div/ul");
foreach (HtmlNode row in infoTable.Descendants("li"))
{
string key = row.SelectNodes("div").First().InnerText.ToLower();
string value = row.SelectNodes("div").Last().InnerText;
string keySanitized = string.Concat(Regex.Matches(key, "[a-z]"));
switch (keySanitized)
{
case "altnames":
string[] alts = value.Split(" ; ");
for (int i = 0; i < alts.Length; i++)
altTitlesDict.Add(i.ToString(), alts[i]);
break;
case "authorsartists":
authorNames = value.Split(',');
break;
case "status":
switch (value.ToLower())
{
case "ongoing": releaseStatus = MangaReleaseStatus.Continuing; break;
case "completed": releaseStatus = MangaReleaseStatus.Completed; break;
}
break;
case "genres":
tags = row.SelectNodes("div").Last().Descendants("a").Select(a => a.InnerText).ToHashSet();
break;
}
}
string coverUrl = document.DocumentNode.SelectSingleNode("//*[@id='single_book']/div[1]/div").Descendants("img").First()
.GetAttributes().First(a => a.Name == "src").Value;
string description = document.DocumentNode.SelectSingleNode("//*[@id='single_book']/div[3]/p").InnerText;
while (description.StartsWith('\n'))
description = description.Substring(1);
uint year = (uint)DateTime.UtcNow.Year;
string yearString = infoTable.Descendants("div").First(d => d.HasClass("updateAt"))
.InnerText.Split('-')[^1];
if(yearString.Contains("ago") == false)
{
year = uint.Parse(yearString);
}
List<Author> authors = authorNames.Select(n => new Author(n)).ToList();
List<MangaTag> mangaTags = tags.Select(n => new MangaTag(n)).ToList();
List<MangaAltTitle> altTitles = altTitlesDict.Select(x => new MangaAltTitle(x.Key, x.Value)).ToList();
Manga manga = new (publicationId, sortName, description, websiteUrl, coverUrl, null, year,
originalLanguage, releaseStatus, -1,
this,
authors,
mangaTags,
[],
altTitles);
return (manga, authors, mangaTags, [], altTitles);
}
public override Chapter[] GetChapters(Manga manga, string language="en")
{
string requestUrl = $"https://mangakatana.com/manga/{manga.MangaId}";
// Leaving this in for verification if the page exists
RequestResult requestResult =
downloadClient.MakeRequest(requestUrl, RequestType.Default);
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
return Array.Empty<Chapter>();
//Return Chapters ordered by Chapter-Number
List<Chapter> chapters = ParseChaptersFromHtml(manga, requestUrl);
return chapters.Order().ToArray();
}
private List<Chapter> ParseChaptersFromHtml(Manga manga, string mangaUrl)
{
// Using HtmlWeb will include the chapters since they are loaded with js
HtmlWeb web = new();
HtmlDocument document = web.Load(mangaUrl);
List<Chapter> ret = new();
HtmlNode chapterList = document.DocumentNode.SelectSingleNode("//div[contains(@class, 'chapters')]/table/tbody");
Regex volumeRex = new(@"[0-9a-z\-\.]+\/[0-9a-z\-]*v([0-9\.]+)");
Regex chapterNumRex = new(@"[0-9a-z\-\.]+\/[0-9a-z\-]*c([0-9\.]+)");
Regex chapterNameRex = new(@"Chapter [0-9\.]+:? (.*)");
foreach (HtmlNode chapterInfo in chapterList.Descendants("tr"))
{
string fullString = chapterInfo.Descendants("a").First().InnerText;
string url = chapterInfo.Descendants("a").First()
.GetAttributeValue("href", "");
int? volumeNumber = volumeRex.IsMatch(url) ? int.Parse(volumeRex.Match(url).Groups[1].Value) : null;
string chapterNumber = new(chapterNumRex.Match(url).Groups[1].Value);
string chapterName = chapterNameRex.Match(fullString).Groups[1].Value;
try
{
ret.Add(new Chapter(manga, url, chapterNumber, volumeNumber, chapterName));
}
catch (Exception e)
{
}
}
return ret;
}
internal override string[] GetChapterImageUrls(Chapter chapter)
{
string requestUrl = chapter.Url;
// Leaving this in to check if the page exists
RequestResult requestResult =
downloadClient.MakeRequest(requestUrl, RequestType.Default);
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300 || requestResult.htmlDocument is null)
{
return [];
}
string[] imageUrls = ParseImageUrlsFromHtml(requestResult.htmlDocument);
return imageUrls;
}
private string[] ParseImageUrlsFromHtml(HtmlDocument document)
{
// Images are loaded dynamically, but the urls are present in a piece of js code on the page
string js = document.DocumentNode.SelectSingleNode("//script[contains(., 'data-src')]").InnerText
.Replace("\r", "")
.Replace("\n", "")
.Replace("\t", "");
// ReSharper disable once StringLiteralTypo
string regexPat = @"(var thzq=\[')(.*)(,];function)";
var group = Regex.Matches(js, regexPat).First().Groups[2].Value.Replace("'", "");
var urls = group.Split(',');
return urls;
}
}

View File

@ -1,219 +0,0 @@
using System.Globalization;
using System.Net;
using System.Text.RegularExpressions;
using API.MangaDownloadClients;
using HtmlAgilityPack;
namespace API.Schema.MangaConnectors;
public class Manganato : MangaConnector
{
public Manganato() : base("Manganato", ["en"],
["natomanga.com", "manganato.gg", "mangakakalot.gg", "nelomanga.com"],
"https://www.manganato.gg/images/favicon-manganato.webp")
{
this.downloadClient = new HttpDownloadClient();
}
public override (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)[] GetManga(
string publicationTitle = "")
{
string sanitizedTitle = string.Join('_', Regex.Matches(publicationTitle, "[A-z]*").Where(str => str.Length > 0))
.ToLower();
string requestUrl = $"https://manganato.gg/search/story/{sanitizedTitle}";
RequestResult requestResult =
downloadClient.MakeRequest(requestUrl, RequestType.Default);
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
return [];
if (requestResult.htmlDocument is null)
return [];
(Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)[] publications =
ParsePublicationsFromHtml(requestResult.htmlDocument);
return publications;
}
private (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)[] ParsePublicationsFromHtml(
HtmlDocument document)
{
List<HtmlNode> searchResults =
document.DocumentNode.Descendants("div").Where(n => n.HasClass("story_item")).ToList();
List<string> urls = new();
foreach (HtmlNode mangaResult in searchResults)
{
try
{
urls.Add(mangaResult.Descendants("h3").First(n => n.HasClass("story_name"))
.Descendants("a").First().GetAttributeValue("href", ""));
}
catch
{
//failed to get a url, send it to the void
}
}
List<(Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)> ret = new();
foreach (string url in urls)
{
(Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)? manga = GetMangaFromUrl(url);
if (manga is { } m)
ret.Add(m);
}
return ret.ToArray();
}
public override (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)? GetMangaFromId(
string publicationId)
{
return GetMangaFromUrl($"https://chapmanganato.com/{publicationId}");
}
public override (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)?
GetMangaFromUrl(string url)
{
RequestResult requestResult =
downloadClient.MakeRequest(url, RequestType.MangaInfo);
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
return null;
if (requestResult.htmlDocument is null)
return null;
return ParseSinglePublicationFromHtml(requestResult.htmlDocument, url.Split('/')[^1], url);
}
private (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?) ParseSinglePublicationFromHtml(
HtmlDocument document, string publicationId, string websiteUrl)
{
Dictionary<string, string> altTitles = new();
List<MangaTag> tags = new();
List<Author> authors = new();
MangaReleaseStatus releaseStatus = MangaReleaseStatus.Unreleased;
HtmlNode infoNode = document.DocumentNode.Descendants("ul").First(d => d.HasClass("manga-info-text"));
string sortName = infoNode.Descendants("h1").First().InnerText;
foreach (HtmlNode li in infoNode.Descendants("li"))
{
string text = li.InnerText.Trim().ToLower();
if (text.StartsWith("author(s) :"))
{
authors = li.Descendants("a").Select(a => a.InnerText.Trim()).Select(a => new Author(a)).ToList();
}
else if (text.StartsWith("status :"))
{
string status = text.Replace("status :", "").Trim().ToLower();
if (string.IsNullOrWhiteSpace(status))
releaseStatus = MangaReleaseStatus.Continuing;
else if (status == "ongoing")
releaseStatus = MangaReleaseStatus.Continuing;
else
releaseStatus = Enum.Parse<MangaReleaseStatus>(status, true);
}
else if (li.HasClass("genres"))
{
tags = li.Descendants("a").Select(a => new MangaTag(a.InnerText.Trim())).ToList();
}
}
string posterUrl = document.DocumentNode.Descendants("div").First(s => s.HasClass("manga-info-pic"))
.Descendants("img").First()
.GetAttributes().First(a => a.Name == "src").Value;
string description = document.DocumentNode.SelectSingleNode("//div[@id='contentBox']")
.InnerText.Replace("Description :", "");
while (description.StartsWith('\n'))
description = description.Substring(1);
string pattern = "MMM-dd-yyyy HH:mm";
HtmlNode? oldestChapter = document.DocumentNode
.SelectNodes("//div[contains(concat(' ',normalize-space(@class),' '),' row ')]/span[@title]").MaxBy(
node => DateTime.ParseExact(node.GetAttributeValue("title", "Dec-31-2400 23:59"), pattern,
CultureInfo.InvariantCulture).Millisecond);
uint year = Convert.ToUInt32(DateTime.ParseExact(
oldestChapter?.GetAttributeValue("title", "Dec 31 2400, 23:59") ?? "Dec 31 2400, 23:59", pattern,
CultureInfo.InvariantCulture).Year);
Manga manga = new(publicationId, sortName, description, websiteUrl, posterUrl, null, year, null, releaseStatus,
-1, this, authors, tags, [], []);
return (manga, authors, tags, [], []);
}
public override Chapter[] GetChapters(Manga manga, string language = "en")
{
string requestUrl = manga.WebsiteUrl;
RequestResult requestResult =
downloadClient.MakeRequest(requestUrl, RequestType.Default);
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
return Array.Empty<Chapter>();
//Return Chapters ordered by Chapter-Number
if (requestResult.htmlDocument is null)
return Array.Empty<Chapter>();
List<Chapter> chapters = ParseChaptersFromHtml(manga, requestResult.htmlDocument);
return chapters.Order().ToArray();
}
internal override string[] GetChapterImageUrls(Chapter chapter)
{
string requestUrl = chapter.Url;
RequestResult requestResult =
downloadClient.MakeRequest(requestUrl, RequestType.Default);
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300 ||
requestResult.htmlDocument is null)
return [];
string[] imageUrls = ParseImageUrlsFromHtml(requestResult.htmlDocument);
return imageUrls;
}
private List<Chapter> ParseChaptersFromHtml(Manga manga, HtmlDocument document)
{
List<Chapter> ret = new();
HtmlNode chapterList = document.DocumentNode.Descendants("div").First(l => l.HasClass("chapter-list"));
Regex volRex = new(@"Vol\.([0-9]+).*");
Regex chapterRex = new(@"https:\/\/chapmanganato.[A-z]+\/manga-[A-z0-9]+\/chapter-([0-9\.]+)");
Regex nameRex = new(@"Chapter ([0-9]+(\.[0-9]+)*){1}:? (.*)");
foreach (HtmlNode chapterInfo in chapterList.Descendants("div").Where(x => x.HasClass("row")))
{
string url = chapterInfo.Descendants("a").First().GetAttributeValue("href", "");
var name = chapterInfo.Descendants("a").First().InnerText.Trim();
string chapterName = nameRex.Match(name).Groups[3].Value;
string chapterNumber = Regex.Match(name, @"Chapter ([0-9]+(\.[0-9]+)*)").Groups[1].Value;
string? volumeNumber = Regex.Match(chapterName, @"Vol\.([0-9]+)").Groups[1].Value;
if (string.IsNullOrWhiteSpace(volumeNumber))
volumeNumber = "0";
try
{
ret.Add(new Chapter(manga, url, chapterNumber, int.Parse(volumeNumber), chapterName));
}
catch (Exception e)
{
}
}
ret.Reverse();
return ret;
}
private string[] ParseImageUrlsFromHtml(HtmlDocument document)
{
List<string> ret = new();
HtmlNode imageContainer =
document.DocumentNode.Descendants("div").First(i => i.HasClass("container-chapter-reader"));
foreach (HtmlNode imageNode in imageContainer.Descendants("img"))
ret.Add(imageNode.GetAttributeValue("src", ""));
return ret.ToArray();
}
}

View File

@ -1,223 +0,0 @@
using System.Text.RegularExpressions;
using API.MangaDownloadClients;
using HtmlAgilityPack;
namespace API.Schema.MangaConnectors;
public class Mangaworld : MangaConnector
{
public Mangaworld() : base("Mangaworld", ["it"], ["www.mangaworld.ac", "www.mangaworld.nz"], "https://www.mangaworld.nz/public/assets/seo/android-icon-192x192.png")
{
this.downloadClient = new ChromiumDownloadClient();
}
public override (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)[] GetManga(string publicationTitle = "")
{
string sanitizedTitle = string.Join(' ', Regex.Matches(publicationTitle, "[A-z]*").Where(str => str.Length > 0)).ToLower();
string requestUrl = $"https://www.mangaworld.ac/archive?keyword={sanitizedTitle}";
RequestResult requestResult =
downloadClient.MakeRequest(requestUrl, RequestType.Default);
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
return [];
if (requestResult.htmlDocument is null)
return [];
(Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)[] publications = ParsePublicationsFromHtml(requestResult.htmlDocument);
return publications;
}
private (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)[] ParsePublicationsFromHtml(HtmlDocument document)
{
if (!document.DocumentNode.SelectSingleNode("//div[@class='comics-grid']").ChildNodes
.Any(node => node.HasClass("entry")))
return [];
List<string> urls = document.DocumentNode
.SelectNodes(
"//div[@class='comics-grid']//div[@class='entry']//a[contains(concat(' ',normalize-space(@class),' '),'thumb')]")
.Select(thumb => thumb.GetAttributeValue("href", "")).ToList();
List<(Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)> ret = new();
foreach (string url in urls)
{
(Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)? manga = GetMangaFromUrl(url);
if (manga is { } x)
ret.Add(x);
}
return ret.ToArray();
}
public override (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)? GetMangaFromId(string publicationId)
{
return GetMangaFromUrl($"https://www.mangaworld.ac/manga/{publicationId}");
}
public override (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)? GetMangaFromUrl(string url)
{
RequestResult requestResult =
downloadClient.MakeRequest(url, RequestType.MangaInfo);
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
return null;
if (requestResult.htmlDocument is null)
return null;
Regex idRex = new (@"https:\/\/www\.mangaworld\.[a-z]{0,63}\/manga\/([0-9]+\/[0-9A-z\-]+).*");
string id = idRex.Match(url).Groups[1].Value;
return ParseSinglePublicationFromHtml(requestResult.htmlDocument, id, url);
}
private (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?) ParseSinglePublicationFromHtml(HtmlDocument document, string publicationId, string websiteUrl)
{
Dictionary<string, string> altTitlesDict = new();
string originalLanguage = "";
MangaReleaseStatus releaseStatus = MangaReleaseStatus.Unreleased;
HtmlNode infoNode = document.DocumentNode.Descendants("div").First(d => d.HasClass("info"));
string sortName = infoNode.Descendants("h1").First().InnerText;
HtmlNode metadata = infoNode.Descendants().First(d => d.HasClass("meta-data"));
HtmlNode altTitlesNode = metadata.SelectSingleNode("//span[text()='Titoli alternativi: ' or text()='Titolo alternativo: ']/..").ChildNodes[1];
string[] alts = altTitlesNode.InnerText.Split(", ");
for(int i = 0; i < alts.Length; i++)
altTitlesDict.Add(i.ToString(), alts[i]);
List<MangaAltTitle> altTitles = altTitlesDict.Select(a => new MangaAltTitle(a.Key, a.Value)).ToList();
HtmlNode genresNode =
metadata.SelectSingleNode("//span[text()='Generi: ' or text()='Genero: ']/..");
HashSet<string> tags = genresNode.SelectNodes("a").Select(node => node.InnerText).ToHashSet();
List<MangaTag> mangaTags = tags.Select(t => new MangaTag(t)).ToList();
HtmlNode authorsNode =
metadata.SelectSingleNode("//span[text()='Autore: ' or text()='Autori: ']/..");
string[] authorNames = authorsNode.SelectNodes("a").Select(node => node.InnerText).ToArray();
List<Author> authors = authorNames.Select(n => new Author(n)).ToList();
string status = metadata.SelectSingleNode("//span[text()='Stato: ']/..").SelectNodes("a").First().InnerText;
// ReSharper disable 5 times StringLiteralTypo
switch (status.ToLower())
{
case "cancellato": releaseStatus = MangaReleaseStatus.Cancelled; break;
case "in pausa": releaseStatus = MangaReleaseStatus.OnHiatus; break;
case "droppato": releaseStatus = MangaReleaseStatus.Cancelled; break;
case "finito": releaseStatus = MangaReleaseStatus.Completed; break;
case "in corso": releaseStatus = MangaReleaseStatus.Continuing; break;
}
string coverUrl = document.DocumentNode.SelectSingleNode("//img[@class='rounded']").GetAttributeValue("src", "");
string description = document.DocumentNode.SelectSingleNode("//div[@id='noidungm']").InnerText;
string yearString = metadata.SelectSingleNode("//span[text()='Anno di uscita: ']/..").SelectNodes("a").First().InnerText;
uint year = uint.Parse(yearString);
Manga manga = new (publicationId, sortName, description, websiteUrl, coverUrl, null, year,
originalLanguage, releaseStatus, -1,
this,
authors,
mangaTags,
[],
altTitles);
return (manga, authors, mangaTags, [], altTitles);
}
public override Chapter[] GetChapters(Manga manga, string language="en")
{
string requestUrl = $"https://www.mangaworld.ac/manga/{manga.MangaId}";
RequestResult requestResult =
downloadClient.MakeRequest(requestUrl, RequestType.Default);
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300 || requestResult.htmlDocument is null)
return [];
List<Chapter> chapters = ParseChaptersFromHtml(manga, requestResult.htmlDocument);
return chapters.Order().ToArray();
}
private List<Chapter> ParseChaptersFromHtml(Manga manga, HtmlDocument document)
{
List<Chapter> ret = new();
HtmlNode chaptersWrapper =
document.DocumentNode.SelectSingleNode(
"//div[contains(concat(' ',normalize-space(@class),' '),'chapters-wrapper')]");
Regex volumeRex = new(@"[Vv]olume ([0-9]+).*");
Regex chapterRex = new(@"[Cc]apitolo ([0-9]+(?:\.[0-9]+)?).*");
Regex idRex = new(@".*\/read\/([a-z0-9]+)(?:[?\/].*)?");
if (chaptersWrapper.Descendants("div").Any(descendant => descendant.HasClass("volume-element")))
{
foreach (HtmlNode volNode in document.DocumentNode.SelectNodes("//div[contains(concat(' ',normalize-space(@class),' '),'volume-element')]"))
{
string volumeStr = volumeRex.Match(volNode.SelectNodes("div").First(node => node.HasClass("volume")).SelectSingleNode("p").InnerText).Groups[1].Value;
int volume = int.Parse(volumeStr);
foreach (HtmlNode chNode in volNode.SelectNodes("div").First(node => node.HasClass("volume-chapters")).SelectNodes("div"))
{
string numberStr = chapterRex.Match(chNode.SelectSingleNode("a").SelectSingleNode("span").InnerText).Groups[1].Value;
string chapterNumber = new(numberStr);
string url = chNode.SelectSingleNode("a").GetAttributeValue("href", "");
string id = idRex.Match(chNode.SelectSingleNode("a").GetAttributeValue("href", "")).Groups[1].Value;
try
{
ret.Add(new Chapter(manga, url, chapterNumber, volume, null));
}
catch (Exception e)
{
}
}
}
}
else
{
foreach (HtmlNode chNode in chaptersWrapper.SelectNodes("div").Where(node => node.HasClass("chapter")))
{
string numberStr = chapterRex.Match(chNode.SelectSingleNode("a").SelectSingleNode("span").InnerText).Groups[1].Value;
string chapterNumber = new(numberStr);
string url = chNode.SelectSingleNode("a").GetAttributeValue("href", "");
string id = idRex.Match(chNode.SelectSingleNode("a").GetAttributeValue("href", "")).Groups[1].Value;
try
{
ret.Add(new Chapter(manga, url, chapterNumber, null, null));
}
catch (Exception e)
{
}
}
}
ret.Reverse();
return ret;
}
internal override string[] GetChapterImageUrls(Chapter chapter)
{
string requestUrl = $"{chapter.Url}?style=list";
RequestResult requestResult =
downloadClient.MakeRequest(requestUrl, RequestType.Default);
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300 || requestResult.htmlDocument is null)
{
return [];
}
string[] imageUrls = ParseImageUrlsFromHtml(requestResult.htmlDocument);
return imageUrls;
}
private string[] ParseImageUrlsFromHtml(HtmlDocument document)
{
List<string> ret = new();
HtmlNode imageContainer =
document.DocumentNode.SelectSingleNode("//div[@id='page']");
foreach(HtmlNode imageNode in imageContainer.Descendants("img"))
ret.Add(imageNode.GetAttributeValue("src", ""));
return ret.ToArray();
}
}

View File

@ -1,179 +0,0 @@
using System.Text.RegularExpressions;
using API.MangaDownloadClients;
using HtmlAgilityPack;
namespace API.Schema.MangaConnectors;
public class ManhuaPlus : MangaConnector
{
public ManhuaPlus() : base("ManhuaPlus", ["en"], ["manhuaplus.org"], "https://manhuaplus.org/uploads/images/favicon.png")
{
this.downloadClient = new ChromiumDownloadClient();
}
public override (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)[] GetManga(string publicationTitle = "")
{
string sanitizedTitle = string.Join(' ', Regex.Matches(publicationTitle, "[A-z]*").Where(str => str.Length > 0)).ToLower();
string requestUrl = $"https://manhuaplus.org/search?keyword={sanitizedTitle}";
RequestResult requestResult =
downloadClient.MakeRequest(requestUrl, RequestType.Default);
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300 || requestResult.htmlDocument is null)
return [];
(Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)[] publications = ParsePublicationsFromHtml(requestResult.htmlDocument);
return publications;
}
private (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)[] ParsePublicationsFromHtml(HtmlDocument document)
{
if (document.DocumentNode.SelectSingleNode("//h1/../..").ChildNodes//I already want to not.
.Any(node => node.InnerText.Contains("No manga found")))
return [];
List<string> urls = document.DocumentNode
.SelectNodes("//h1/../..//a[contains(@href, 'https://manhuaplus.org/manga/') and contains(concat(' ',normalize-space(@class),' '),' clamp ') and not(contains(@href, '/chapter'))]")
.Select(mangaNode => mangaNode.GetAttributeValue("href", "")).ToList();
List<(Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)> ret = new();
foreach (string url in urls)
{
(Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)? manga = GetMangaFromUrl(url);
if (manga is { } x)
ret.Add(x);
}
return ret.ToArray();
}
public override (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)? GetMangaFromId(string publicationId)
{
return GetMangaFromUrl($"https://manhuaplus.org/manga/{publicationId}");
}
public override (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)? GetMangaFromUrl(string url)
{
Regex publicationIdRex = new(@"https:\/\/manhuaplus.org\/manga\/(.*)(\/.*)*");
string publicationId = publicationIdRex.Match(url).Groups[1].Value;
RequestResult requestResult = this.downloadClient.MakeRequest(url, RequestType.MangaInfo);
if((int)requestResult.statusCode < 300 && (int)requestResult.statusCode >= 200 && requestResult.htmlDocument is not null && requestResult.redirectedToUrl != "https://manhuaplus.org/home") //When manga doesnt exists it redirects to home
return ParseSinglePublicationFromHtml(requestResult.htmlDocument, publicationId, url);
return null;
}
private (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?) ParseSinglePublicationFromHtml(HtmlDocument document, string publicationId, string websiteUrl)
{
string originalLanguage = "", status = "";
Dictionary<string, string> altTitles = new(), links = new();
HashSet<string> tags = new();
MangaReleaseStatus releaseStatus = MangaReleaseStatus.Unreleased;
HtmlNode posterNode = document.DocumentNode.SelectSingleNode("/html/body/main/div/div/div[2]/div[1]/figure/a/img");//BRUH
Regex posterRex = new(@".*(\/uploads/covers/[a-zA-Z0-9\-\._\~\!\$\&\'\(\)\*\+\,\;\=\:\@]+).*");
string coverUrl = $"https://manhuaplus.org/{posterRex.Match(posterNode.GetAttributeValue("src", "")).Groups[1].Value}";
HtmlNode titleNode = document.DocumentNode.SelectSingleNode("//h1");
string sortName = titleNode.InnerText.Replace("\n", "");
List<string> authorNames = new();
try
{
HtmlNode[] authorsNodes = document.DocumentNode
.SelectNodes("//a[contains(@href, 'https://manhuaplus.org/authors/')]")
.ToArray();
foreach (HtmlNode authorNode in authorsNodes)
authorNames.Add(authorNode.InnerText);
}
catch (ArgumentNullException e)
{
}
List<Author> authors = authorNames.Select(a => new Author(a)).ToList();
try
{
HtmlNode[] genreNodes = document.DocumentNode
.SelectNodes("//a[contains(@href, 'https://manhuaplus.org/genres/')]").ToArray();
foreach (HtmlNode genreNode in genreNodes)
tags.Add(genreNode.InnerText.Replace("\n", ""));
}
catch (ArgumentNullException e)
{
}
List<MangaTag> mangaTags = tags.Select(t => new MangaTag(t)).ToList();
Regex yearRex = new(@"(?:[0-9]{1,2}\/){2}([0-9]{2,4}) [0-9]{1,2}:[0-9]{1,2}");
HtmlNode yearNode = document.DocumentNode.SelectSingleNode("//aside//i[contains(concat(' ',normalize-space(@class),' '),' fa-clock ')]/../span");
Match match = yearRex.Match(yearNode.InnerText);
uint year = match.Success && match.Groups[1].Success ? uint.Parse(match.Groups[1].Value) : 0;
status = document.DocumentNode.SelectSingleNode("//aside//i[contains(concat(' ',normalize-space(@class),' '),' fa-rss ')]/../span").InnerText.Replace("\n", "");
switch (status.ToLower())
{
case "cancelled": releaseStatus = MangaReleaseStatus.Cancelled; break;
case "hiatus": releaseStatus = MangaReleaseStatus.OnHiatus; break;
case "discontinued": releaseStatus = MangaReleaseStatus.Cancelled; break;
case "complete": releaseStatus = MangaReleaseStatus.Completed; break;
case "ongoing": releaseStatus = MangaReleaseStatus.Continuing; break;
}
HtmlNode descriptionNode = document.DocumentNode
.SelectSingleNode("//div[@id='syn-target']");
string description = descriptionNode.InnerText;
Manga manga = new (publicationId, sortName, description, websiteUrl, coverUrl, null, year,
originalLanguage, releaseStatus, -1,
this,
authors,
mangaTags,
[],
[]);
return (manga, authors, mangaTags, [], []);
}
public override Chapter[] GetChapters(Manga manga, string language="en")
{
RequestResult result = downloadClient.MakeRequest($"https://manhuaplus.org/manga/{manga.MangaId}", RequestType.Default);
if ((int)result.statusCode < 200 || (int)result.statusCode >= 300 || result.htmlDocument is null)
{
return Array.Empty<Chapter>();
}
HtmlNodeCollection chapterNodes = result.htmlDocument.DocumentNode.SelectNodes("//li[contains(concat(' ',normalize-space(@class),' '),' chapter ')]//a");
string[] urls = chapterNodes.Select(node => node.GetAttributeValue("href", "")).ToArray();
Regex urlRex = new (@".*\/chapter-([0-9\-]+).*");
List<Chapter> chapters = new();
foreach (string url in urls)
{
Match rexMatch = urlRex.Match(url);
string chapterNumber = new(rexMatch.Groups[1].Value);
string fullUrl = url;
try
{
chapters.Add(new Chapter(manga, fullUrl, chapterNumber, null, null));
}
catch (Exception e)
{
}
}
//Return Chapters ordered by Chapter-Number
return chapters.Order().ToArray();
}
internal override string[] GetChapterImageUrls(Chapter chapter)
{
RequestResult requestResult = this.downloadClient.MakeRequest(chapter.Url, RequestType.Default);
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300 || requestResult.htmlDocument is null)
{
return [];
}
HtmlDocument document = requestResult.htmlDocument;
HtmlNode[] images = document.DocumentNode.SelectNodes("//a[contains(concat(' ',normalize-space(@class),' '),' readImg ')]/img").ToArray();
List<string> urls = images.Select(node => node.GetAttributeValue("src", "")).ToList();
return urls.ToArray();
}
}

View File

@ -1,259 +0,0 @@
using System.Net;
using System.Text.RegularExpressions;
using API.MangaDownloadClients;
using HtmlAgilityPack;
namespace API.Schema.MangaConnectors;
public class Webtoons : MangaConnector
{
public Webtoons() : base("Webtoons", ["en"], ["www.webtoons.com"], "https://webtoons-static.pstatic.net/image/favicon/favicon.ico")
{
this.downloadClient = new HttpDownloadClient();
}
// Done
public override (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)[] GetManga(string publicationTitle = "")
{
string sanitizedTitle = string.Join(' ', Regex.Matches(publicationTitle, "[A-z]*").Where(m => m.Value.Length > 0)).ToLower();
string requestUrl = $"https://www.webtoons.com/en/search?keyword={sanitizedTitle}&searchType=WEBTOON";
RequestResult requestResult =
downloadClient.MakeRequest(requestUrl, RequestType.Default);
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300) {
return [];
}
if (requestResult.htmlDocument is null)
{
return [];
}
(Manga, List<Author>, List<MangaTag>, List<Link>, List<MangaAltTitle>)[] publications =
ParsePublicationsFromHtml(requestResult.htmlDocument);
return publications;
}
// Done
public override (Manga, List<Author>, List<MangaTag>, List<Link>, List<MangaAltTitle>)? GetMangaFromId(string publicationId)
{
PublicationManager pb = new PublicationManager(publicationId);
return GetMangaFromUrl($"https://www.webtoons.com/en/{pb.Category}/{pb.Title}/list?title_no={pb.Id}");
}
// Done
public override (Manga, List<Author>, List<MangaTag>, List<Link>, List<MangaAltTitle>)? GetMangaFromUrl(string url)
{
RequestResult requestResult = downloadClient.MakeRequest(url, RequestType.MangaInfo);
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300) {
return null;
}
if (requestResult.htmlDocument is null)
{
return null;
}
Regex regex = new Regex(@".*webtoons\.com/en/(?<category>[^/]+)/(?<title>[^/]+)/list\?title_no=(?<id>\d+).*");
Match match = regex.Match(url);
if(match.Success) {
PublicationManager pm = new PublicationManager(match.Groups["title"].Value, match.Groups["category"].Value, match.Groups["id"].Value);
return ParseSinglePublicationFromHtml(requestResult.htmlDocument, pm.getPublicationId(), url);
}
return null;
}
// Done
private (Manga, List<Author>, List<MangaTag>, List<Link>, List<MangaAltTitle>)[] ParsePublicationsFromHtml(HtmlDocument document)
{
HtmlNode mangaList = document.DocumentNode.SelectSingleNode("//ul[contains(@class, 'card_lst')]");
if (!mangaList.ChildNodes.Any(node => node.Name == "li")) {
return [];
}
List<string> urls = document.DocumentNode
.SelectNodes("//ul[contains(@class, 'card_lst')]/li/a")
.Select(node => node.GetAttributeValue("href", "https://www.webtoons.com"))
.ToList();
List<(Manga, List<Author>, List<MangaTag>, List<Link>, List<MangaAltTitle>)> ret = new();
foreach (string url in urls)
{
(Manga, List<Author>, List<MangaTag>, List<Link>, List<MangaAltTitle>)? manga = GetMangaFromUrl(url);
if(manga is { } m)
ret.Add(m);
}
return ret.ToArray();
}
private string capitalizeString(string str = "") {
if(str.Length == 0) return "";
if(str.Length == 1) return str.ToUpper();
return char.ToUpper(str[0]) + str.Substring(1).ToLower();
}
// Done
private (Manga, List<Author>, List<MangaTag>, List<Link>, List<MangaAltTitle>) ParseSinglePublicationFromHtml(HtmlDocument document, string publicationId, string websiteUrl)
{
HtmlNode infoNode1 = document.DocumentNode.SelectSingleNode("//*[@id='content']/div[2]/div[1]/div[1]");
HtmlNode infoNode2 = document.DocumentNode.SelectSingleNode("//*[@id='content']/div[2]/div[2]/div[2]");
string sortName = infoNode1.SelectSingleNode(".//h1[contains(@class, 'subj')]").InnerText;
string description = infoNode2.SelectSingleNode(".//p[contains(@class, 'summary')]")
.InnerText.Trim();
HtmlNode posterNode = document.DocumentNode.SelectSingleNode("//div[contains(@class, 'detail_body') and contains(@class, 'banner')]");
Regex regex = new Regex(@"url\((?<url>.*?)\)");
Match match = regex.Match(posterNode.GetAttributeValue("style", ""));
string coverUrl = match.Groups["url"].Value;
string genre = infoNode1.SelectSingleNode(".//h2[contains(@class, 'genre')]")
.InnerText.Trim();
List<MangaTag> mangaTags = [new MangaTag(genre)];
List<HtmlNode> authorsNodes = infoNode1.SelectSingleNode(".//div[contains(@class, 'author_area')]").Descendants("a").ToList();
List<Author> authors = authorsNodes.Select(node => new Author(node.InnerText.Trim())).ToList();
string originalLanguage = "";
uint year = 0;
string status1 = infoNode2.SelectSingleNode(".//p").InnerText;
string status2 = infoNode2.SelectSingleNode(".//p/span").InnerText;
MangaReleaseStatus releaseStatus = MangaReleaseStatus.Unreleased;
if(status2.Length == 0 || status1.ToLower() == "completed") {
releaseStatus = MangaReleaseStatus.Completed;
} else if(status2.ToLower() == "up") {
releaseStatus = MangaReleaseStatus.Continuing;
}
Manga manga = new (publicationId, sortName, description, websiteUrl, coverUrl, null, year,
originalLanguage, releaseStatus, -1,
this,
authors,
mangaTags,
[],
[]);
return (manga, authors, mangaTags, [], []);
}
// Done
public override Chapter[] GetChapters(Manga manga, string language = "en")
{
PublicationManager pm = new(manga.MangaId);
string requestUrl = $"https://www.webtoons.com/en/{pm.Category}/{pm.Title}/list?title_no={pm.Id}";
// Leaving this in for verification if the page exists
RequestResult requestResult =
downloadClient.MakeRequest(requestUrl, RequestType.Default);
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
return Array.Empty<Chapter>();
// Get number of pages
int pages = requestResult.htmlDocument.DocumentNode
.SelectNodes("//div[contains(@class, 'paginate')]/a")
.ToList()
.Count;
List<Chapter> chapters = new List<Chapter>();
for(int page = 1; page <= pages; page++) {
string pageRequestUrl = $"{requestUrl}&page={page}";
chapters.AddRange(ParseChaptersFromHtml(manga, pageRequestUrl));
}
return chapters.Order().ToArray();
}
// Done
private List<Chapter> ParseChaptersFromHtml(Manga manga, string mangaUrl)
{
RequestResult result = downloadClient.MakeRequest(mangaUrl, RequestType.Default);
if ((int)result.statusCode < 200 || (int)result.statusCode >= 300 || result.htmlDocument is null)
{
return new List<Chapter>();
}
List<Chapter> ret = new();
foreach (HtmlNode chapterInfo in result.htmlDocument.DocumentNode.SelectNodes("//ul/li[contains(@class, '_episodeItem')]"))
{
HtmlNode infoNode = chapterInfo.SelectSingleNode(".//a");
string url = infoNode.GetAttributeValue("href", "");
string id = chapterInfo.GetAttributeValue("id", "");
if(id == "") continue;
string chapterNumber = chapterInfo.GetAttributeValue("data-episode-no", "");
if(chapterNumber == "") continue;
string chapterName = infoNode.SelectSingleNode(".//span[contains(@class, 'subj')]/span").InnerText.Trim();
ret.Add(new Chapter(manga, url, chapterNumber, null, chapterName));
}
return ret;
}
internal override string[] GetChapterImageUrls(Chapter chapter)
{
string requestUrl = chapter.Url;
// Leaving this in to check if the page exists
RequestResult requestResult =
downloadClient.MakeRequest(requestUrl, RequestType.Default);
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
{
return [];
}
string[] imageUrls = ParseImageUrlsFromHtml(requestUrl);
return imageUrls;
}
private string[] ParseImageUrlsFromHtml(string mangaUrl)
{
RequestResult requestResult =
downloadClient.MakeRequest(mangaUrl, RequestType.Default);
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
{
return [];
}
if (requestResult.htmlDocument is null)
{
return [];
}
return requestResult.htmlDocument.DocumentNode
.SelectNodes("//*[@id='_imageList']/img")
.Select(node =>
node.GetAttributeValue("data-url", ""))
.ToArray();
}
}
internal class PublicationManager {
public PublicationManager(string title = "", string category = "", string id = "") {
this.Title = title;
this.Category = category;
this.Id = id;
}
public PublicationManager(string publicationId) {
string[] parts = publicationId.Split("|");
if(parts.Length == 3) {
this.Title = parts[0];
this.Category = parts[1];
this.Id = parts[2];
} else {
this.Title = "";
this.Category = "";
this.Id = "";
}
}
public string getPublicationId() {
return $"{this.Title}|{this.Category}|{this.Id}";
}
public string Title { get; set; }
public string Category { get; set; }
public string Id { get; set; }
}

View File

@ -1,175 +0,0 @@
using System.Net;
using System.Text.RegularExpressions;
using API.MangaDownloadClients;
using HtmlAgilityPack;
namespace API.Schema.MangaConnectors;
public class Weebcentral : MangaConnector
{
private readonly string[] _filterWords =
{ "a", "the", "of", "as", "to", "no", "for", "on", "with", "be", "and", "in", "wa", "at", "be", "ni" };
public Weebcentral() : base("Weebcentral", ["en"], ["weebcentral.com"], "https://weebcentral.com/favicon.ico")
{
downloadClient = new ChromiumDownloadClient();
}
public override (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)[] GetManga(string publicationTitle = "")
{
const int limit = 32; //How many values we want returned at once
var offset = 0; //"Page"
var requestUrl =
$"https://{BaseUris[0]}/search/data?limit={limit}&offset={offset}&text={publicationTitle}&sort=Best+Match&order=Ascending&official=Any&display_mode=Minimal%20Display";
var requestResult =
downloadClient.MakeRequest(requestUrl, RequestType.Default);
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300 ||
requestResult.htmlDocument == null)
{
return [];
}
var publications = ParsePublicationsFromHtml(requestResult.htmlDocument);
return publications;
}
private (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)[] ParsePublicationsFromHtml(HtmlDocument document)
{
if (document.DocumentNode.SelectNodes("//article").Count < 1)
return [];
var urls = document.DocumentNode.SelectNodes("/html/body/article/a[contains(concat(' ',normalize-space(@class),' '),' link ')]")
.Select(elem => elem.GetAttributeValue("href", "")).ToList();
List<(Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)> ret = new();
foreach (var url in urls)
{
(Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)? manga = GetMangaFromUrl(url);
if (manga is { })
ret.Add(((Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?))manga);
}
return ret.ToArray();
}
public override (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)? GetMangaFromUrl(string url)
{
Regex publicationIdRex = new(@"https:\/\/weebcentral\.com\/series\/(\w*)\/(.*)");
var publicationId = publicationIdRex.Match(url).Groups[1].Value;
var requestResult = downloadClient.MakeRequest(url, RequestType.MangaInfo);
if ((int)requestResult.statusCode < 300 && (int)requestResult.statusCode >= 200 &&
requestResult.htmlDocument is not null)
return ParseSinglePublicationFromHtml(requestResult.htmlDocument, publicationId, url);
return null;
}
private (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?) ParseSinglePublicationFromHtml(HtmlDocument document, string publicationId, string websiteUrl)
{
HtmlNode posterNode =
document.DocumentNode.SelectSingleNode("//section[@class='flex items-center justify-center']/picture/img");
string posterUrl = posterNode?.GetAttributeValue("src", "") ?? "";
HtmlNode titleNode = document.DocumentNode.SelectSingleNode("//section/h1");
string sortName = titleNode?.InnerText ?? "Undefined";
HtmlNode[] authorsNodes =
document.DocumentNode.SelectNodes("//ul/li[strong/text() = 'Author(s): ']/span").ToArray();
List<Author> authors = authorsNodes.Select(n => new Author(n.InnerText)).ToList();
HtmlNode[] genreNodes =
document.DocumentNode.SelectNodes("//ul/li[strong/text() = 'Tags(s): ']/span").ToArray();
List<MangaTag> tags = genreNodes.Select(n => new MangaTag(n.InnerText.EndsWith(',') ? n.InnerText.Substring(0,n.InnerText.Length-1) : n.InnerText)).ToList();
HtmlNode statusNode = document.DocumentNode.SelectSingleNode("//ul/li[strong/text() = 'Status: ']/a");
string statusText = statusNode?.InnerText ?? "";
MangaReleaseStatus releaseStatus = statusText.ToLower() switch
{
"cancelled" => MangaReleaseStatus.Cancelled,
"hiatus" => MangaReleaseStatus.OnHiatus,
"complete" => MangaReleaseStatus.Completed,
"ongoing" => MangaReleaseStatus.Continuing,
_ => MangaReleaseStatus.Unreleased
};
HtmlNode yearNode = document.DocumentNode.SelectSingleNode("//ul/li[strong/text() = 'Released: ']/span");
uint year = Convert.ToUInt32(yearNode?.InnerText ?? "0");
HtmlNode descriptionNode = document.DocumentNode.SelectSingleNode("//ul/li[strong/text() = 'Description']/p");
string description = descriptionNode?.InnerText ?? "Undefined";
HtmlNode[] altTitleNodes = document.DocumentNode
.SelectNodes("//ul/li[strong/text() = 'Associated Name(s)']/ul/li")?.ToArray() ?? [];
List<MangaAltTitle> altTitles = altTitleNodes.Select(n => new MangaAltTitle("", n.InnerText)).ToList();
Manga m = new(publicationId, sortName, description, websiteUrl, posterUrl, null, year, null, releaseStatus, -1,
this, authors, tags, [], altTitles);
return (m, authors, tags, [], altTitles);
}
public override (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)? GetMangaFromId(string publicationId)
{
return GetMangaFromUrl($"https://{BaseUris[0]}/series/{publicationId}");
}
public override Chapter[] GetChapters(Manga manga, string language = "en")
{
var requestUrl = $"https://{BaseUris[0]}/series/{manga.MangaConnectorId}/full-chapter-list";
var requestResult =
downloadClient.MakeRequest(requestUrl, RequestType.Default);
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
return [];
//Return Chapters ordered by Chapter-Number
if (requestResult.htmlDocument is null)
return [];
var chapters = ParseChaptersFromHtml(manga, requestResult.htmlDocument);
return chapters.Order().ToArray();
}
internal override string[] GetChapterImageUrls(Chapter chapter)
{
var requestResult = downloadClient.MakeRequest(chapter.Url, RequestType.Default);
if (requestResult.htmlDocument is null)
return [];
var document = requestResult.htmlDocument;
var imageNodes =
document.DocumentNode.SelectNodes($"//section[@hx-get='{chapter.Url}/images']/img")?.ToArray() ?? [];
var urls = imageNodes.Select(imgNode => imgNode.GetAttributeValue("src", "")).ToArray();
return urls;
}
private List<Chapter> ParseChaptersFromHtml(Manga manga, HtmlDocument document)
{
var chaptersWrapper = document.DocumentNode.SelectSingleNode("/html/body");
Regex chapterRex = new(@"(\d+(?:\.\d+)*)");
Regex idRex = new(@"https:\/\/weebcentral\.com\/chapters\/(\w*)");
var ret = chaptersWrapper.Descendants("a").Select(elem =>
{
var url = elem.GetAttributeValue("href", "") ?? "Undefined";
if (!url.StartsWith("https://") && !url.StartsWith("http://"))
return new Chapter(manga, "", "");
var idMatch = idRex.Match(url);
var id = idMatch.Success ? idMatch.Groups[1].Value : null;
var chapterNode = elem.SelectSingleNode("span[@class='grow flex items-center gap-2']/span")?.InnerText ??
"Undefined";
var chapterNumberMatch = chapterRex.Match(chapterNode);
var chapterNumber = chapterNumberMatch.Success ? chapterNumberMatch.Groups[1].Value : "-1";
return new Chapter(manga, url, chapterNumber);
}).Where(elem => elem.ChapterNumber != String.Empty && elem.Url != string.Empty).ToList();
ret.Reverse();
return ret;
}
}

View File

@ -14,100 +14,159 @@ public class PgsqlContext(DbContextOptions<PgsqlContext> options) : DbContext(op
public DbSet<LocalLibrary> LocalLibraries { get; set; } public DbSet<LocalLibrary> LocalLibraries { get; set; }
public DbSet<Chapter> Chapters { get; set; } public DbSet<Chapter> Chapters { get; set; }
public DbSet<Author> Authors { get; set; } public DbSet<Author> Authors { get; set; }
public DbSet<Link> Links { get; set; }
public DbSet<MangaTag> Tags { get; set; } public DbSet<MangaTag> Tags { get; set; }
public DbSet<MangaAltTitle> AltTitles { get; set; }
public DbSet<LibraryConnector> LibraryConnectors { get; set; } public DbSet<LibraryConnector> LibraryConnectors { get; set; }
public DbSet<NotificationConnector> NotificationConnectors { get; set; } public DbSet<NotificationConnector> NotificationConnectors { get; set; }
public DbSet<Notification> Notifications { get; set; } public DbSet<Notification> Notifications { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder) protected override void OnModelCreating(ModelBuilder modelBuilder)
{ {
modelBuilder.Entity<MangaConnector>() //Job Types
.HasDiscriminator(c => c.Name)
.HasValue<Global>("Global")
.HasValue<AsuraToon>("AsuraToon")
.HasValue<Bato>("Bato")
.HasValue<MangaHere>("MangaHere")
.HasValue<MangaKatana>("MangaKatana")
.HasValue<Mangaworld>("Mangaworld")
.HasValue<ManhuaPlus>("ManhuaPlus")
.HasValue<Weebcentral>("Weebcentral")
.HasValue<Manganato>("Manganato")
.HasValue<MangaDex>("MangaDex");
modelBuilder.Entity<LibraryConnector>()
.HasDiscriminator<LibraryType>(l => l.LibraryType)
.HasValue<Komga>(LibraryType.Komga)
.HasValue<Kavita>(LibraryType.Kavita);
modelBuilder.Entity<Job>() modelBuilder.Entity<Job>()
.HasDiscriminator<JobType>(j => j.JobType) .HasDiscriminator(j => j.JobType)
.HasValue<MoveFileOrFolderJob>(JobType.MoveFileOrFolderJob) .HasValue<MoveFileOrFolderJob>(JobType.MoveFileOrFolderJob)
.HasValue<MoveMangaLibraryJob>(JobType.MoveMangaLibraryJob)
.HasValue<DownloadAvailableChaptersJob>(JobType.DownloadAvailableChaptersJob) .HasValue<DownloadAvailableChaptersJob>(JobType.DownloadAvailableChaptersJob)
.HasValue<DownloadSingleChapterJob>(JobType.DownloadSingleChapterJob) .HasValue<DownloadSingleChapterJob>(JobType.DownloadSingleChapterJob)
.HasValue<DownloadMangaCoverJob>(JobType.DownloadMangaCoverJob) .HasValue<DownloadMangaCoverJob>(JobType.DownloadMangaCoverJob)
.HasValue<UpdateMetadataJob>(JobType.UpdateMetaDataJob)
.HasValue<RetrieveChaptersJob>(JobType.RetrieveChaptersJob) .HasValue<RetrieveChaptersJob>(JobType.RetrieveChaptersJob)
.HasValue<UpdateFilesDownloadedJob>(JobType.UpdateFilesDownloadedJob); .HasValue<UpdateFilesDownloadedJob>(JobType.UpdateFilesDownloadedJob);
modelBuilder.Entity<Job>()
.HasMany<Job>() //Job specification
.WithOne(j => j.ParentJob) modelBuilder.Entity<DownloadAvailableChaptersJob>()
.HasOne<Manga>(j => j.Manga)
.WithMany()
.HasForeignKey(j => j.MangaId)
.IsRequired()
.OnDelete(DeleteBehavior.Cascade); .OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<Job>() modelBuilder.Entity<DownloadAvailableChaptersJob>()
.HasMany<Job>(j => j.DependsOnJobs) .Navigation(j => j.Manga)
.WithMany(); .AutoInclude();
modelBuilder.Entity<UpdateMetadataJob>() modelBuilder.Entity<DownloadMangaCoverJob>()
.Navigation(umj => umj.Manga) .HasOne<Manga>(j => j.Manga)
.WithMany()
.HasForeignKey(j => j.MangaId)
.IsRequired()
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<DownloadMangaCoverJob>()
.Navigation(j => j.Manga)
.AutoInclude();
modelBuilder.Entity<DownloadSingleChapterJob>()
.HasOne<Chapter>(j => j.Chapter)
.WithMany()
.HasForeignKey(j => j.ChapterId)
.IsRequired()
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<DownloadSingleChapterJob>()
.Navigation(j => j.Chapter)
.AutoInclude();
modelBuilder.Entity<MoveMangaLibraryJob>()
.HasOne<Manga>(j => j.Manga)
.WithMany()
.HasForeignKey(j => j.MangaId)
.IsRequired()
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<MoveMangaLibraryJob>()
.Navigation(j => j.Manga)
.AutoInclude();
modelBuilder.Entity<MoveMangaLibraryJob>()
.HasOne<LocalLibrary>(j => j.ToLibrary)
.WithMany()
.HasForeignKey(j => j.ToLibraryId)
.IsRequired()
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<MoveMangaLibraryJob>()
.Navigation(j => j.ToLibrary)
.AutoInclude();
modelBuilder.Entity<RetrieveChaptersJob>()
.HasOne<Manga>(j => j.Manga)
.WithMany()
.HasForeignKey(j => j.MangaId)
.IsRequired()
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<RetrieveChaptersJob>()
.Navigation(j => j.Manga)
.AutoInclude(); .AutoInclude();
modelBuilder.Entity<Manga>() //Job has possible ParentJob
.HasOne<MangaConnector>(m => m.MangaConnector) modelBuilder.Entity<Job>()
.WithMany() .HasMany<Job>()
.HasForeignKey(m => m.MangaConnectorId) .WithOne(childJob => childJob.ParentJob)
.HasForeignKey(childjob => childjob.ParentJobId)
.IsRequired(false)
.OnDelete(DeleteBehavior.Cascade);
//Job might be dependent on other Jobs
modelBuilder.Entity<Job>()
.HasMany<Job>(root => root.DependsOnJobs)
.WithMany();
modelBuilder.Entity<Job>()
.Navigation(root => root.DependsOnJobs)
.AutoInclude(false);
//MangaConnector Types
modelBuilder.Entity<MangaConnector>()
.HasDiscriminator(c => c.Name)
.HasValue<Global>("Global")
.HasValue<MangaDex>("MangaDex");
//MangaConnector is responsible for many Manga
modelBuilder.Entity<MangaConnector>()
.HasMany<Manga>()
.WithOne(m => m.MangaConnector)
.HasForeignKey(m => m.MangaConnectorName)
.IsRequired()
.OnDelete(DeleteBehavior.Cascade); .OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<Manga>() modelBuilder.Entity<Manga>()
.Navigation(m => m.MangaConnector) .Navigation(m => m.MangaConnector)
.AutoInclude(); .AutoInclude();
//Manga has many Chapters
modelBuilder.Entity<Manga>() modelBuilder.Entity<Manga>()
.HasOne<LocalLibrary>(m => m.Library) .HasMany<Chapter>(m => m.Chapters)
.WithMany() .WithOne(c => c.ParentManga)
.OnDelete(DeleteBehavior.Restrict);
modelBuilder.Entity<Manga>()
.Navigation(m => m.Library)
.AutoInclude();
modelBuilder.Entity<Manga>()
.HasMany<Author>(m => m.Authors)
.WithMany();
modelBuilder.Entity<Manga>()
.Navigation(m => m.Authors)
.AutoInclude();
modelBuilder.Entity<Manga>()
.HasMany<MangaTag>(m => m.MangaTags)
.WithMany();
modelBuilder.Entity<Manga>()
.Navigation(m => m.MangaTags)
.AutoInclude();
modelBuilder.Entity<Manga>()
.HasMany<Link>(m => m.Links)
.WithOne()
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<Manga>()
.Navigation(m => m.Links)
.AutoInclude();
modelBuilder.Entity<Manga>()
.HasMany<MangaAltTitle>(m => m.AltTitles)
.WithOne()
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<Manga>()
.Navigation(m => m.AltTitles)
.AutoInclude();
modelBuilder.Entity<Chapter>()
.HasOne<Manga>(c => c.ParentManga)
.WithMany()
.HasForeignKey(c => c.ParentMangaId) .HasForeignKey(c => c.ParentMangaId)
.IsRequired()
.OnDelete(DeleteBehavior.Cascade); .OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<Chapter>() modelBuilder.Entity<Chapter>()
.Navigation(c => c.ParentManga) .Navigation(c => c.ParentManga)
.AutoInclude(); .AutoInclude();
//Manga owns MangaAltTitles
modelBuilder.Entity<Manga>()
.OwnsMany<MangaAltTitle>(m => m.AltTitles)
.WithOwner();
//Manga owns Links
modelBuilder.Entity<Manga>()
.OwnsMany<Link>(m => m.Links)
.WithOwner();
//Manga has many Tags associated with many Manga
modelBuilder.Entity<Manga>()
.HasMany<MangaTag>(m => m.MangaTags)
.WithMany()
.UsingEntity("MangaTagToManga",
l=> l.HasOne(typeof(MangaTag)).WithMany().HasForeignKey("MangaTagIds").HasPrincipalKey(nameof(MangaTag.Tag)),
r => r.HasOne(typeof(Manga)).WithMany().HasForeignKey("MangaIds").HasPrincipalKey(nameof(Manga.MangaId)),
j => j.HasKey("MangaTagIds", "MangaIds")
);
//Manga has many Authors associated with many Manga
modelBuilder.Entity<Manga>()
.HasMany<Author>(m => m.Authors)
.WithMany()
.UsingEntity("AuthorToManga",
l=> l.HasOne(typeof(Author)).WithMany().HasForeignKey("AuthorIds").HasPrincipalKey(nameof(Author.AuthorId)),
r => r.HasOne(typeof(Manga)).WithMany().HasForeignKey("MangaIds").HasPrincipalKey(nameof(Manga.MangaId)),
j => j.HasKey("AuthorIds", "MangaIds")
);
//LocalLibrary has many Mangas
modelBuilder.Entity<LocalLibrary>()
.HasMany<Manga>()
.WithOne(m => m.Library)
.HasForeignKey(m => m.LibraryId)
.OnDelete(DeleteBehavior.SetNull);
//LibraryConnector Types
modelBuilder.Entity<LibraryConnector>()
.HasDiscriminator(l => l.LibraryType)
.HasValue<Komga>(LibraryType.Komga)
.HasValue<Kavita>(LibraryType.Kavita);
} }
} }

View File

@ -119,50 +119,35 @@ public static class Tranga
Log.Info("JobStarter Thread running."); Log.Info("JobStarter Thread running.");
while (true) while (true)
{ {
List<Job> completedJobs = context.Jobs.Where(j => j.state >= JobState.Completed).ToList(); //Update finished Jobs to new states
Log.Debug($"Completed jobs: {completedJobs.Count}"); List<Job> completedJobs = context.Jobs.Where(j => j.state == JobState.Completed).ToList();
foreach (Job job in completedJobs) foreach (Job completedJob in completedJobs)
if (job.RecurrenceMs <= 0) if (completedJob.RecurrenceMs <= 0)
context.Jobs.Remove(job); context.Jobs.Remove(completedJob);
else else
{ {
if (job.state >= JobState.Failed) completedJob.state = JobState.CompletedWaiting;
job.Enabled = false; completedJob.LastExecution = DateTime.UtcNow;
else }
job.state = JobState.Waiting; List<Job> failedJobs = context.Jobs.Where(j => j.state == JobState.Failed).ToList();
job.LastExecution = DateTime.UtcNow; foreach (Job failedJob in failedJobs)
{
failedJob.Enabled = false;
failedJob.LastExecution = DateTime.UtcNow;
} }
List<Job> runJobs = context.Jobs.Where(j => j.state <= JobState.Running && j.Enabled == true).ToList() //Retrieve waiting and due Jobs
.Where(j => j.NextExecution < DateTime.UtcNow).ToList(); List<Job> waitingJobs = context.Jobs.Where(j =>
IEnumerable<Job> orderedJobs = OrderJobs(runJobs, context).ToList(); j.Enabled && (j.state == JobState.FirstExecution || j.state == JobState.CompletedWaiting)).ToList();
Log.Debug($"Jobs Due: {runJobs.Count} Running: {RunningJobs.Count} Ordered: {orderedJobs.Count()}"); List<Job> runningJobs = context.Jobs.Where(j => j.state == JobState.Running).ToList();
foreach (Job job in orderedJobs) List<Job> dueJobs = waitingJobs.Where(j => j.NextExecution < DateTime.UtcNow).ToList();
{
// If the job is already running, skip it
if (RunningJobs.Values.Any(j => j.JobId == job.JobId)) continue;
//If a Job for that connector is already running, skip it List<MangaConnector> busyConnectors = GetBusyConnectors(runningJobs);
if (job is DownloadAvailableChaptersJob dncj) List<Job> startJobs = FilterJobPreconditions(dueJobs, busyConnectors);
{
if (RunningJobs.Values.Any(j =>
j is DownloadAvailableChaptersJob rdncj &&
context.Mangas.Find(rdncj.MangaId)?.MangaConnector == context.Mangas.Find(dncj.MangaId)?.MangaConnector))
{
continue;
}
}
else if (job is DownloadSingleChapterJob dscj)
{
if (RunningJobs.Values.Any(j =>
j is DownloadSingleChapterJob rdscj &&
context.Chapters.Find(rdscj.ChapterId)?.ParentManga?.MangaConnector ==
context.Chapters.Find(dscj.ChapterId)?.ParentManga?.MangaConnector))
{
continue;
}
}
//Start Jobs that are allowed to run (preconditions match)
foreach (Job job in startJobs)
{
Thread t = new(() => Thread t = new(() =>
{ {
job.Run(serviceProvider); job.Run(serviceProvider);
@ -170,6 +155,10 @@ public static class Tranga
RunningJobs.Add(t, job); RunningJobs.Add(t, job);
t.Start(); t.Start();
} }
Log.Debug($"Jobs Completed: {completedJobs.Count} Failed: {failedJobs.Count} Running: {runningJobs.Count}\n" +
$"Waiting: {waitingJobs.Count}\n" +
$"\tof which Due: {dueJobs.Count}\n" +
$"\t\tof which Started: {startJobs.Count}");
(Thread, Job)[] removeFromThreadsList = RunningJobs.Where(t => !t.Key.IsAlive) (Thread, Job)[] removeFromThreadsList = RunningJobs.Where(t => !t.Key.IsAlive)
.Select(t => (t.Key, t.Value)).ToArray(); .Select(t => (t.Key, t.Value)).ToArray();
@ -191,84 +180,39 @@ public static class Tranga
} }
} }
private static IEnumerable<Job> OrderJobs(List<Job> jobs, PgsqlContext context) private static List<MangaConnector> GetBusyConnectors(List<Job> runningJobs)
{ {
Dictionary<JobType, List<Job>> jobsByType = new(); HashSet<MangaConnector> busyConnectors = new();
foreach (Job job in jobs) foreach (Job runningJob in runningJobs)
if(!jobsByType.TryAdd(job.JobType, [job]))
jobsByType[job.JobType].Add(job);
IEnumerable<Job> ret = new List<Job>();
if(jobsByType.ContainsKey(JobType.MoveMangaLibraryJob))
ret = ret.Concat(jobsByType[JobType.MoveMangaLibraryJob]);
if(jobsByType.ContainsKey(JobType.MoveFileOrFolderJob))
ret = ret.Concat(jobsByType[JobType.MoveFileOrFolderJob]);
if(jobsByType.ContainsKey(JobType.DownloadMangaCoverJob))
ret = ret.Concat(jobsByType[JobType.DownloadMangaCoverJob]);
if(jobsByType.ContainsKey(JobType.UpdateFilesDownloadedJob))
ret = ret.Concat(jobsByType[JobType.UpdateFilesDownloadedJob]);
Dictionary<MangaConnector, List<Job>> metadataJobsByConnector = new();
if (jobsByType.ContainsKey(JobType.DownloadAvailableChaptersJob))
{ {
foreach (DownloadAvailableChaptersJob job in jobsByType[JobType.DownloadAvailableChaptersJob]) if(GetJobConnector(runningJob) is { } mangaConnector)
{ busyConnectors.Add(mangaConnector);
Manga? manga = context.Mangas.Find(job.MangaId);
if(manga is null)
continue;
MangaConnector connector = manga.MangaConnector ?? context.MangaConnectors.Find(manga.MangaConnectorId)!;
if(!metadataJobsByConnector.TryAdd(connector, [job]))
metadataJobsByConnector[connector].Add(job);
} }
} return busyConnectors.ToList();
if (jobsByType.ContainsKey(JobType.UpdateMetaDataJob))
{
foreach (UpdateMetadataJob job in jobsByType[JobType.UpdateMetaDataJob])
{
Manga manga = job.Manga ?? context.Mangas.Find(job.MangaId)!;
MangaConnector connector = manga.MangaConnector ?? context.MangaConnectors.Find(manga.MangaConnectorId)!;
if(!metadataJobsByConnector.TryAdd(connector, [job]))
metadataJobsByConnector[connector].Add(job);
}
}
if (jobsByType.ContainsKey(JobType.RetrieveChaptersJob))
{
foreach (RetrieveChaptersJob job in jobsByType[JobType.RetrieveChaptersJob])
{
Manga? manga = context.Mangas.Find(job.MangaId);
if(manga is null)
continue;
MangaConnector connector = manga.MangaConnector ?? context.MangaConnectors.Find(manga.MangaConnectorId)!;
if(!metadataJobsByConnector.TryAdd(connector, [job]))
metadataJobsByConnector[connector].Add(job);
}
}
foreach (List<Job> metadataJobs in metadataJobsByConnector.Values)
ret = ret.Append(metadataJobs.MinBy(j => j.NextExecution))!;
if (jobsByType.ContainsKey(JobType.DownloadSingleChapterJob))
{
Dictionary<MangaConnector, List<DownloadSingleChapterJob>> downloadJobsByConnector = new();
foreach (DownloadSingleChapterJob job in jobsByType[JobType.DownloadSingleChapterJob])
{
Chapter? chapter = context.Chapters.Find(job.ChapterId);
if(chapter is null)
continue;
Manga manga = chapter.ParentManga ?? context.Mangas.Find(chapter.ParentMangaId)!;
MangaConnector connector = manga.MangaConnector ?? context.MangaConnectors.Find(manga.MangaConnectorId)!;
if(!downloadJobsByConnector.TryAdd(connector, [job]))
downloadJobsByConnector[connector].Add(job);
}
//From all jobs select those that are supposed to be executed soonest, then select the minimum chapternumber
foreach (List<DownloadSingleChapterJob> downloadJobs in downloadJobsByConnector.Values)
ret = ret.Append(
downloadJobs.Where(j => j.NextExecution == downloadJobs
.MinBy(mj => mj.NextExecution)!.NextExecution)
.MinBy(j => context.Chapters.Find(j.ChapterId)!))!;
} }
return ret; private static List<Job> FilterJobPreconditions(List<Job> dueJobs, List<MangaConnector> busyConnectors) =>
dueJobs
.Where(j => j.DependenciesFulfilled)
.Where(j =>
{
//Filter jobs with busy connectors
if (GetJobConnector(j) is { } mangaConnector)
return busyConnectors.Contains(mangaConnector) == false;
return true;
})
.ToList();
private static MangaConnector? GetJobConnector(Job job)
{
if (job is DownloadAvailableChaptersJob dacj)
return dacj.Manga.MangaConnector;
if (job is DownloadMangaCoverJob dmcj)
return dmcj.Manga.MangaConnector;
if (job is DownloadSingleChapterJob dscj)
return dscj.Chapter.ParentManga.MangaConnector;
if (job is RetrieveChaptersJob rcj)
return rcj.Manga.MangaConnector;
return null;
} }
} }