Compare commits

..

No commits in common. "fb2b4d6920f0c7a4992f40c10e4b304b75874e96" and "7477f4d04d4afd556236d65ee1c74ab838d80a78" have entirely different histories.

81 changed files with 6734 additions and 4232 deletions

View File

@ -19,7 +19,6 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Proxies" Version="9.0.5" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="Npgsql" Version="9.0.3" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4" />

View File

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

View File

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

View File

@ -0,0 +1,16 @@
using System.Text.RegularExpressions;
namespace API.APIEndpointRecords;
public record LunaseaRecord(string id)
{
private static Regex validateRex = new(@"(?:device|user)\/[0-9a-zA-Z\-]+");
public bool Validate()
{
if (id == string.Empty)
return false;
if (!validateRex.IsMatch(id))
return false;
return true;
}
}

View File

@ -1,19 +1,16 @@
using API.APIEndpointRecords;
using API.Schema;
using API.Schema.Contexts;
using API.Schema.Jobs;
using Asp.Versioning;
using log4net;
using Microsoft.AspNetCore.Mvc;
using static Microsoft.AspNetCore.Http.StatusCodes;
// ReSharper disable InconsistentNaming
namespace API.Controllers;
[ApiVersion(2)]
[ApiController]
[Route("v{version:apiVersion}/[controller]")]
public class JobController(PgsqlContext context, ILog Log) : Controller
public class JobController(PgsqlContext context) : Controller
{
/// <summary>
/// Returns all Jobs
@ -105,7 +102,7 @@ public class JobController(PgsqlContext context, ILog Log) : Controller
/// <param name="MangaId">ID of Manga</param>
/// <param name="record">Job-Configuration</param>
/// <response code="201">Job-IDs</response>
/// <response code="400">Could not find ToLibrary with ID</response>
/// <response code="400">Could not find Library with ID</response>
/// <response code="404">Could not find Manga with ID</response>
/// <response code="500">Error during Database Operation</response>
[HttpPut("DownloadAvailableChaptersJob/{MangaId}")]
@ -113,7 +110,7 @@ public class JobController(PgsqlContext context, ILog Log) : Controller
[ProducesResponseType(Status400BadRequest)]
[ProducesResponseType(Status404NotFound)]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public IActionResult CreateDownloadAvailableChaptersJob(string MangaId, [FromBody]DownloadAvailableChaptersJobRecord record)
public IActionResult CreateDownloadAvailableChaptersJob(string MangaId, [FromBody]DownloadAvailableJobsRecord record)
{
if (context.Mangas.Find(MangaId) is not { } m)
return NotFound();
@ -129,17 +126,13 @@ public class JobController(PgsqlContext context, ILog Log) : Controller
}
catch (Exception e)
{
Log.Error(e);
return StatusCode(500, e.Message);
}
}
Job retrieveChapters = new RetrieveChaptersJob(m, record.language, record.recurrenceTimeMs);
Job updateFilesDownloaded =
new UpdateChaptersDownloadedJob(m, record.recurrenceTimeMs, dependsOnJobs: [retrieveChapters]);
Job downloadChapters = new DownloadAvailableChaptersJob(m, record.recurrenceTimeMs, dependsOnJobs: [retrieveChapters, updateFilesDownloaded]);
retrieveChapters.ParentJob = downloadChapters;
updateFilesDownloaded.ParentJob = retrieveChapters;
return AddJobs([retrieveChapters, downloadChapters, updateFilesDownloaded]);
Job job = new DownloadAvailableChaptersJob(record.recurrenceTimeMs, MangaId);
Job dep = new RetrieveChaptersJob(record.recurrenceTimeMs, MangaId, job.JobId);
job.DependsOnJobsIds?.Add(dep.JobId);
return AddJobs([dep, job]);
}
/// <summary>
@ -155,14 +148,14 @@ public class JobController(PgsqlContext context, ILog Log) : Controller
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public IActionResult CreateNewDownloadChapterJob(string ChapterId)
{
if(context.Chapters.Find(ChapterId) is not { } c)
if(context.Chapters.Find(ChapterId) is null)
return NotFound();
Job job = new DownloadSingleChapterJob(c);
Job job = new DownloadSingleChapterJob(ChapterId);
return AddJobs([job]);
}
/// <summary>
/// Create a new UpdateChaptersDownloadedJob
/// Create a new UpdateFilesDownloadedJob
/// </summary>
/// <param name="MangaId">ID of the Manga</param>
/// <response code="201">Job-IDs</response>
@ -174,9 +167,9 @@ public class JobController(PgsqlContext context, ILog Log) : Controller
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public IActionResult CreateUpdateFilesDownloadedJob(string MangaId)
{
if(context.Mangas.Find(MangaId) is not { } m)
if(context.Mangas.Find(MangaId) is null)
return NotFound();
Job job = new UpdateChaptersDownloadedJob(m, 0);
Job job = new UpdateFilesDownloadedJob(0, MangaId);
return AddJobs([job]);
}
@ -190,7 +183,8 @@ public class JobController(PgsqlContext context, ILog Log) : Controller
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public IActionResult CreateUpdateAllFilesDownloadedJob()
{
List<UpdateChaptersDownloadedJob> jobs = context.Mangas.Select(m => new UpdateChaptersDownloadedJob(m, 0, null, null)).ToList();
List<string> ids = context.Mangas.Select(m => m.MangaId).ToList();
List<UpdateFilesDownloadedJob> jobs = ids.Select(id => new UpdateFilesDownloadedJob(0, id)).ToList();
try
{
context.Jobs.AddRange(jobs);
@ -199,13 +193,12 @@ public class JobController(PgsqlContext context, ILog Log) : Controller
}
catch (Exception e)
{
Log.Error(e);
return StatusCode(500, e.Message);
}
}
/// <summary>
/// Not Implemented: Create a new UpdateMetadataJob
/// Create a new UpdateMetadataJob
/// </summary>
/// <param name="MangaId">ID of the Manga</param>
/// <response code="201">Job-IDs</response>
@ -217,11 +210,14 @@ public class JobController(PgsqlContext context, ILog Log) : Controller
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public IActionResult CreateUpdateMetadataJob(string MangaId)
{
return StatusCode(Status501NotImplemented);
if(context.Mangas.Find(MangaId) is null)
return NotFound();
Job job = new UpdateMetadataJob(0, MangaId);
return AddJobs([job]);
}
/// <summary>
/// Not Implemented: Create a new UpdateMetadataJob for all Manga
/// Create a new UpdateMetadataJob for all Manga
/// </summary>
/// <response code="201">Job-IDs</response>
/// <response code="500">Error during Database Operation</response>
@ -230,7 +226,18 @@ public class JobController(PgsqlContext context, ILog Log) : Controller
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public IActionResult CreateUpdateAllMetadataJob()
{
return StatusCode(Status501NotImplemented);
List<string> ids = context.Mangas.Select(m => m.MangaId).ToList();
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)
@ -243,7 +250,6 @@ public class JobController(PgsqlContext context, ILog Log) : Controller
}
catch (Exception e)
{
Log.Error(e);
return StatusCode(500, e.Message);
}
}
@ -263,7 +269,8 @@ public class JobController(PgsqlContext context, ILog Log) : Controller
{
try
{
if(context.Jobs.Find(JobId) is not { } ret)
Job? ret = context.Jobs.Find(JobId);
if(ret is null)
return NotFound();
context.Remove(ret);
@ -272,7 +279,6 @@ public class JobController(PgsqlContext context, ILog Log) : Controller
}
catch (Exception e)
{
Log.Error(e);
return StatusCode(500, e.Message);
}
}
@ -316,7 +322,6 @@ public class JobController(PgsqlContext context, ILog Log) : Controller
}
catch (Exception e)
{
Log.Error(e);
return StatusCode(500, e.Message);
}
}
@ -349,7 +354,6 @@ public class JobController(PgsqlContext context, ILog Log) : Controller
}
catch (Exception e)
{
Log.Error(e);
return StatusCode(500, e.Message);
}
}
@ -363,6 +367,6 @@ public class JobController(PgsqlContext context, ILog Log) : Controller
[ProducesResponseType(Status501NotImplemented)]
public IActionResult StopJob(string JobId)
{
return StatusCode(Status501NotImplemented);
return StatusCode(501);
}
}

View File

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

View File

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

View File

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

View File

@ -1,22 +1,19 @@
using API.Schema;
using API.Schema.Contexts;
using API.Schema.Jobs;
using Asp.Versioning;
using log4net;
using Microsoft.AspNetCore.Mvc;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Formats.Jpeg;
using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp.Processing.Processors.Transforms;
using static Microsoft.AspNetCore.Http.StatusCodes;
// ReSharper disable InconsistentNaming
namespace API.Controllers;
[ApiVersion(2)]
[ApiController]
[Route("v{v:apiVersion}/[controller]")]
public class MangaController(PgsqlContext context, ILog Log) : Controller
public class MangaController(PgsqlContext context) : Controller
{
/// <summary>
/// Returns all cached Manga
@ -85,7 +82,6 @@ public class MangaController(PgsqlContext context, ILog Log) : Controller
}
catch (Exception e)
{
Log.Error(e);
return StatusCode(500, e.Message);
}
}
@ -109,15 +105,17 @@ public class MangaController(PgsqlContext context, ILog Log) : Controller
[ProducesResponseType<int>(Status503ServiceUnavailable, "text/plain")]
public IActionResult GetCover(string MangaId, [FromQuery]int? width, [FromQuery]int? height)
{
if(context.Mangas.Find(MangaId) is not { } m)
DateTime requestStarted = HttpContext.Features.Get<IHttpRequestTimeFeature>()?.RequestTime ?? DateTime.Now;
Manga? m = context.Mangas.Find(MangaId);
if (m is null)
return NotFound();
if (!System.IO.File.Exists(m.CoverFileNameInCache))
{
List<Job> coverDownloadJobs = context.Jobs.Where(j => j.JobType == JobType.DownloadMangaCoverJob).ToList();
if (coverDownloadJobs.Any(j => j is DownloadMangaCoverJob dmc && dmc.MangaId == MangaId && dmc.state < JobState.Completed))
if (coverDownloadJobs.Any(j => j is DownloadMangaCoverJob dmc && dmc.MangaId == MangaId))
{
Response.Headers.Append("Retry-After", $"{TrangaSettings.startNewJobTimeoutMs * coverDownloadJobs.Count() * 2 / 1000:D}");
Response.Headers.Add("Retry-After", $"{TrangaSettings.startNewJobTimeoutMs * coverDownloadJobs.Count() * 2 / 1000:D}");
return StatusCode(Status503ServiceUnavailable, TrangaSettings.startNewJobTimeoutMs * coverDownloadJobs.Count() * 2 / 1000);
}
else
@ -153,11 +151,12 @@ public class MangaController(PgsqlContext context, ILog Log) : Controller
[ProducesResponseType(Status404NotFound)]
public IActionResult GetChapters(string MangaId)
{
if(context.Mangas.Find(MangaId) is not { } m)
Manga? m = context.Mangas.Find(MangaId);
if (m is null)
return NotFound();
Chapter[] chapters = m.Chapters.ToArray();
return Ok(chapters);
Chapter[] ret = context.Chapters.Where(c => c.ParentMangaId == m.MangaId).ToArray();
return Ok(ret);
}
/// <summary>
@ -173,10 +172,11 @@ public class MangaController(PgsqlContext context, ILog Log) : Controller
[ProducesResponseType(Status404NotFound)]
public IActionResult GetChaptersDownloaded(string MangaId)
{
if(context.Mangas.Find(MangaId) is not { } m)
Manga? m = context.Mangas.Find(MangaId);
if (m is null)
return NotFound();
List<Chapter> chapters = m.Chapters.ToList();
List<Chapter> chapters = context.Chapters.Where(c => c.ParentMangaId == m.MangaId && c.Downloaded == true).ToList();
if (chapters.Count == 0)
return NoContent();
@ -196,10 +196,11 @@ public class MangaController(PgsqlContext context, ILog Log) : Controller
[ProducesResponseType(Status404NotFound)]
public IActionResult GetChaptersNotDownloaded(string MangaId)
{
if(context.Mangas.Find(MangaId) is not { } m)
Manga? m = context.Mangas.Find(MangaId);
if (m is null)
return NotFound();
List<Chapter> chapters = m.Chapters.ToList();
List<Chapter> chapters = context.Chapters.Where(c => c.ParentMangaId == m.MangaId && c.Downloaded == false).ToList();
if (chapters.Count == 0)
return NoContent();
@ -223,19 +224,20 @@ public class MangaController(PgsqlContext context, ILog Log) : Controller
[ProducesResponseType<int>(Status503ServiceUnavailable, "text/plain")]
public IActionResult GetLatestChapter(string MangaId)
{
if(context.Mangas.Find(MangaId) is not { } m)
Manga? m = context.Mangas.Find(MangaId);
if (m is null)
return NotFound();
List<Chapter> chapters = m.Chapters.ToList();
List<Chapter> chapters = context.Chapters.Where(c => c.ParentMangaId == m.MangaId).ToList();
if (chapters.Count == 0)
{
List<Job> retrieveChapterJobs = context.Jobs.Where(j => j.JobType == JobType.RetrieveChaptersJob).ToList();
if (retrieveChapterJobs.Any(j => j is RetrieveChaptersJob rcj && rcj.MangaId == MangaId && rcj.state < JobState.Completed))
if (retrieveChapterJobs.Any(j => j is RetrieveChaptersJob rcj && rcj.MangaId == MangaId))
{
Response.Headers.Append("Retry-After", $"{TrangaSettings.startNewJobTimeoutMs * retrieveChapterJobs.Count() * 2 / 1000:D}");
Response.Headers.Add("Retry-After", $"{TrangaSettings.startNewJobTimeoutMs * retrieveChapterJobs.Count() * 2 / 1000:D}");
return StatusCode(Status503ServiceUnavailable, TrangaSettings.startNewJobTimeoutMs * retrieveChapterJobs.Count() * 2/ 1000);
}else
return Ok(0);
return NoContent();
}
Chapter? max = chapters.Max();
@ -262,16 +264,18 @@ public class MangaController(PgsqlContext context, ILog Log) : Controller
[ProducesResponseType<int>(Status503ServiceUnavailable, "text/plain")]
public IActionResult GetLatestChapterDownloaded(string MangaId)
{
if(context.Mangas.Find(MangaId) is not { } m)
Manga? m = context.Mangas.Find(MangaId);
if (m is null)
return NotFound();
List<Chapter> chapters = m.Chapters.ToList();
List<Chapter> chapters = context.Chapters.Where(c => c.ParentMangaId == m.MangaId && c.Downloaded == true).ToList();
if (chapters.Count == 0)
{
List<Job> retrieveChapterJobs = context.Jobs.Where(j => j.JobType == JobType.RetrieveChaptersJob).ToList();
if (retrieveChapterJobs.Any(j => j is RetrieveChaptersJob rcj && rcj.MangaId == MangaId && rcj.state < JobState.Completed))
if (retrieveChapterJobs.Any(j => j is RetrieveChaptersJob rcj && rcj.MangaId == MangaId))
{
Response.Headers.Append("Retry-After", $"{TrangaSettings.startNewJobTimeoutMs * retrieveChapterJobs.Count() * 2 / 1000:D}");
Response.Headers.Add("Retry-After", $"{TrangaSettings.startNewJobTimeoutMs * retrieveChapterJobs.Count() * 2 / 1000:D}");
return StatusCode(Status503ServiceUnavailable, TrangaSettings.startNewJobTimeoutMs * retrieveChapterJobs.Count() * 2 / 1000);
}else
return NoContent();
@ -288,7 +292,6 @@ public class MangaController(PgsqlContext context, ILog Log) : Controller
/// Configure the cut-off for Manga
/// </summary>
/// <param name="MangaId">Manga-ID</param>
/// <param name="chapterThreshold">Threshold (Chapter Number)</param>
/// <response code="200"></response>
/// <response code="404">Manga with ID not found.</response>
/// <response code="500">Error during Database Operation</response>
@ -304,22 +307,21 @@ public class MangaController(PgsqlContext context, ILog Log) : Controller
try
{
m.IgnoreChaptersBefore = chapterThreshold;
m.IgnoreChapterBefore = chapterThreshold;
context.SaveChanges();
return Ok();
}
catch (Exception e)
{
Log.Error(e);
return StatusCode(500, e.Message);
}
}
/// <summary>
/// Move Manga to different ToLibrary
/// Move Manga to different Library
/// </summary>
/// <param name="MangaId">Manga-ID</param>
/// <param name="LibraryId">ToLibrary-Id</param>
/// <param name="LibraryId">Library-Id</param>
/// <response code="202">Folder is going to be moved</response>
/// <response code="404">MangaId or LibraryId not found</response>
/// <response code="500">Error during Database Operation</response>
@ -329,23 +331,24 @@ public class MangaController(PgsqlContext context, ILog Log) : Controller
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public IActionResult MoveFolder(string MangaId, string LibraryId)
{
if (context.Mangas.Find(MangaId) is not { } manga)
Manga? manga = context.Mangas.Find(MangaId);
if (manga is null)
return NotFound();
if(context.LocalLibraries.Find(LibraryId) is not { } library)
LocalLibrary? library = context.LocalLibraries.Find(LibraryId);
if (library is null)
return NotFound();
MoveMangaLibraryJob moveLibrary = new(manga, library);
UpdateChaptersDownloadedJob updateDownloadedFiles = new(manga, 0, dependsOnJobs: [moveLibrary]);
MoveMangaLibraryJob dep = new (MangaId, LibraryId);
UpdateFilesDownloadedJob up = new (0, manga.MangaId, null, [dep.JobId]);
try
{
context.Jobs.AddRange(moveLibrary, updateDownloadedFiles);
context.Jobs.AddRange([dep, up]);
context.SaveChanges();
return Accepted();
}
catch (Exception e)
{
Log.Error(e);
return StatusCode(500, e.Message);
}
}

View File

@ -1,9 +1,8 @@
using System.Text;
using API.APIEndpointRecords;
using API.Schema.Contexts;
using API.Schema;
using API.Schema.NotificationConnectors;
using Asp.Versioning;
using log4net;
using Microsoft.AspNetCore.Mvc;
using static Microsoft.AspNetCore.Http.StatusCodes;
@ -13,7 +12,7 @@ namespace API.Controllers;
[ApiController]
[Produces("application/json")]
[Route("v{v:apiVersion}/[controller]")]
public class NotificationConnectorController(NotificationsContext context, ILog Log) : Controller
public class NotificationConnectorController(PgsqlContext context) : Controller
{
/// <summary>
/// Gets all configured Notification-Connectors
@ -70,7 +69,6 @@ public class NotificationConnectorController(NotificationsContext context, ILog
}
catch (Exception e)
{
Log.Error(e);
return StatusCode(500, e.Message);
}
}
@ -134,6 +132,32 @@ public class NotificationConnectorController(NotificationsContext context, ILog
return CreateConnector(ntfyConnector);
}
/// <summary>
/// Creates a new Lunasea-Notification-Connector
/// </summary>
/// <remarks>https://docs.lunasea.app/lunasea/notifications/custom-notifications for id. Either device/:device_id or user/:user_id</remarks>
/// <response code="201">ID of new connector</response>
/// <response code="400"></response>
/// <response code="409">A NotificationConnector with name already exists</response>
/// <response code="500">Error during Database Operation</response>
[HttpPut("Lunasea")]
[ProducesResponseType<string>(Status201Created, "application/json")]
[ProducesResponseType(Status400BadRequest)]
[ProducesResponseType(Status409Conflict)]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public IActionResult CreateLunaseaConnector([FromBody]LunaseaRecord lunaseaRecord)
{
if(!lunaseaRecord.Validate())
return BadRequest();
NotificationConnector lunaseaConnector = new (TokenGen.CreateToken("Lunasea"),
$"https://notify.lunasea.app/v1/custom/{lunaseaRecord.id}",
new Dictionary<string, string>(),
"POST",
"{\"title\": \"%title\", \"body\": \"%text\"}");
return CreateConnector(lunaseaConnector);
}
/// <summary>
/// Creates a new Pushover-Notification-Connector
/// </summary>
@ -185,7 +209,6 @@ public class NotificationConnectorController(NotificationsContext context, ILog
}
catch (Exception e)
{
Log.Error(e);
return StatusCode(500, e.Message);
}
}

View File

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

View File

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

View File

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

View File

@ -15,7 +15,7 @@ internal abstract class DownloadClient
public RequestResult MakeRequest(string url, RequestType requestType, string? referrer = null, string? clickButton = null)
{
Log.Debug($"Requesting {requestType} {url}");
Log.Debug($"Requesting {url}");
if (!TrangaSettings.requestLimits.ContainsKey(requestType))
{
return new RequestResult(HttpStatusCode.NotAcceptable, null, Stream.Null);
@ -30,7 +30,7 @@ internal abstract class DownloadClient
LastExecutedRateLimit.TryAdd(requestType, now.Subtract(timeBetweenRequests));
TimeSpan rateLimitTimeout = timeBetweenRequests.Subtract(now.Subtract(LastExecutedRateLimit[requestType]));
Log.Debug($"Request limit {requestType} {rateLimit}/Minute timeBetweenRequests: {timeBetweenRequests:ss'.'fffff} Timeout: {rateLimitTimeout:ss'.'fffff}");
Log.Debug($"Request limit {rateLimit}/Minute timeBetweenRequests: {timeBetweenRequests:ss'.'fffff} Timeout: {rateLimitTimeout:ss'.'fffff}");
if (rateLimitTimeout > TimeSpan.Zero)
{
@ -39,7 +39,6 @@ internal abstract class DownloadClient
RequestResult result = MakeRequestInternal(url, referrer, clickButton);
LastExecutedRateLimit[requestType] = DateTime.UtcNow;
Log.Debug($"Result {url}: {result}");
return result;
}

View File

@ -24,10 +24,4 @@ public struct RequestResult
this.hasBeenRedirected = hasBeenRedirected;
redirectedToUrl = redirectedTo;
}
public override string ToString()
{
return
$"{(int)statusCode} {statusCode.ToString()} {(hasBeenRedirected ? "Redirected: " : "")} {redirectedToUrl}";
}
}

View File

@ -0,0 +1,821 @@
// <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("20250316143014_dev-160325-Initial")]
partial class dev160325Initial
{
/// <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")
.IsRequired()
.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.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.Cascade);
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,16 +1,20 @@
using System;
using System.Collections.Generic;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace API.Migrations.pgsql
namespace API.Migrations
{
/// <inheritdoc />
public partial class Initial1 : Migration
public partial class dev160325Initial : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterDatabase()
.Annotation("Npgsql:PostgresExtension:hstore", ",,");
migrationBuilder.CreateTable(
name: "Authors",
columns: table => new
@ -23,6 +27,20 @@ namespace API.Migrations.pgsql
table.PrimaryKey("PK_Authors", x => x.AuthorId);
});
migrationBuilder.CreateTable(
name: "LibraryConnectors",
columns: table => new
{
LibraryConnectorId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
LibraryType = table.Column<byte>(type: "smallint", nullable: false),
BaseUrl = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
Auth = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_LibraryConnectors", x => x.LibraryConnectorId);
});
migrationBuilder.CreateTable(
name: "LocalLibraries",
columns: table => new
@ -51,6 +69,36 @@ namespace API.Migrations.pgsql
table.PrimaryKey("PK_MangaConnectors", x => x.Name);
});
migrationBuilder.CreateTable(
name: "NotificationConnectors",
columns: table => new
{
Name = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
Url = table.Column<string>(type: "character varying(2048)", maxLength: 2048, nullable: false),
Headers = table.Column<Dictionary<string, string>>(type: "hstore", nullable: false),
HttpMethod = table.Column<string>(type: "character varying(8)", maxLength: 8, nullable: false),
Body = table.Column<string>(type: "character varying(4096)", maxLength: 4096, nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_NotificationConnectors", x => x.Name);
});
migrationBuilder.CreateTable(
name: "Notifications",
columns: table => new
{
NotificationId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
Urgency = table.Column<byte>(type: "smallint", nullable: false),
Title = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: false),
Message = table.Column<string>(type: "character varying(512)", maxLength: 512, nullable: false),
Date = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Notifications", x => x.NotificationId);
});
migrationBuilder.CreateTable(
name: "Tags",
columns: table => new
@ -67,56 +115,76 @@ namespace API.Migrations.pgsql
columns: table => new
{
MangaId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
IdOnConnectorSite = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
Name = table.Column<string>(type: "character varying(512)", maxLength: 512, nullable: false),
IdOnConnectorSite = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: false),
Name = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
Description = table.Column<string>(type: "text", nullable: false),
WebsiteUrl = table.Column<string>(type: "character varying(512)", maxLength: 512, nullable: false),
CoverUrl = table.Column<string>(type: "character varying(512)", maxLength: 512, nullable: false),
ReleaseStatus = table.Column<byte>(type: "smallint", nullable: false),
LibraryId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: true),
MangaConnectorName = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: false),
IgnoreChaptersBefore = table.Column<float>(type: "real", nullable: false),
DirectoryName = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: false),
CoverFileNameInCache = table.Column<string>(type: "character varying(512)", maxLength: 512, nullable: true),
WebsiteUrl = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
CoverUrl = table.Column<string>(type: "text", 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: true)
OriginalLanguage = table.Column<string>(type: "character varying(8)", maxLength: 8, nullable: false),
ReleaseStatus = table.Column<byte>(type: "smallint", nullable: false),
DirectoryName = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
LibraryLocalLibraryId = table.Column<string>(type: "character varying(64)", nullable: true),
IgnoreChapterBefore = table.Column<float>(type: "real", nullable: false),
MangaConnectorId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Mangas", x => x.MangaId);
table.ForeignKey(
name: "FK_Mangas_LocalLibraries_LibraryId",
column: x => x.LibraryId,
name: "FK_Mangas_LocalLibraries_LibraryLocalLibraryId",
column: x => x.LibraryLocalLibraryId,
principalTable: "LocalLibraries",
principalColumn: "LocalLibraryId",
onDelete: ReferentialAction.SetNull);
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_Mangas_MangaConnectors_MangaConnectorName",
column: x => x.MangaConnectorName,
name: "FK_Mangas_MangaConnectors_MangaConnectorId",
column: x => x.MangaConnectorId,
principalTable: "MangaConnectors",
principalColumn: "Name",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "AuthorToManga",
name: "AltTitles",
columns: table => new
{
AuthorIds = table.Column<string>(type: "character varying(64)", nullable: false),
MangaIds = table.Column<string>(type: "character varying(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),
Title = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
MangaId = table.Column<string>(type: "character varying(64)", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_AuthorToManga", x => new { x.AuthorIds, x.MangaIds });
table.PrimaryKey("PK_AltTitles", x => x.AltTitleId);
table.ForeignKey(
name: "FK_AuthorToManga_Authors_AuthorIds",
column: x => x.AuthorIds,
name: "FK_AltTitles_Mangas_MangaId",
column: x => x.MangaId,
principalTable: "Mangas",
principalColumn: "MangaId",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "AuthorManga",
columns: table => new
{
AuthorsAuthorId = table.Column<string>(type: "character varying(64)", nullable: false),
MangaId = table.Column<string>(type: "character varying(64)", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_AuthorManga", x => new { x.AuthorsAuthorId, x.MangaId });
table.ForeignKey(
name: "FK_AuthorManga_Authors_AuthorsAuthorId",
column: x => x.AuthorsAuthorId,
principalTable: "Authors",
principalColumn: "AuthorId",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_AuthorToManga_Mangas_MangaIds",
column: x => x.MangaIds,
name: "FK_AuthorManga_Mangas_MangaId",
column: x => x.MangaId,
principalTable: "Mangas",
principalColumn: "MangaId",
onDelete: ReferentialAction.Cascade);
@ -127,13 +195,13 @@ namespace API.Migrations.pgsql
columns: table => new
{
ChapterId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
ParentMangaId = table.Column<string>(type: "character varying(64)", nullable: false),
VolumeNumber = table.Column<int>(type: "integer", nullable: true),
ChapterNumber = table.Column<string>(type: "character varying(10)", maxLength: 10, nullable: false),
Url = table.Column<string>(type: "character varying(2048)", maxLength: 2048, nullable: false),
Title = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
FileName = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
Downloaded = table.Column<bool>(type: "boolean", nullable: false)
Downloaded = table.Column<bool>(type: "boolean", nullable: false),
ParentMangaId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false)
},
constraints: table =>
{
@ -147,19 +215,19 @@ namespace API.Migrations.pgsql
});
migrationBuilder.CreateTable(
name: "Link",
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)
MangaId = table.Column<string>(type: "character varying(64)", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Link", x => x.LinkId);
table.PrimaryKey("PK_Links", x => x.LinkId);
table.ForeignKey(
name: "FK_Link_Mangas_MangaId",
name: "FK_Links_Mangas_MangaId",
column: x => x.MangaId,
principalTable: "Mangas",
principalColumn: "MangaId",
@ -167,44 +235,24 @@ namespace API.Migrations.pgsql
});
migrationBuilder.CreateTable(
name: "MangaAltTitle",
name: "MangaMangaTag",
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)
MangaId = table.Column<string>(type: "character varying(64)", nullable: false),
MangaTagsTag = table.Column<string>(type: "character varying(64)", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_MangaAltTitle", x => x.AltTitleId);
table.PrimaryKey("PK_MangaMangaTag", x => new { x.MangaId, x.MangaTagsTag });
table.ForeignKey(
name: "FK_MangaAltTitle_Mangas_MangaId",
name: "FK_MangaMangaTag_Mangas_MangaId",
column: x => x.MangaId,
principalTable: "Mangas",
principalColumn: "MangaId",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "MangaTagToManga",
columns: table => new
{
MangaTagIds = table.Column<string>(type: "character varying(64)", nullable: false),
MangaIds = table.Column<string>(type: "character varying(64)", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_MangaTagToManga", x => new { x.MangaTagIds, x.MangaIds });
table.ForeignKey(
name: "FK_MangaTagToManga_Mangas_MangaIds",
column: x => x.MangaIds,
principalTable: "Mangas",
principalColumn: "MangaId",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_MangaTagToManga_Tags_MangaTagIds",
column: x => x.MangaTagIds,
name: "FK_MangaMangaTag_Tags_MangaTagsTag",
column: x => x.MangaTagsTag,
principalTable: "Tags",
principalColumn: "Tag",
onDelete: ReferentialAction.Cascade);
@ -216,6 +264,7 @@ namespace API.Migrations.pgsql
{
JobId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
ParentJobId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: true),
DependsOnJobsIds = table.Column<string[]>(type: "text[]", maxLength: 64, nullable: true),
JobType = table.Column<byte>(type: "smallint", nullable: false),
RecurrenceMs = table.Column<decimal>(type: "numeric(20,0)", nullable: false),
LastExecution = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
@ -226,11 +275,9 @@ namespace API.Migrations.pgsql
ChapterId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: true),
FromLocation = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
ToLocation = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
MoveMangaLibraryJob_MangaId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: true),
ToLibraryId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: true),
RetrieveChaptersJob_MangaId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: true),
Language = table.Column<string>(type: "character varying(8)", maxLength: 8, nullable: true),
UpdateFilesDownloadedJob_MangaId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: true)
UpdateFilesDownloadedJob_MangaId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: true),
UpdateMetadataJob_MangaId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: true)
},
constraints: table =>
{
@ -247,12 +294,6 @@ namespace API.Migrations.pgsql
principalTable: "Jobs",
principalColumn: "JobId",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_Jobs_LocalLibraries_ToLibraryId",
column: x => x.ToLibraryId,
principalTable: "LocalLibraries",
principalColumn: "LocalLibraryId",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_Jobs_Mangas_DownloadAvailableChaptersJob_MangaId",
column: x => x.DownloadAvailableChaptersJob_MangaId,
@ -265,12 +306,6 @@ namespace API.Migrations.pgsql
principalTable: "Mangas",
principalColumn: "MangaId",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_Jobs_Mangas_MoveMangaLibraryJob_MangaId",
column: x => x.MoveMangaLibraryJob_MangaId,
principalTable: "Mangas",
principalColumn: "MangaId",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_Jobs_Mangas_RetrieveChaptersJob_MangaId",
column: x => x.RetrieveChaptersJob_MangaId,
@ -283,6 +318,12 @@ namespace API.Migrations.pgsql
principalTable: "Mangas",
principalColumn: "MangaId",
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(
@ -310,9 +351,14 @@ namespace API.Migrations.pgsql
});
migrationBuilder.CreateIndex(
name: "IX_AuthorToManga_MangaIds",
table: "AuthorToManga",
column: "MangaIds");
name: "IX_AltTitles_MangaId",
table: "AltTitles",
column: "MangaId");
migrationBuilder.CreateIndex(
name: "IX_AuthorManga_MangaId",
table: "AuthorManga",
column: "MangaId");
migrationBuilder.CreateIndex(
name: "IX_Chapters_ParentMangaId",
@ -339,11 +385,6 @@ namespace API.Migrations.pgsql
table: "Jobs",
column: "MangaId");
migrationBuilder.CreateIndex(
name: "IX_Jobs_MoveMangaLibraryJob_MangaId",
table: "Jobs",
column: "MoveMangaLibraryJob_MangaId");
migrationBuilder.CreateIndex(
name: "IX_Jobs_ParentJobId",
table: "Jobs",
@ -354,59 +395,63 @@ namespace API.Migrations.pgsql
table: "Jobs",
column: "RetrieveChaptersJob_MangaId");
migrationBuilder.CreateIndex(
name: "IX_Jobs_ToLibraryId",
table: "Jobs",
column: "ToLibraryId");
migrationBuilder.CreateIndex(
name: "IX_Jobs_UpdateFilesDownloadedJob_MangaId",
table: "Jobs",
column: "UpdateFilesDownloadedJob_MangaId");
migrationBuilder.CreateIndex(
name: "IX_Link_MangaId",
table: "Link",
name: "IX_Jobs_UpdateMetadataJob_MangaId",
table: "Jobs",
column: "UpdateMetadataJob_MangaId");
migrationBuilder.CreateIndex(
name: "IX_Links_MangaId",
table: "Links",
column: "MangaId");
migrationBuilder.CreateIndex(
name: "IX_MangaAltTitle_MangaId",
table: "MangaAltTitle",
column: "MangaId");
name: "IX_MangaMangaTag_MangaTagsTag",
table: "MangaMangaTag",
column: "MangaTagsTag");
migrationBuilder.CreateIndex(
name: "IX_Mangas_LibraryId",
name: "IX_Mangas_LibraryLocalLibraryId",
table: "Mangas",
column: "LibraryId");
column: "LibraryLocalLibraryId");
migrationBuilder.CreateIndex(
name: "IX_Mangas_MangaConnectorName",
name: "IX_Mangas_MangaConnectorId",
table: "Mangas",
column: "MangaConnectorName");
migrationBuilder.CreateIndex(
name: "IX_MangaTagToManga_MangaIds",
table: "MangaTagToManga",
column: "MangaIds");
column: "MangaConnectorId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "AuthorToManga");
name: "AltTitles");
migrationBuilder.DropTable(
name: "AuthorManga");
migrationBuilder.DropTable(
name: "JobJob");
migrationBuilder.DropTable(
name: "Link");
name: "LibraryConnectors");
migrationBuilder.DropTable(
name: "MangaAltTitle");
name: "Links");
migrationBuilder.DropTable(
name: "MangaTagToManga");
name: "MangaMangaTag");
migrationBuilder.DropTable(
name: "NotificationConnectors");
migrationBuilder.DropTable(
name: "Notifications");
migrationBuilder.DropTable(
name: "Authors");

View File

@ -1,6 +1,7 @@
// <auto-generated />
using System;
using API.Schema.Contexts;
using System.Collections.Generic;
using API.Schema;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
@ -9,11 +10,11 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace API.Migrations.pgsql
namespace API.Migrations
{
[DbContext(typeof(PgsqlContext))]
[Migration("20250516121725_Manga-Year-Nullable")]
partial class MangaYearNullable
[Migration("20250316150158_dev-160325-2")]
partial class dev1603252
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
@ -23,6 +24,7 @@ namespace API.Migrations.pgsql
.HasAnnotation("ProductVersion", "9.0.3")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "hstore");
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("API.Schema.Author", b =>
@ -62,6 +64,7 @@ namespace API.Migrations.pgsql
b.Property<string>("ParentMangaId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("Title")
@ -89,6 +92,10 @@ namespace API.Migrations.pgsql
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.PrimitiveCollection<string[]>("DependsOnJobsIds")
.HasMaxLength(64)
.HasColumnType("text[]");
b.Property<bool>("Enabled")
.HasColumnType("boolean");
@ -119,6 +126,60 @@ namespace API.Migrations.pgsql
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")
@ -147,13 +208,11 @@ namespace API.Migrations.pgsql
.HasColumnType("character varying(64)");
b.Property<string>("CoverFileNameInCache")
.HasMaxLength(512)
.HasColumnType("character varying(512)");
.HasColumnType("text");
b.Property<string>("CoverUrl")
.IsRequired()
.HasMaxLength(512)
.HasColumnType("character varying(512)");
.HasColumnType("text");
b.Property<string>("Description")
.IsRequired()
@ -161,32 +220,32 @@ namespace API.Migrations.pgsql
b.Property<string>("DirectoryName")
.IsRequired()
.HasMaxLength(1024)
.HasColumnType("character varying(1024)");
.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<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")
.IsRequired()
.HasMaxLength(8)
.HasColumnType("character varying(8)");
@ -195,21 +254,47 @@ namespace API.Migrations.pgsql
b.Property<string>("WebsiteUrl")
.IsRequired()
.HasMaxLength(512)
.HasColumnType("character varying(512)");
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<long?>("Year")
b.Property<long>("Year")
.HasColumnType("bigint");
b.HasKey("MangaId");
b.HasIndex("LibraryId");
b.HasIndex("LibraryLocalLibraryId");
b.HasIndex("MangaConnectorName");
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")
@ -254,19 +339,76 @@ namespace API.Migrations.pgsql
b.ToTable("Tags");
});
modelBuilder.Entity("AuthorToManga", b =>
modelBuilder.Entity("API.Schema.Notification", b =>
{
b.Property<string>("AuthorIds")
b.Property<string>("NotificationId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("MangaIds")
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.HasKey("AuthorIds", "MangaIds");
b.Property<string>("Body")
.IsRequired()
.HasMaxLength(4096)
.HasColumnType("character varying(4096)");
b.HasIndex("MangaIds");
b.Property<Dictionary<string, string>>("Headers")
.IsRequired()
.HasColumnType("hstore");
b.ToTable("AuthorToManga");
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 =>
@ -284,19 +426,19 @@ namespace API.Migrations.pgsql
b.ToTable("JobJob");
});
modelBuilder.Entity("MangaTagToManga", b =>
modelBuilder.Entity("MangaMangaTag", b =>
{
b.Property<string>("MangaTagIds")
b.Property<string>("MangaId")
.HasColumnType("character varying(64)");
b.Property<string>("MangaIds")
b.Property<string>("MangaTagsTag")
.HasColumnType("character varying(64)");
b.HasKey("MangaTagIds", "MangaIds");
b.HasKey("MangaId", "MangaTagsTag");
b.HasIndex("MangaIds");
b.HasIndex("MangaTagsTag");
b.ToTable("MangaTagToManga");
b.ToTable("MangaMangaTag");
});
modelBuilder.Entity("API.Schema.Jobs.DownloadAvailableChaptersJob", b =>
@ -364,42 +506,10 @@ namespace API.Migrations.pgsql
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)
@ -416,7 +526,7 @@ namespace API.Migrations.pgsql
b.HasDiscriminator().HasValue((byte)5);
});
modelBuilder.Entity("API.Schema.Jobs.UpdateChaptersDownloadedJob", b =>
modelBuilder.Entity("API.Schema.Jobs.UpdateFilesDownloadedJob", b =>
{
b.HasBaseType("API.Schema.Jobs.Job");
@ -436,18 +546,52 @@ namespace API.Migrations.pgsql
b.HasDiscriminator().HasValue((byte)6);
});
modelBuilder.Entity("API.Schema.MangaConnectors.ComickIo", b =>
modelBuilder.Entity("API.Schema.Jobs.UpdateMetadataJob", b =>
{
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
b.HasBaseType("API.Schema.Jobs.Job");
b.HasDiscriminator().HasValue("ComickIo");
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.MangaConnectors.Global", b =>
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("Global");
b.HasDiscriminator().HasValue("AsuraToon");
});
modelBuilder.Entity("API.Schema.MangaConnectors.Bato", b =>
{
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
b.HasDiscriminator().HasValue("Bato");
});
modelBuilder.Entity("API.Schema.MangaConnectors.MangaDex", b =>
@ -457,10 +601,52 @@ namespace API.Migrations.pgsql
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("Chapters")
.WithMany()
.HasForeignKey("ParentMangaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
@ -478,98 +664,51 @@ namespace API.Migrations.pgsql
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("LibraryId")
.OnDelete(DeleteBehavior.SetNull);
.HasForeignKey("LibraryLocalLibraryId")
.OnDelete(DeleteBehavior.Restrict);
b.HasOne("API.Schema.MangaConnectors.MangaConnector", "MangaConnector")
.WithMany()
.HasForeignKey("MangaConnectorName")
.HasForeignKey("MangaConnectorId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.OwnsMany("API.Schema.Link", "Links", b1 =>
{
b1.Property<string>("LinkId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b1.Property<string>("LinkProvider")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b1.Property<string>("LinkUrl")
.IsRequired()
.HasMaxLength(2048)
.HasColumnType("character varying(2048)");
b1.Property<string>("MangaId")
.IsRequired()
.HasColumnType("character varying(64)");
b1.HasKey("LinkId");
b1.HasIndex("MangaId");
b1.ToTable("Link");
b1.WithOwner()
.HasForeignKey("MangaId");
});
b.OwnsMany("API.Schema.MangaAltTitle", "AltTitles", b1 =>
{
b1.Property<string>("MangaId")
.HasColumnType("character varying(64)");
b1.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property<int>("Id"));
b1.Property<string>("Language")
.IsRequired()
.HasMaxLength(8)
.HasColumnType("character varying(8)");
b1.Property<string>("Title")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b1.HasKey("MangaId", "Id");
b1.ToTable("MangaAltTitle");
b1.WithOwner()
.HasForeignKey("MangaId");
});
b.Navigation("AltTitles");
b.Navigation("Library");
b.Navigation("Links");
b.Navigation("MangaConnector");
});
modelBuilder.Entity("AuthorToManga", b =>
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("AuthorIds")
.HasForeignKey("AuthorsAuthorId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("API.Schema.Manga", null)
.WithMany()
.HasForeignKey("MangaIds")
.HasForeignKey("MangaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
@ -589,17 +728,17 @@ namespace API.Migrations.pgsql
.IsRequired();
});
modelBuilder.Entity("MangaTagToManga", b =>
modelBuilder.Entity("MangaMangaTag", b =>
{
b.HasOne("API.Schema.Manga", null)
.WithMany()
.HasForeignKey("MangaIds")
.HasForeignKey("MangaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("API.Schema.MangaTag", null)
.WithMany()
.HasForeignKey("MangaTagIds")
.HasForeignKey("MangaTagsTag")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
@ -637,25 +776,6 @@ namespace API.Migrations.pgsql
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")
@ -667,7 +787,18 @@ namespace API.Migrations.pgsql
b.Navigation("Manga");
});
modelBuilder.Entity("API.Schema.Jobs.UpdateChaptersDownloadedJob", b =>
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()
@ -680,7 +811,9 @@ namespace API.Migrations.pgsql
modelBuilder.Entity("API.Schema.Manga", b =>
{
b.Navigation("Chapters");
b.Navigation("AltTitles");
b.Navigation("Links");
});
#pragma warning restore 612, 618
}

View File

@ -0,0 +1,42 @@
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

@ -0,0 +1,827 @@
// <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

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

View File

@ -1,6 +1,7 @@
// <auto-generated />
using System;
using API.Schema.Contexts;
using System.Collections.Generic;
using API.Schema;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
@ -9,11 +10,11 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace API.Migrations.pgsql
namespace API.Migrations
{
[DbContext(typeof(PgsqlContext))]
[Migration("20250516180953_Split-UpdateChaptersDownloadedJob-Into-UpdateSingleChapterDownloadedJob")]
partial class SplitUpdateChaptersDownloadedJobIntoUpdateSingleChapterDownloadedJob
[Migration("20250401162026_dev-010425-2-Longer_Var_Chars")]
partial class dev0104252Longer_Var_Chars
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
@ -23,6 +24,7 @@ namespace API.Migrations.pgsql
.HasAnnotation("ProductVersion", "9.0.3")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "hstore");
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("API.Schema.Author", b =>
@ -62,6 +64,7 @@ namespace API.Migrations.pgsql
b.Property<string>("ParentMangaId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("Title")
@ -89,6 +92,10 @@ namespace API.Migrations.pgsql
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.PrimitiveCollection<string[]>("DependsOnJobsIds")
.HasMaxLength(64)
.HasColumnType("text[]");
b.Property<bool>("Enabled")
.HasColumnType("boolean");
@ -119,6 +126,60 @@ namespace API.Migrations.pgsql
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")
@ -147,13 +208,11 @@ namespace API.Migrations.pgsql
.HasColumnType("character varying(64)");
b.Property<string>("CoverFileNameInCache")
.HasMaxLength(512)
.HasColumnType("character varying(512)");
.HasColumnType("text");
b.Property<string>("CoverUrl")
.IsRequired()
.HasMaxLength(512)
.HasColumnType("character varying(512)");
.HasColumnType("text");
b.Property<string>("Description")
.IsRequired()
@ -169,17 +228,16 @@ namespace API.Migrations.pgsql
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<float>("IgnoreChaptersBefore")
b.Property<float>("IgnoreChapterBefore")
.HasColumnType("real");
b.Property<string>("LibraryId")
.HasMaxLength(64)
b.Property<string>("LibraryLocalLibraryId")
.HasColumnType("character varying(64)");
b.Property<string>("MangaConnectorName")
b.Property<string>("MangaConnectorId")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("character varying(32)");
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("Name")
.IsRequired()
@ -198,18 +256,44 @@ namespace API.Migrations.pgsql
.HasMaxLength(512)
.HasColumnType("character varying(512)");
b.Property<long?>("Year")
b.Property<long>("Year")
.HasColumnType("bigint");
b.HasKey("MangaId");
b.HasIndex("LibraryId");
b.HasIndex("LibraryLocalLibraryId");
b.HasIndex("MangaConnectorName");
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")
@ -254,19 +338,76 @@ namespace API.Migrations.pgsql
b.ToTable("Tags");
});
modelBuilder.Entity("AuthorToManga", b =>
modelBuilder.Entity("API.Schema.Notification", b =>
{
b.Property<string>("AuthorIds")
b.Property<string>("NotificationId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("MangaIds")
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.HasKey("AuthorIds", "MangaIds");
b.Property<string>("Body")
.IsRequired()
.HasMaxLength(4096)
.HasColumnType("character varying(4096)");
b.HasIndex("MangaIds");
b.Property<Dictionary<string, string>>("Headers")
.IsRequired()
.HasColumnType("hstore");
b.ToTable("AuthorToManga");
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 =>
@ -284,19 +425,19 @@ namespace API.Migrations.pgsql
b.ToTable("JobJob");
});
modelBuilder.Entity("MangaTagToManga", b =>
modelBuilder.Entity("MangaMangaTag", b =>
{
b.Property<string>("MangaTagIds")
b.Property<string>("MangaId")
.HasColumnType("character varying(64)");
b.Property<string>("MangaIds")
b.Property<string>("MangaTagsTag")
.HasColumnType("character varying(64)");
b.HasKey("MangaTagIds", "MangaIds");
b.HasKey("MangaId", "MangaTagsTag");
b.HasIndex("MangaIds");
b.HasIndex("MangaTagsTag");
b.ToTable("MangaTagToManga");
b.ToTable("MangaMangaTag");
});
modelBuilder.Entity("API.Schema.Jobs.DownloadAvailableChaptersJob", b =>
@ -364,42 +505,10 @@ namespace API.Migrations.pgsql
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)
@ -416,7 +525,7 @@ namespace API.Migrations.pgsql
b.HasDiscriminator().HasValue((byte)5);
});
modelBuilder.Entity("API.Schema.Jobs.UpdateChaptersDownloadedJob", b =>
modelBuilder.Entity("API.Schema.Jobs.UpdateFilesDownloadedJob", b =>
{
b.HasBaseType("API.Schema.Jobs.Job");
@ -430,37 +539,58 @@ namespace API.Migrations.pgsql
b.ToTable("Jobs", t =>
{
t.Property("MangaId")
.HasColumnName("UpdateChaptersDownloadedJob_MangaId");
.HasColumnName("UpdateFilesDownloadedJob_MangaId");
});
b.HasDiscriminator().HasValue((byte)6);
});
modelBuilder.Entity("API.Schema.Jobs.UpdateSingleChapterDownloadedJob", b =>
modelBuilder.Entity("API.Schema.Jobs.UpdateMetadataJob", b =>
{
b.HasBaseType("API.Schema.Jobs.Job");
b.Property<string>("ChapterId")
b.Property<string>("MangaId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.HasIndex("ChapterId");
b.HasIndex("MangaId");
b.ToTable("Jobs", t =>
{
t.Property("ChapterId")
.HasColumnName("UpdateSingleChapterDownloadedJob_ChapterId");
t.Property("MangaId")
.HasColumnName("UpdateMetadataJob_MangaId");
});
b.HasDiscriminator().HasValue((byte)8);
b.HasDiscriminator().HasValue((byte)2);
});
modelBuilder.Entity("API.Schema.MangaConnectors.ComickIo", b =>
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("ComickIo");
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 =>
@ -477,10 +607,52 @@ namespace API.Migrations.pgsql
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("Chapters")
.WithMany()
.HasForeignKey("ParentMangaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
@ -498,99 +670,51 @@ namespace API.Migrations.pgsql
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("LibraryId")
.OnDelete(DeleteBehavior.SetNull);
.HasForeignKey("LibraryLocalLibraryId")
.OnDelete(DeleteBehavior.Restrict);
b.HasOne("API.Schema.MangaConnectors.MangaConnector", "MangaConnector")
.WithMany()
.HasForeignKey("MangaConnectorName")
.HasForeignKey("MangaConnectorId")
.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 =>
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("AuthorIds")
.HasForeignKey("AuthorsAuthorId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("API.Schema.Manga", null)
.WithMany()
.HasForeignKey("MangaIds")
.HasForeignKey("MangaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
@ -610,17 +734,17 @@ namespace API.Migrations.pgsql
.IsRequired();
});
modelBuilder.Entity("MangaTagToManga", b =>
modelBuilder.Entity("MangaMangaTag", b =>
{
b.HasOne("API.Schema.Manga", null)
.WithMany()
.HasForeignKey("MangaIds")
.HasForeignKey("MangaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("API.Schema.MangaTag", null)
.WithMany()
.HasForeignKey("MangaTagIds")
.HasForeignKey("MangaTagsTag")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
@ -658,25 +782,6 @@ namespace API.Migrations.pgsql
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")
@ -688,7 +793,7 @@ namespace API.Migrations.pgsql
b.Navigation("Manga");
});
modelBuilder.Entity("API.Schema.Jobs.UpdateChaptersDownloadedJob", b =>
modelBuilder.Entity("API.Schema.Jobs.UpdateFilesDownloadedJob", b =>
{
b.HasOne("API.Schema.Manga", "Manga")
.WithMany()
@ -699,20 +804,22 @@ namespace API.Migrations.pgsql
b.Navigation("Manga");
});
modelBuilder.Entity("API.Schema.Jobs.UpdateSingleChapterDownloadedJob", b =>
modelBuilder.Entity("API.Schema.Jobs.UpdateMetadataJob", b =>
{
b.HasOne("API.Schema.Chapter", "Chapter")
b.HasOne("API.Schema.Manga", "Manga")
.WithMany()
.HasForeignKey("ChapterId")
.HasForeignKey("MangaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Chapter");
b.Navigation("Manga");
});
modelBuilder.Entity("API.Schema.Manga", b =>
{
b.Navigation("Chapters");
b.Navigation("AltTitles");
b.Navigation("Links");
});
#pragma warning restore 612, 618
}

View File

@ -0,0 +1,98 @@
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,6 +1,7 @@
// <auto-generated />
using System;
using API.Schema.Contexts;
using System.Collections.Generic;
using API.Schema;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
@ -9,11 +10,11 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace API.Migrations.pgsql
namespace API.Migrations
{
[DbContext(typeof(PgsqlContext))]
[Migration("20250516121442_AltTitle-Owned")]
partial class AltTitleOwned
[Migration("20250401234456_dev-010425-3-ParentJobOwnership")]
partial class dev0104253ParentJobOwnership
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
@ -23,6 +24,7 @@ namespace API.Migrations.pgsql
.HasAnnotation("ProductVersion", "9.0.3")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "hstore");
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("API.Schema.Author", b =>
@ -62,6 +64,7 @@ namespace API.Migrations.pgsql
b.Property<string>("ParentMangaId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("Title")
@ -89,6 +92,10 @@ namespace API.Migrations.pgsql
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.PrimitiveCollection<string[]>("DependsOnJobsIds")
.HasMaxLength(64)
.HasColumnType("text[]");
b.Property<bool>("Enabled")
.HasColumnType("boolean");
@ -119,6 +126,60 @@ namespace API.Migrations.pgsql
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")
@ -147,13 +208,11 @@ namespace API.Migrations.pgsql
.HasColumnType("character varying(64)");
b.Property<string>("CoverFileNameInCache")
.HasMaxLength(512)
.HasColumnType("character varying(512)");
.HasColumnType("text");
b.Property<string>("CoverUrl")
.IsRequired()
.HasMaxLength(512)
.HasColumnType("character varying(512)");
.HasColumnType("text");
b.Property<string>("Description")
.IsRequired()
@ -169,17 +228,16 @@ namespace API.Migrations.pgsql
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<float>("IgnoreChaptersBefore")
b.Property<float>("IgnoreChapterBefore")
.HasColumnType("real");
b.Property<string>("LibraryId")
.HasMaxLength(64)
b.Property<string>("LibraryLocalLibraryId")
.HasColumnType("character varying(64)");
b.Property<string>("MangaConnectorName")
b.Property<string>("MangaConnectorId")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("character varying(32)");
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("Name")
.IsRequired()
@ -203,13 +261,39 @@ namespace API.Migrations.pgsql
b.HasKey("MangaId");
b.HasIndex("LibraryId");
b.HasIndex("LibraryLocalLibraryId");
b.HasIndex("MangaConnectorName");
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")
@ -254,19 +338,76 @@ namespace API.Migrations.pgsql
b.ToTable("Tags");
});
modelBuilder.Entity("AuthorToManga", b =>
modelBuilder.Entity("API.Schema.Notification", b =>
{
b.Property<string>("AuthorIds")
b.Property<string>("NotificationId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("MangaIds")
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.HasKey("AuthorIds", "MangaIds");
b.Property<string>("Body")
.IsRequired()
.HasMaxLength(4096)
.HasColumnType("character varying(4096)");
b.HasIndex("MangaIds");
b.Property<Dictionary<string, string>>("Headers")
.IsRequired()
.HasColumnType("hstore");
b.ToTable("AuthorToManga");
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 =>
@ -284,19 +425,19 @@ namespace API.Migrations.pgsql
b.ToTable("JobJob");
});
modelBuilder.Entity("MangaTagToManga", b =>
modelBuilder.Entity("MangaMangaTag", b =>
{
b.Property<string>("MangaTagIds")
b.Property<string>("MangaId")
.HasColumnType("character varying(64)");
b.Property<string>("MangaIds")
b.Property<string>("MangaTagsTag")
.HasColumnType("character varying(64)");
b.HasKey("MangaTagIds", "MangaIds");
b.HasKey("MangaId", "MangaTagsTag");
b.HasIndex("MangaIds");
b.HasIndex("MangaTagsTag");
b.ToTable("MangaTagToManga");
b.ToTable("MangaMangaTag");
});
modelBuilder.Entity("API.Schema.Jobs.DownloadAvailableChaptersJob", b =>
@ -364,42 +505,10 @@ namespace API.Migrations.pgsql
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)
@ -416,7 +525,7 @@ namespace API.Migrations.pgsql
b.HasDiscriminator().HasValue((byte)5);
});
modelBuilder.Entity("API.Schema.Jobs.UpdateChaptersDownloadedJob", b =>
modelBuilder.Entity("API.Schema.Jobs.UpdateFilesDownloadedJob", b =>
{
b.HasBaseType("API.Schema.Jobs.Job");
@ -436,11 +545,52 @@ namespace API.Migrations.pgsql
b.HasDiscriminator().HasValue((byte)6);
});
modelBuilder.Entity("API.Schema.MangaConnectors.ComickIo", b =>
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("ComickIo");
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 =>
@ -457,10 +607,52 @@ namespace API.Migrations.pgsql
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("Chapters")
.WithMany()
.HasForeignKey("ParentMangaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
@ -478,98 +670,51 @@ namespace API.Migrations.pgsql
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("LibraryId")
.OnDelete(DeleteBehavior.SetNull);
.HasForeignKey("LibraryLocalLibraryId")
.OnDelete(DeleteBehavior.Restrict);
b.HasOne("API.Schema.MangaConnectors.MangaConnector", "MangaConnector")
.WithMany()
.HasForeignKey("MangaConnectorName")
.HasForeignKey("MangaConnectorId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.OwnsMany("API.Schema.Link", "Links", b1 =>
{
b1.Property<string>("LinkId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b1.Property<string>("LinkProvider")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b1.Property<string>("LinkUrl")
.IsRequired()
.HasMaxLength(2048)
.HasColumnType("character varying(2048)");
b1.Property<string>("MangaId")
.IsRequired()
.HasColumnType("character varying(64)");
b1.HasKey("LinkId");
b1.HasIndex("MangaId");
b1.ToTable("Link");
b1.WithOwner()
.HasForeignKey("MangaId");
});
b.OwnsMany("API.Schema.MangaAltTitle", "AltTitles", b1 =>
{
b1.Property<string>("MangaId")
.HasColumnType("character varying(64)");
b1.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property<int>("Id"));
b1.Property<string>("Language")
.IsRequired()
.HasMaxLength(8)
.HasColumnType("character varying(8)");
b1.Property<string>("Title")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b1.HasKey("MangaId", "Id");
b1.ToTable("MangaAltTitle");
b1.WithOwner()
.HasForeignKey("MangaId");
});
b.Navigation("AltTitles");
b.Navigation("Library");
b.Navigation("Links");
b.Navigation("MangaConnector");
});
modelBuilder.Entity("AuthorToManga", b =>
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("AuthorIds")
.HasForeignKey("AuthorsAuthorId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("API.Schema.Manga", null)
.WithMany()
.HasForeignKey("MangaIds")
.HasForeignKey("MangaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
@ -589,17 +734,17 @@ namespace API.Migrations.pgsql
.IsRequired();
});
modelBuilder.Entity("MangaTagToManga", b =>
modelBuilder.Entity("MangaMangaTag", b =>
{
b.HasOne("API.Schema.Manga", null)
.WithMany()
.HasForeignKey("MangaIds")
.HasForeignKey("MangaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("API.Schema.MangaTag", null)
.WithMany()
.HasForeignKey("MangaTagIds")
.HasForeignKey("MangaTagsTag")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
@ -637,25 +782,6 @@ namespace API.Migrations.pgsql
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")
@ -667,7 +793,18 @@ namespace API.Migrations.pgsql
b.Navigation("Manga");
});
modelBuilder.Entity("API.Schema.Jobs.UpdateChaptersDownloadedJob", b =>
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()
@ -680,7 +817,9 @@ namespace API.Migrations.pgsql
modelBuilder.Entity("API.Schema.Manga", b =>
{
b.Navigation("Chapters");
b.Navigation("AltTitles");
b.Navigation("Links");
});
#pragma warning restore 612, 618
}

View File

@ -0,0 +1,22 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace API.Migrations
{
/// <inheritdoc />
public partial class dev0104253ParentJobOwnership : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
}
}
}

View File

@ -1,6 +1,7 @@
// <auto-generated />
using System;
using API.Schema.Contexts;
using System.Collections.Generic;
using API.Schema;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
@ -9,11 +10,11 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace API.Migrations.pgsql
namespace API.Migrations
{
[DbContext(typeof(PgsqlContext))]
[Migration("20250516122242_AltTitle-Owned-WithId")]
partial class AltTitleOwnedWithId
[Migration("20250402001438_dev-010425-4")]
partial class dev0104254
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
@ -23,6 +24,7 @@ namespace API.Migrations.pgsql
.HasAnnotation("ProductVersion", "9.0.3")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "hstore");
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("API.Schema.Author", b =>
@ -62,6 +64,7 @@ namespace API.Migrations.pgsql
b.Property<string>("ParentMangaId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("Title")
@ -89,6 +92,10 @@ namespace API.Migrations.pgsql
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.PrimitiveCollection<string[]>("DependsOnJobsIds")
.HasMaxLength(64)
.HasColumnType("text[]");
b.Property<bool>("Enabled")
.HasColumnType("boolean");
@ -119,6 +126,60 @@ namespace API.Migrations.pgsql
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")
@ -147,13 +208,11 @@ namespace API.Migrations.pgsql
.HasColumnType("character varying(64)");
b.Property<string>("CoverFileNameInCache")
.HasMaxLength(512)
.HasColumnType("character varying(512)");
.HasColumnType("text");
b.Property<string>("CoverUrl")
.IsRequired()
.HasMaxLength(512)
.HasColumnType("character varying(512)");
.HasColumnType("text");
b.Property<string>("Description")
.IsRequired()
@ -169,17 +228,16 @@ namespace API.Migrations.pgsql
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<float>("IgnoreChaptersBefore")
b.Property<float>("IgnoreChapterBefore")
.HasColumnType("real");
b.Property<string>("LibraryId")
.HasMaxLength(64)
b.Property<string>("LibraryLocalLibraryId")
.HasColumnType("character varying(64)");
b.Property<string>("MangaConnectorName")
b.Property<string>("MangaConnectorId")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("character varying(32)");
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("Name")
.IsRequired()
@ -198,18 +256,44 @@ namespace API.Migrations.pgsql
.HasMaxLength(512)
.HasColumnType("character varying(512)");
b.Property<long?>("Year")
b.Property<long>("Year")
.HasColumnType("bigint");
b.HasKey("MangaId");
b.HasIndex("LibraryId");
b.HasIndex("LibraryLocalLibraryId");
b.HasIndex("MangaConnectorName");
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")
@ -254,19 +338,76 @@ namespace API.Migrations.pgsql
b.ToTable("Tags");
});
modelBuilder.Entity("AuthorToManga", b =>
modelBuilder.Entity("API.Schema.Notification", b =>
{
b.Property<string>("AuthorIds")
b.Property<string>("NotificationId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("MangaIds")
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.HasKey("AuthorIds", "MangaIds");
b.Property<string>("Body")
.IsRequired()
.HasMaxLength(4096)
.HasColumnType("character varying(4096)");
b.HasIndex("MangaIds");
b.Property<Dictionary<string, string>>("Headers")
.IsRequired()
.HasColumnType("hstore");
b.ToTable("AuthorToManga");
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 =>
@ -284,19 +425,19 @@ namespace API.Migrations.pgsql
b.ToTable("JobJob");
});
modelBuilder.Entity("MangaTagToManga", b =>
modelBuilder.Entity("MangaMangaTag", b =>
{
b.Property<string>("MangaTagIds")
b.Property<string>("MangaId")
.HasColumnType("character varying(64)");
b.Property<string>("MangaIds")
b.Property<string>("MangaTagsTag")
.HasColumnType("character varying(64)");
b.HasKey("MangaTagIds", "MangaIds");
b.HasKey("MangaId", "MangaTagsTag");
b.HasIndex("MangaIds");
b.HasIndex("MangaTagsTag");
b.ToTable("MangaTagToManga");
b.ToTable("MangaMangaTag");
});
modelBuilder.Entity("API.Schema.Jobs.DownloadAvailableChaptersJob", b =>
@ -308,8 +449,6 @@ namespace API.Migrations.pgsql
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.HasIndex("MangaId");
b.ToTable("Jobs", t =>
{
t.Property("MangaId")
@ -328,8 +467,6 @@ namespace API.Migrations.pgsql
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.HasIndex("MangaId");
b.HasDiscriminator().HasValue((byte)4);
});
@ -342,8 +479,6 @@ namespace API.Migrations.pgsql
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.HasIndex("ChapterId");
b.HasDiscriminator().HasValue((byte)0);
});
@ -364,49 +499,15 @@ namespace API.Migrations.pgsql
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")
@ -416,7 +517,25 @@ namespace API.Migrations.pgsql
b.HasDiscriminator().HasValue((byte)5);
});
modelBuilder.Entity("API.Schema.Jobs.UpdateChaptersDownloadedJob", b =>
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");
@ -430,17 +549,38 @@ namespace API.Migrations.pgsql
b.ToTable("Jobs", t =>
{
t.Property("MangaId")
.HasColumnName("UpdateFilesDownloadedJob_MangaId");
.HasColumnName("UpdateMetadataJob_MangaId");
});
b.HasDiscriminator().HasValue((byte)6);
b.HasDiscriminator().HasValue((byte)2);
});
modelBuilder.Entity("API.Schema.MangaConnectors.ComickIo", b =>
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("ComickIo");
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 =>
@ -457,10 +597,52 @@ namespace API.Migrations.pgsql
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("Chapters")
.WithMany()
.HasForeignKey("ParentMangaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
@ -478,99 +660,51 @@ namespace API.Migrations.pgsql
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("LibraryId")
.OnDelete(DeleteBehavior.SetNull);
.HasForeignKey("LibraryLocalLibraryId")
.OnDelete(DeleteBehavior.Restrict);
b.HasOne("API.Schema.MangaConnectors.MangaConnector", "MangaConnector")
.WithMany()
.HasForeignKey("MangaConnectorName")
.HasForeignKey("MangaConnectorId")
.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 =>
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("AuthorIds")
.HasForeignKey("AuthorsAuthorId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("API.Schema.Manga", null)
.WithMany()
.HasForeignKey("MangaIds")
.HasForeignKey("MangaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
@ -590,85 +724,22 @@ namespace API.Migrations.pgsql
.IsRequired();
});
modelBuilder.Entity("MangaTagToManga", b =>
modelBuilder.Entity("MangaMangaTag", b =>
{
b.HasOne("API.Schema.Manga", null)
.WithMany()
.HasForeignKey("MangaIds")
.HasForeignKey("MangaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("API.Schema.MangaTag", null)
.WithMany()
.HasForeignKey("MangaTagIds")
.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.MoveMangaLibraryJob", b =>
{
b.HasOne("API.Schema.Manga", "Manga")
.WithMany()
.HasForeignKey("MangaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("API.Schema.LocalLibrary", "ToLibrary")
.WithMany()
.HasForeignKey("ToLibraryId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Manga");
b.Navigation("ToLibrary");
});
modelBuilder.Entity("API.Schema.Jobs.RetrieveChaptersJob", b =>
{
b.HasOne("API.Schema.Manga", "Manga")
.WithMany()
.HasForeignKey("MangaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Manga");
});
modelBuilder.Entity("API.Schema.Jobs.UpdateChaptersDownloadedJob", b =>
modelBuilder.Entity("API.Schema.Jobs.UpdateMetadataJob", b =>
{
b.HasOne("API.Schema.Manga", "Manga")
.WithMany()
@ -681,7 +752,9 @@ namespace API.Migrations.pgsql
modelBuilder.Entity("API.Schema.Manga", b =>
{
b.Navigation("Chapters");
b.Navigation("AltTitles");
b.Navigation("Links");
});
#pragma warning restore 612, 618
}

View File

@ -0,0 +1,123 @@
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

@ -1,6 +1,7 @@
// <auto-generated />
using System;
using API.Schema.Contexts;
using System.Collections.Generic;
using API.Schema;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
@ -8,7 +9,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace API.Migrations.pgsql
namespace API.Migrations
{
[DbContext(typeof(PgsqlContext))]
partial class PgsqlContextModelSnapshot : ModelSnapshot
@ -20,6 +21,7 @@ namespace API.Migrations.pgsql
.HasAnnotation("ProductVersion", "9.0.3")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "hstore");
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("API.Schema.Author", b =>
@ -59,6 +61,7 @@ namespace API.Migrations.pgsql
b.Property<string>("ParentMangaId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("Title")
@ -86,6 +89,10 @@ namespace API.Migrations.pgsql
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.PrimitiveCollection<string[]>("DependsOnJobsIds")
.HasMaxLength(64)
.HasColumnType("text[]");
b.Property<bool>("Enabled")
.HasColumnType("boolean");
@ -116,6 +123,60 @@ namespace API.Migrations.pgsql
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")
@ -144,13 +205,11 @@ namespace API.Migrations.pgsql
.HasColumnType("character varying(64)");
b.Property<string>("CoverFileNameInCache")
.HasMaxLength(512)
.HasColumnType("character varying(512)");
.HasColumnType("text");
b.Property<string>("CoverUrl")
.IsRequired()
.HasMaxLength(512)
.HasColumnType("character varying(512)");
.HasColumnType("text");
b.Property<string>("Description")
.IsRequired()
@ -166,17 +225,16 @@ namespace API.Migrations.pgsql
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<float>("IgnoreChaptersBefore")
b.Property<float>("IgnoreChapterBefore")
.HasColumnType("real");
b.Property<string>("LibraryId")
.HasMaxLength(64)
b.Property<string>("LibraryLocalLibraryId")
.HasColumnType("character varying(64)");
b.Property<string>("MangaConnectorName")
b.Property<string>("MangaConnectorId")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("character varying(32)");
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("Name")
.IsRequired()
@ -195,18 +253,44 @@ namespace API.Migrations.pgsql
.HasMaxLength(512)
.HasColumnType("character varying(512)");
b.Property<long?>("Year")
b.Property<long>("Year")
.HasColumnType("bigint");
b.HasKey("MangaId");
b.HasIndex("LibraryId");
b.HasIndex("LibraryLocalLibraryId");
b.HasIndex("MangaConnectorName");
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")
@ -251,19 +335,76 @@ namespace API.Migrations.pgsql
b.ToTable("Tags");
});
modelBuilder.Entity("AuthorToManga", b =>
modelBuilder.Entity("API.Schema.Notification", b =>
{
b.Property<string>("AuthorIds")
b.Property<string>("NotificationId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("MangaIds")
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.HasKey("AuthorIds", "MangaIds");
b.Property<string>("Body")
.IsRequired()
.HasMaxLength(4096)
.HasColumnType("character varying(4096)");
b.HasIndex("MangaIds");
b.Property<Dictionary<string, string>>("Headers")
.IsRequired()
.HasColumnType("hstore");
b.ToTable("AuthorToManga");
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 =>
@ -281,19 +422,19 @@ namespace API.Migrations.pgsql
b.ToTable("JobJob");
});
modelBuilder.Entity("MangaTagToManga", b =>
modelBuilder.Entity("MangaMangaTag", b =>
{
b.Property<string>("MangaTagIds")
b.Property<string>("MangaId")
.HasColumnType("character varying(64)");
b.Property<string>("MangaIds")
b.Property<string>("MangaTagsTag")
.HasColumnType("character varying(64)");
b.HasKey("MangaTagIds", "MangaIds");
b.HasKey("MangaId", "MangaTagsTag");
b.HasIndex("MangaIds");
b.HasIndex("MangaTagsTag");
b.ToTable("MangaTagToManga");
b.ToTable("MangaMangaTag");
});
modelBuilder.Entity("API.Schema.Jobs.DownloadAvailableChaptersJob", b =>
@ -305,8 +446,6 @@ namespace API.Migrations.pgsql
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.HasIndex("MangaId");
b.ToTable("Jobs", t =>
{
t.Property("MangaId")
@ -325,8 +464,6 @@ namespace API.Migrations.pgsql
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.HasIndex("MangaId");
b.HasDiscriminator().HasValue((byte)4);
});
@ -339,8 +476,6 @@ namespace API.Migrations.pgsql
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.HasIndex("ChapterId");
b.HasDiscriminator().HasValue((byte)0);
});
@ -361,49 +496,15 @@ namespace API.Migrations.pgsql
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")
@ -413,7 +514,25 @@ namespace API.Migrations.pgsql
b.HasDiscriminator().HasValue((byte)5);
});
modelBuilder.Entity("API.Schema.Jobs.UpdateChaptersDownloadedJob", b =>
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");
@ -427,37 +546,38 @@ namespace API.Migrations.pgsql
b.ToTable("Jobs", t =>
{
t.Property("MangaId")
.HasColumnName("UpdateChaptersDownloadedJob_MangaId");
.HasColumnName("UpdateMetadataJob_MangaId");
});
b.HasDiscriminator().HasValue((byte)6);
b.HasDiscriminator().HasValue((byte)2);
});
modelBuilder.Entity("API.Schema.Jobs.UpdateSingleChapterDownloadedJob", b =>
modelBuilder.Entity("API.Schema.LibraryConnectors.Kavita", b =>
{
b.HasBaseType("API.Schema.Jobs.Job");
b.HasBaseType("API.Schema.LibraryConnectors.LibraryConnector");
b.Property<string>("ChapterId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.HasIndex("ChapterId");
b.ToTable("Jobs", t =>
{
t.Property("ChapterId")
.HasColumnName("UpdateSingleChapterDownloadedJob_ChapterId");
});
b.HasDiscriminator().HasValue((byte)8);
b.HasDiscriminator().HasValue((byte)1);
});
modelBuilder.Entity("API.Schema.MangaConnectors.ComickIo", b =>
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("ComickIo");
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 =>
@ -474,10 +594,52 @@ namespace API.Migrations.pgsql
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("Chapters")
.WithMany()
.HasForeignKey("ParentMangaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
@ -495,99 +657,51 @@ namespace API.Migrations.pgsql
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("LibraryId")
.OnDelete(DeleteBehavior.SetNull);
.HasForeignKey("LibraryLocalLibraryId")
.OnDelete(DeleteBehavior.Restrict);
b.HasOne("API.Schema.MangaConnectors.MangaConnector", "MangaConnector")
.WithMany()
.HasForeignKey("MangaConnectorName")
.HasForeignKey("MangaConnectorId")
.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 =>
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("AuthorIds")
.HasForeignKey("AuthorsAuthorId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("API.Schema.Manga", null)
.WithMany()
.HasForeignKey("MangaIds")
.HasForeignKey("MangaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
@ -607,22 +721,22 @@ namespace API.Migrations.pgsql
.IsRequired();
});
modelBuilder.Entity("MangaTagToManga", b =>
modelBuilder.Entity("MangaMangaTag", b =>
{
b.HasOne("API.Schema.Manga", null)
.WithMany()
.HasForeignKey("MangaIds")
.HasForeignKey("MangaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("API.Schema.MangaTag", null)
.WithMany()
.HasForeignKey("MangaTagIds")
.HasForeignKey("MangaTagsTag")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("API.Schema.Jobs.DownloadAvailableChaptersJob", b =>
modelBuilder.Entity("API.Schema.Jobs.UpdateMetadataJob", b =>
{
b.HasOne("API.Schema.Manga", "Manga")
.WithMany()
@ -633,83 +747,11 @@ namespace API.Migrations.pgsql
b.Navigation("Manga");
});
modelBuilder.Entity("API.Schema.Jobs.DownloadMangaCoverJob", b =>
{
b.HasOne("API.Schema.Manga", "Manga")
.WithMany()
.HasForeignKey("MangaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Manga");
});
modelBuilder.Entity("API.Schema.Jobs.DownloadSingleChapterJob", b =>
{
b.HasOne("API.Schema.Chapter", "Chapter")
.WithMany()
.HasForeignKey("ChapterId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Chapter");
});
modelBuilder.Entity("API.Schema.Jobs.MoveMangaLibraryJob", b =>
{
b.HasOne("API.Schema.Manga", "Manga")
.WithMany()
.HasForeignKey("MangaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("API.Schema.LocalLibrary", "ToLibrary")
.WithMany()
.HasForeignKey("ToLibraryId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Manga");
b.Navigation("ToLibrary");
});
modelBuilder.Entity("API.Schema.Jobs.RetrieveChaptersJob", b =>
{
b.HasOne("API.Schema.Manga", "Manga")
.WithMany()
.HasForeignKey("MangaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Manga");
});
modelBuilder.Entity("API.Schema.Jobs.UpdateChaptersDownloadedJob", b =>
{
b.HasOne("API.Schema.Manga", "Manga")
.WithMany()
.HasForeignKey("MangaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Manga");
});
modelBuilder.Entity("API.Schema.Jobs.UpdateSingleChapterDownloadedJob", b =>
{
b.HasOne("API.Schema.Chapter", "Chapter")
.WithMany()
.HasForeignKey("ChapterId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Chapter");
});
modelBuilder.Entity("API.Schema.Manga", b =>
{
b.Navigation("Chapters");
b.Navigation("AltTitles");
b.Navigation("Links");
});
#pragma warning restore 612, 618
}

View File

@ -1,71 +0,0 @@
// <auto-generated />
using API.Schema.Contexts;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace API.Migrations.library
{
[DbContext(typeof(LibraryContext))]
[Migration("20250515120732_Initial")]
partial class Initial
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "9.0.3")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("API.Schema.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.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);
});
#pragma warning restore 612, 618
}
}
}

View File

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

View File

@ -1,68 +0,0 @@
// <auto-generated />
using API.Schema.Contexts;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace API.Migrations.library
{
[DbContext(typeof(LibraryContext))]
partial class LibraryContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "9.0.3")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
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.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);
});
#pragma warning restore 612, 618
}
}
}

View File

@ -1,89 +0,0 @@
// <auto-generated />
using System;
using System.Collections.Generic;
using API.Schema.Contexts;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace API.Migrations.notifications
{
[DbContext(typeof(NotificationsContext))]
[Migration("20250515120746_Initial")]
partial class Initial
{
/// <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.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");
});
#pragma warning restore 612, 618
}
}
}

View File

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

View File

@ -1,86 +0,0 @@
// <auto-generated />
using System;
using System.Collections.Generic;
using API.Schema.Contexts;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace API.Migrations.notifications
{
[DbContext(typeof(NotificationsContext))]
partial class NotificationsContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "9.0.3")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "hstore");
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
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");
});
#pragma warning restore 612, 618
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,13 +1,12 @@
using System.Reflection;
using System.Text.Json.Serialization;
using API;
using API.Schema;
using API.Schema.Contexts;
using API.Schema.Jobs;
using API.Schema.MangaConnectors;
using Asp.Versioning;
using Asp.Versioning.Builder;
using Asp.Versioning.Conventions;
using log4net;
using Microsoft.EntityFrameworkCore;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
@ -55,17 +54,11 @@ builder.Services.AddSwaggerGen(opt =>
});
builder.Services.ConfigureOptions<NamedSwaggerGenOptions>();
string ConnectionString = $"Host={Environment.GetEnvironmentVariable("POSTGRES_HOST") ?? "localhost:5432"}; " +
$"Database={Environment.GetEnvironmentVariable("POSTGRES_DB") ?? "postgres"}; " +
$"Username={Environment.GetEnvironmentVariable("POSTGRES_USER") ?? "postgres"}; " +
$"Password={Environment.GetEnvironmentVariable("POSTGRES_PASSWORD") ?? "postgres"}";
builder.Services.AddDbContext<PgsqlContext>(options =>
options.UseNpgsql(ConnectionString));
builder.Services.AddDbContext<NotificationsContext>(options =>
options.UseNpgsql(ConnectionString));
builder.Services.AddDbContext<LibraryContext>(options =>
options.UseNpgsql(ConnectionString));
options.UseNpgsql($"Host={Environment.GetEnvironmentVariable("POSTGRES_HOST")??"localhost:5432"}; " +
$"Database={Environment.GetEnvironmentVariable("POSTGRES_DB")??"postgres"}; " +
$"Username={Environment.GetEnvironmentVariable("POSTGRES_USER")??"postgres"}; " +
$"Password={Environment.GetEnvironmentVariable("POSTGRES_PASSWORD")??"postgres"}"));
builder.Services.AddControllers(options =>
{
@ -76,7 +69,6 @@ builder.Services.AddControllers().AddNewtonsoftJson(opts =>
opts.SerializerSettings.Converters.Add(new StringEnumConverter());
opts.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore;
});
builder.Services.AddScoped<ILog>(opts => LogManager.GetLogger("API"));
builder.WebHost.UseUrls("http://*:6531");
@ -105,40 +97,38 @@ app.UseHttpsRedirection();
app.UseMiddleware<RequestTimeMiddleware>();
using (IServiceScope scope = app.Services.CreateScope())
using (var scope = app.Services.CreateScope())
{
PgsqlContext context = scope.ServiceProvider.GetRequiredService<PgsqlContext>();
context.Database.Migrate();
MangaConnector[] connectors =
[
new MangaDex(),
new ComickIo(),
new Global(scope.ServiceProvider.GetService<PgsqlContext>()!)
];
MangaConnector[] newConnectors = connectors.Where(c => !context.MangaConnectors.Contains(c)).ToArray();
context.MangaConnectors.AddRange(newConnectors);
if (!context.LocalLibraries.Any())
context.LocalLibraries.Add(new LocalLibrary(TrangaSettings.downloadLocation, "Default Library"));
context.Jobs.AddRange(context.Jobs.Where(j => j.JobType == JobType.DownloadAvailableChaptersJob)
.Include(downloadAvailableChaptersJob => ((DownloadAvailableChaptersJob)downloadAvailableChaptersJob).Manga)
.ToList()
.Select(dacj => new UpdateChaptersDownloadedJob(((DownloadAvailableChaptersJob)dacj).Manga, 0)));
context.Jobs.RemoveRange(context.Jobs.Where(j => j.state == JobState.Completed && j.RecurrenceMs < 1));
foreach (Job job in context.Jobs.Where(j => j.state == JobState.Running))
{
job.state = JobState.FirstExecution;
job.LastExecution = DateTime.UnixEpoch;
}
context.SaveChanges();
var db = scope.ServiceProvider.GetRequiredService<PgsqlContext>();
db.Database.Migrate();
}
using (IServiceScope scope = app.Services.CreateScope())
using (var scope = app.Services.CreateScope())
{
NotificationsContext context = scope.ServiceProvider.GetRequiredService<NotificationsContext>();
context.Database.Migrate();
PgsqlContext context = scope.ServiceProvider.GetService<PgsqlContext>()!;
MangaConnector[] connectors =
[
new AsuraToon(),
new Bato(),
new MangaDex(),
new MangaHere(),
new MangaKatana(),
new Mangaworld(),
new ManhuaPlus(),
new Weebcentral(),
//new Manganato(),
new Global(scope.ServiceProvider.GetService<PgsqlContext>()!)
];
MangaConnector[] newConnectors = connectors.Where(c => !context.MangaConnectors.Contains(c)).ToArray();
context.MangaConnectors.AddRange(newConnectors);
context.Jobs.AddRange(context.Mangas.AsEnumerable().Select(m => new UpdateFilesDownloadedJob(0, m.MangaId)));
context.Jobs.RemoveRange(context.Jobs.Where(j => j.state == JobState.Completed && j.RecurrenceMs < 1));
if (!context.LocalLibraries.Any())
context.LocalLibraries.Add(new LocalLibrary(TrangaSettings.downloadLocation, "Default Library"));
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));
@ -150,7 +140,7 @@ using (IServiceScope scope = app.Services.CreateScope())
TrangaSettings.Load();
Tranga.StartLogger();
Tranga.JobStarterThread.Start(app.Services);
//Tranga.NotificationSenderThread.Start(app.Services); //TODO RE-ENABLE
Tranga.NotificationSenderThread.Start(app.Services);
app.UseCors("AllowAll");

View File

@ -12,9 +12,4 @@ public class Author(string authorName)
[StringLength(128)]
[Required]
public string AuthorName { get; init; } = authorName;
public override string ToString()
{
return $"{AuthorId} {AuthorName}";
}
}

View File

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

View File

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

View File

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

View File

@ -1,204 +0,0 @@
using API.Schema.Jobs;
using API.Schema.MangaConnectors;
using log4net;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Diagnostics;
namespace API.Schema.Contexts;
public class PgsqlContext(DbContextOptions<PgsqlContext> options) : DbContext(options)
{
public DbSet<Job> Jobs { get; set; }
public DbSet<MangaConnector> MangaConnectors { get; set; }
public DbSet<Manga> Mangas { get; set; }
public DbSet<LocalLibrary> LocalLibraries { get; set; }
public DbSet<Chapter> Chapters { get; set; }
public DbSet<Author> Authors { get; set; }
public DbSet<MangaTag> Tags { get; set; }
private ILog Log => LogManager.GetLogger(GetType());
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
base.OnConfiguring(optionsBuilder);
optionsBuilder.EnableSensitiveDataLogging();
optionsBuilder.LogTo(s =>
{
Log.Debug(s);
}, [DbLoggerCategory.Query.Name], LogLevel.Trace, DbContextLoggerOptions.Level | DbContextLoggerOptions.Category);
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
//Job Types
modelBuilder.Entity<Job>()
.HasDiscriminator(j => j.JobType)
.HasValue<MoveFileOrFolderJob>(JobType.MoveFileOrFolderJob)
.HasValue<MoveMangaLibraryJob>(JobType.MoveMangaLibraryJob)
.HasValue<DownloadAvailableChaptersJob>(JobType.DownloadAvailableChaptersJob)
.HasValue<DownloadSingleChapterJob>(JobType.DownloadSingleChapterJob)
.HasValue<DownloadMangaCoverJob>(JobType.DownloadMangaCoverJob)
.HasValue<RetrieveChaptersJob>(JobType.RetrieveChaptersJob)
.HasValue<UpdateChaptersDownloadedJob>(JobType.UpdateChaptersDownloadedJob)
.HasValue<UpdateSingleChapterDownloadedJob>(JobType.UpdateSingleChapterDownloadedJob);
//Job specification
modelBuilder.Entity<DownloadAvailableChaptersJob>()
.HasOne<Manga>(j => j.Manga)
.WithMany()
.HasForeignKey(j => j.MangaId)
.IsRequired()
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<DownloadAvailableChaptersJob>()
.Navigation(j => j.Manga)
.EnableLazyLoading();
modelBuilder.Entity<DownloadMangaCoverJob>()
.HasOne<Manga>(j => j.Manga)
.WithMany()
.HasForeignKey(j => j.MangaId)
.IsRequired()
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<DownloadMangaCoverJob>()
.Navigation(j => j.Manga)
.EnableLazyLoading();
modelBuilder.Entity<DownloadSingleChapterJob>()
.HasOne<Chapter>(j => j.Chapter)
.WithMany()
.HasForeignKey(j => j.ChapterId)
.IsRequired()
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<DownloadSingleChapterJob>()
.Navigation(j => j.Chapter)
.EnableLazyLoading();
modelBuilder.Entity<MoveMangaLibraryJob>()
.HasOne<Manga>(j => j.Manga)
.WithMany()
.HasForeignKey(j => j.MangaId)
.IsRequired()
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<MoveMangaLibraryJob>()
.Navigation(j => j.Manga)
.EnableLazyLoading();
modelBuilder.Entity<MoveMangaLibraryJob>()
.HasOne<LocalLibrary>(j => j.ToLibrary)
.WithMany()
.HasForeignKey(j => j.ToLibraryId)
.IsRequired()
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<MoveMangaLibraryJob>()
.Navigation(j => j.ToLibrary)
.EnableLazyLoading();
modelBuilder.Entity<RetrieveChaptersJob>()
.HasOne<Manga>(j => j.Manga)
.WithMany()
.HasForeignKey(j => j.MangaId)
.IsRequired()
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<RetrieveChaptersJob>()
.Navigation(j => j.Manga)
.EnableLazyLoading();
modelBuilder.Entity<UpdateChaptersDownloadedJob>()
.HasOne<Manga>(j => j.Manga)
.WithMany()
.HasForeignKey(j => j.MangaId)
.IsRequired()
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<UpdateChaptersDownloadedJob>()
.Navigation(j => j.Manga)
.EnableLazyLoading();
//Job has possible ParentJob
modelBuilder.Entity<Job>()
.HasOne<Job>(childJob => childJob.ParentJob)
.WithMany()
.HasForeignKey(childjob => childjob.ParentJobId)
.OnDelete(DeleteBehavior.Cascade);
//Job might be dependent on other Jobs
modelBuilder.Entity<Job>()
.HasMany<Job>(root => root.DependsOnJobs)
.WithMany();
modelBuilder.Entity<Job>()
.Navigation(j => j.DependsOnJobs)
.AutoInclude(false)
.EnableLazyLoading();
//MangaConnector Types
modelBuilder.Entity<MangaConnector>()
.HasDiscriminator(c => c.Name)
.HasValue<Global>("Global")
.HasValue<MangaDex>("MangaDex")
.HasValue<ComickIo>("ComickIo");
//MangaConnector is responsible for many Manga
modelBuilder.Entity<MangaConnector>()
.HasMany<Manga>()
.WithOne(m => m.MangaConnector)
.HasForeignKey(m => m.MangaConnectorName)
.IsRequired()
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<Manga>()
.Navigation(m => m.MangaConnector)
.AutoInclude();
//Manga has many Chapters
modelBuilder.Entity<Manga>()
.HasMany<Chapter>(m => m.Chapters)
.WithOne(c => c.ParentManga)
.HasForeignKey(c => c.ParentMangaId)
.IsRequired()
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<Chapter>()
.Navigation(c => c.ParentManga)
.AutoInclude();
modelBuilder.Entity<Manga>()
.Navigation(m => m.Chapters)
.AutoInclude(false)
.EnableLazyLoading();
//Manga owns MangaAltTitles
modelBuilder.Entity<Manga>()
.OwnsMany<MangaAltTitle>(m => m.AltTitles)
.WithOwner();
modelBuilder.Entity<Manga>()
.Navigation(m => m.AltTitles)
.AutoInclude();
//Manga owns Links
modelBuilder.Entity<Manga>()
.OwnsMany<Link>(m => m.Links)
.WithOwner();
modelBuilder.Entity<Manga>()
.Navigation(m => m.Links)
.AutoInclude();
//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")
);
modelBuilder.Entity<Manga>()
.Navigation(m => m.MangaTags)
.AutoInclude();
//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")
);
modelBuilder.Entity<Manga>()
.Navigation(m => m.Authors)
.AutoInclude();
//LocalLibrary has many Mangas
modelBuilder.Entity<LocalLibrary>()
.HasMany<Manga>()
.WithOne(m => m.Library)
.HasForeignKey(m => m.LibraryId)
.OnDelete(DeleteBehavior.SetNull);
modelBuilder.Entity<Manga>()
.Navigation(m => m.Library)
.AutoInclude();
}
}

View File

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

View File

@ -1,51 +1,26 @@
using System.ComponentModel.DataAnnotations;
using API.Schema.Contexts;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Newtonsoft.Json;
namespace API.Schema.Jobs;
public class DownloadMangaCoverJob : Job
public class DownloadMangaCoverJob(string mangaId, string? parentJobId = null, ICollection<string>? dependsOnJobsIds = null)
: Job(TokenGen.CreateToken(typeof(DownloadMangaCoverJob)), JobType.DownloadMangaCoverJob, 0, parentJobId, dependsOnJobsIds)
{
[StringLength(64)] [Required] public string MangaId { get; init; }
private Manga _manga = null!;
[JsonIgnore]
public Manga Manga
{
get => LazyLoader.Load(this, ref _manga);
init => _manga = value;
}
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>
internal DownloadMangaCoverJob(ILazyLoader lazyLoader, string mangaId, string? parentJobId)
: base(lazyLoader, TokenGen.CreateToken(typeof(DownloadMangaCoverJob)), JobType.DownloadMangaCoverJob, 0, parentJobId)
{
this.MangaId = mangaId;
}
[StringLength(64)]
[Required]
public string MangaId { get; init; } = mangaId;
protected override IEnumerable<Job> RunInternal(PgsqlContext context)
{
try
Manga? manga = context.Mangas.Find(this.MangaId);
if (manga is null)
{
Manga.CoverFileNameInCache = Manga.MangaConnector.SaveCoverImageToCache(Manga);
context.SaveChanges();
}
catch (DbUpdateException e)
{
Log.Error(e);
Log.Error($"Manga {this.MangaId} not found.");
return [];
}
manga.CoverFileNameInCache = manga.SaveCoverImageToCache();
context.SaveChanges();
Log.Info($"Saved cover for Manga {this.MangaId} to cache at {manga.CoverFileNameInCache}.");
return [];
}
}

View File

@ -2,10 +2,7 @@
using System.IO.Compression;
using System.Runtime.InteropServices;
using API.MangaDownloadClients;
using API.Schema.Contexts;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Newtonsoft.Json;
using API.Schema.MangaConnectors;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Formats.Jpeg;
using SixLabors.ImageSharp.Processing;
@ -14,44 +11,45 @@ using static System.IO.UnixFileMode;
namespace API.Schema.Jobs;
public class DownloadSingleChapterJob : Job
public class DownloadSingleChapterJob(string chapterId, string? parentJobId = null, ICollection<string>? dependsOnJobsIds = null)
: Job(TokenGen.CreateToken(typeof(DownloadSingleChapterJob)), JobType.DownloadSingleChapterJob, 0, parentJobId, dependsOnJobsIds)
{
[StringLength(64)] [Required] public string ChapterId { get; init; }
private Chapter _chapter = null!;
[JsonIgnore]
public Chapter Chapter
{
get => LazyLoader.Load(this, ref _chapter);
init => _chapter = value;
}
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>
internal DownloadSingleChapterJob(ILazyLoader lazyLoader, string chapterId, string? parentJobId)
: base(lazyLoader, TokenGen.CreateToken(typeof(DownloadSingleChapterJob)), JobType.DownloadSingleChapterJob, 0, parentJobId)
{
this.ChapterId = chapterId;
}
[StringLength(64)]
[Required]
public string ChapterId { get; init; } = chapterId;
protected override IEnumerable<Job> RunInternal(PgsqlContext context)
{
string[] imageUrls = Chapter.ParentManga.MangaConnector.GetChapterImageUrls(Chapter);
Chapter? chapter = context.Chapters.Find(ChapterId);
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)
{
Log.Info($"No imageUrls for chapter {ChapterId}");
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
string directoryPath = Path.GetDirectoryName(saveArchiveFilePath)!;
@ -90,10 +88,10 @@ public class DownloadSingleChapterJob : Job
}
}
CopyCoverFromCacheToDownloadLocation(Chapter.ParentManga);
CopyCoverFromCacheToDownloadLocation(manga);
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}");
//ZIP-it and ship-it
@ -102,19 +100,10 @@ public class DownloadSingleChapterJob : Job
File.SetUnixFileMode(saveArchiveFilePath, UserRead | UserWrite | UserExecute | GroupRead | GroupWrite | GroupExecute | OtherRead | OtherExecute);
Directory.Delete(tempFolder, true); //Cleanup
Chapter.Downloaded = true;
chapter.Downloaded = true;
context.SaveChanges();
if (context.Jobs.ToList().Any(j =>
{
if (j.JobType != JobType.UpdateChaptersDownloadedJob)
return false;
UpdateChaptersDownloadedJob job = (UpdateChaptersDownloadedJob)j;
return job.MangaId == this.Chapter.ParentMangaId;
}))
return [];
return [new UpdateChaptersDownloadedJob(Chapter.ParentManga, 0, this.ParentJob)];
return [new UpdateFilesDownloadedJob(0, manga.MangaId, this.JobId)];
}
private void ProcessImage(string imagePath)
@ -149,7 +138,7 @@ public class DownloadSingleChapterJob : Job
}
Log.Info($"Copying cover to {publicationFolder}");
string? fileInCache = manga.CoverFileNameInCache ?? manga.MangaConnector.SaveCoverImageToCache(manga);
string? fileInCache = manga.CoverFileNameInCache ?? manga.SaveCoverImageToCache();
if (fileInCache is null)
{
Log.Error($"File {fileInCache} does not exist");

View File

@ -1,9 +1,7 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using API.Schema.Contexts;
using log4net;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Newtonsoft.Json;
namespace API.Schema.Jobs;
@ -14,100 +12,75 @@ public abstract class Job
[StringLength(64)]
[Required]
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; }
[StringLength(64)] public string? ParentJobId { get; private set; }
[JsonIgnore] public Job? ParentJob { get; internal set; }
private ICollection<Job> _dependsOnJobs = null!;
[JsonIgnore] public ICollection<Job> DependsOnJobs
[Required]
public JobType JobType { get; init; }
[Required]
public ulong RecurrenceMs { get; set; }
[Required]
public DateTime LastExecution { get; internal set; } = DateTime.UnixEpoch;
[NotMapped]
[Required]
public DateTime NextExecution => LastExecution.AddMilliseconds(RecurrenceMs);
[Required]
public JobState state { get; internal set; } = JobState.Waiting;
[Required]
public bool Enabled { get; internal set; } = true;
[NotMapped]
[JsonIgnore]
protected ILog Log { get; init; }
public Job(string jobId, JobType jobType, ulong recurrenceMs, Job? parentJob = null, ICollection<Job>? dependsOnJobs = null)
: this(jobId, jobType, recurrenceMs, parentJob?.JobId, dependsOnJobs?.Select(j => j.JobId).ToList())
{
get => LazyLoader.Load(this, ref _dependsOnJobs);
init => _dependsOnJobs = value;
}
[Required] public JobType JobType { get; init; }
[Required] public ulong RecurrenceMs { get; set; }
[Required] public DateTime LastExecution { get; internal set; } = DateTime.UnixEpoch;
[NotMapped] [Required] public DateTime NextExecution => LastExecution.AddMilliseconds(RecurrenceMs);
[Required] public JobState state { get; internal set; } = JobState.FirstExecution;
[Required] public bool Enabled { get; internal set; } = true;
[JsonIgnore] [NotMapped] internal bool IsCompleted => state is >= (JobState)128 and < (JobState)192;
[JsonIgnore] [NotMapped] internal bool DependenciesFulfilled => DependsOnJobs.All(j => j.IsCompleted);
[NotMapped] [JsonIgnore] protected ILog Log { get; init; }
[NotMapped] [JsonIgnore] protected ILazyLoader LazyLoader { 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.DependsOnJobs = dependsOnJobs ?? [];
this.Log = LogManager.GetLogger(this.GetType());
this.DependsOnJobs = dependsOnJobs;
}
/// <summary>
/// EF ONLY!!!
/// </summary>
protected internal Job(ILazyLoader lazyLoader, string jobId, JobType jobType, ulong recurrenceMs, string? parentJobId)
public Job(string jobId, JobType jobType, ulong recurrenceMs, string? parentJobId = null, ICollection<string>? dependsOnJobsIds = null)
{
this.LazyLoader = lazyLoader;
this.JobId = jobId;
this.JobType = jobType;
this.RecurrenceMs = recurrenceMs;
this.ParentJobId = parentJobId;
this.DependsOnJobs = [];
this.Log = LogManager.GetLogger(this.GetType());
Log = LogManager.GetLogger(GetType());
JobId = jobId;
ParentJobId = parentJobId;
DependsOnJobsIds = dependsOnJobsIds;
JobType = jobType;
RecurrenceMs = recurrenceMs;
}
public IEnumerable<Job> Run(IServiceProvider serviceProvider)
{
Log.Info($"Running job {JobId}");
DateTime jobStart = DateTime.UtcNow;
Job[]? ret = null;
Log.Debug($"Running job {JobId}");
using IServiceScope scope = serviceProvider.CreateScope();
PgsqlContext context = scope.ServiceProvider.GetRequiredService<PgsqlContext>();
try
{
context.Attach(this);
this.state = JobState.Running;
context.SaveChanges();
ret = RunInternal(context).ToArray();
Job[] newJobs = RunInternal(context).ToArray();
this.state = JobState.Completed;
context.Jobs.AddRange(ret);
Log.Info($"Job {JobId} completed. Generated {ret.Length} new jobs.");
context.Jobs.AddRange(newJobs);
context.SaveChanges();
Log.Info($"Job {JobId} completed. Generated {newJobs.Length} new jobs.");
return newJobs;
}
catch (Exception e)
catch (DbUpdateException e)
{
if (e is not DbUpdateException)
{
this.state = JobState.Failed;
Log.Error($"Failed to run job {JobId}", e);
context.SaveChanges();
}
else
{
Log.Error($"Failed to update Database {JobId}", e);
}
this.state = JobState.Failed;
Log.Error($"Failed to run job {JobId}", e);
return [];
}
Log.Info($"Finished Job {JobId}! (took {DateTime.UtcNow.Subtract(jobStart).TotalMilliseconds}ms)");
return ret ?? [];
}
protected abstract IEnumerable<Job> RunInternal(PgsqlContext context);
public override string ToString()
{
return $"{JobId}";
}
}

View File

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

View File

@ -9,7 +9,6 @@ public enum JobType : byte
MoveFileOrFolderJob = 3,
DownloadMangaCoverJob = 4,
RetrieveChaptersJob = 5,
UpdateChaptersDownloadedJob = 6,
MoveMangaLibraryJob = 7,
UpdateSingleChapterDownloadedJob = 8,
UpdateFilesDownloadedJob = 6,
MoveMangaLibraryJob = 7
}

View File

@ -1,34 +1,16 @@
using System.ComponentModel.DataAnnotations;
using API.Schema.Contexts;
using Microsoft.EntityFrameworkCore.Infrastructure;
namespace API.Schema.Jobs;
public class MoveFileOrFolderJob : Job
public class MoveFileOrFolderJob(string fromLocation, string toLocation, string? parentJobId = null, ICollection<string>? dependsOnJobsIds = null)
: Job(TokenGen.CreateToken(typeof(MoveFileOrFolderJob)), JobType.MoveFileOrFolderJob, 0, parentJobId, dependsOnJobsIds)
{
[StringLength(256)]
[Required]
public string FromLocation { get; init; }
public string FromLocation { get; init; } = fromLocation;
[StringLength(256)]
[Required]
public string ToLocation { get; init; }
public MoveFileOrFolderJob(string fromLocation, string toLocation, Job? parentJob = null, ICollection<Job>? dependsOnJobs = null)
: base(TokenGen.CreateToken(typeof(MoveFileOrFolderJob)), JobType.MoveFileOrFolderJob, 0, parentJob, dependsOnJobs)
{
this.FromLocation = fromLocation;
this.ToLocation = toLocation;
}
/// <summary>
/// EF ONLY!!!
/// </summary>
internal MoveFileOrFolderJob(ILazyLoader lazyLoader, string jobId, string fromLocation, string toLocation, string? parentJobId)
: base(lazyLoader, jobId, JobType.MoveFileOrFolderJob, 0, parentJobId)
{
this.FromLocation = fromLocation;
this.ToLocation = toLocation;
}
public string ToLocation { get; init; } = toLocation;
protected override IEnumerable<Job> RunInternal(PgsqlContext context)
{

View File

@ -1,49 +1,35 @@
using System.ComponentModel.DataAnnotations;
using API.Schema.Contexts;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Newtonsoft.Json;
namespace API.Schema.Jobs;
public class MoveMangaLibraryJob : Job
public class MoveMangaLibraryJob(string mangaId, string toLibraryId, string? parentJobId = null, ICollection<string>? dependsOnJobsIds = null)
: Job(TokenGen.CreateToken(typeof(MoveMangaLibraryJob)), JobType.MoveMangaLibraryJob, 0, parentJobId, dependsOnJobsIds)
{
[StringLength(64)] [Required] public string MangaId { get; init; }
private Manga _manga = null!;
[JsonIgnore]
public Manga Manga
{
get => LazyLoader.Load(this, ref _manga);
init => _manga = value;
}
[StringLength(64)] [Required] public string ToLibraryId { get; init; }
public LocalLibrary ToLibrary { get; init; } = null!;
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>
internal MoveMangaLibraryJob(ILazyLoader lazyLoader, string mangaId, string toLibraryId, string? parentJobId)
: base(lazyLoader, TokenGen.CreateToken(typeof(MoveMangaLibraryJob)), JobType.MoveMangaLibraryJob, 0, parentJobId)
{
this.MangaId = mangaId;
this.ToLibraryId = toLibraryId;
}
[StringLength(64)]
[Required]
public string MangaId { get; init; } = mangaId;
[StringLength(64)]
[Required]
public string ToLibraryId { get; init; } = toLibraryId;
protected override IEnumerable<Job> RunInternal(PgsqlContext context)
{
Dictionary<Chapter, string> oldPath = Manga.Chapters.ToDictionary(c => c, c => c.FullArchiveFilePath);
Manga.Library = ToLibrary;
Manga? manga = context.Mangas.Find(MangaId);
if (manga is null)
{
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
{
context.SaveChanges();
@ -54,6 +40,6 @@ public class MoveMangaLibraryJob : Job
return [];
}
return Manga.Chapters.Select(c => new MoveFileOrFolderJob(oldPath[c], c.FullArchiveFilePath));
return chapters.Select(c => new MoveFileOrFolderJob(oldPath[c], c.FullArchiveFilePath!));
}
}

View File

@ -1,52 +1,40 @@
using System.ComponentModel.DataAnnotations;
using API.Schema.Contexts;
using API.Schema.MangaConnectors;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Newtonsoft.Json;
namespace API.Schema.Jobs;
public class RetrieveChaptersJob : Job
public class RetrieveChaptersJob(ulong recurrenceMs, string mangaId, string? parentJobId = null, ICollection<string>? dependsOnJobsIds = null)
: Job(TokenGen.CreateToken(typeof(RetrieveChaptersJob)), JobType.RetrieveChaptersJob, recurrenceMs, parentJobId, dependsOnJobsIds)
{
[StringLength(64)] [Required] public string MangaId { get; init; }
private Manga _manga = null!;
[JsonIgnore]
public Manga Manga
{
get => LazyLoader.Load(this, ref _manga);
init => _manga = value;
}
[StringLength(8)] [Required] public string Language { get; private set; }
public RetrieveChaptersJob(Manga manga, string language, ulong recurrenceMs, Job? parentJob = null, ICollection<Job>? dependsOnJobs = null)
: base(TokenGen.CreateToken(typeof(RetrieveChaptersJob)), JobType.RetrieveChaptersJob, recurrenceMs, parentJob, dependsOnJobs)
{
this.MangaId = manga.MangaId;
this.Manga = manga;
this.Language = language;
}
/// <summary>
/// EF ONLY!!!
/// </summary>
internal RetrieveChaptersJob(ILazyLoader lazyLoader, string mangaId, string language, ulong recurrenceMs, string? parentJobId)
: base(lazyLoader, TokenGen.CreateToken(typeof(RetrieveChaptersJob)), JobType.RetrieveChaptersJob, recurrenceMs, parentJobId)
{
this.MangaId = mangaId;
this.Language = language;
}
[StringLength(64)]
[Required]
public string MangaId { get; init; } = mangaId;
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
Chapter[] allChapters = Manga.MangaConnector.GetChapters(Manga, Language);
Chapter[] newChapters = allChapters.Where(chapter => Manga.Chapters.Contains(chapter) == false).ToArray();
Log.Info($"{newChapters.Length} new chapters.");
Chapter[] allNewChapters = connector.GetNewChapters(manga).DistinctBy(c => c.ChapterId).ToArray();
Log.Info($"{allNewChapters.Length} new chapters.");
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.SaveChanges();
}

View File

@ -1,41 +0,0 @@
using System.ComponentModel.DataAnnotations;
using API.Schema.Contexts;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Newtonsoft.Json;
namespace API.Schema.Jobs;
public class UpdateChaptersDownloadedJob : Job
{
[StringLength(64)] [Required] public string MangaId { get; init; }
private Manga _manga = null!;
[JsonIgnore]
public Manga Manga
{
get => LazyLoader.Load(this, ref _manga);
init => _manga = value;
}
public UpdateChaptersDownloadedJob(Manga manga, ulong recurrenceMs, Job? parentJob = null, ICollection<Job>? dependsOnJobs = null)
: base(TokenGen.CreateToken(typeof(UpdateChaptersDownloadedJob)), JobType.UpdateChaptersDownloadedJob, recurrenceMs, parentJob, dependsOnJobs)
{
this.MangaId = manga.MangaId;
this.Manga = manga;
}
/// <summary>
/// EF ONLY!!!
/// </summary>
internal UpdateChaptersDownloadedJob(ILazyLoader lazyLoader, string mangaId, ulong recurrenceMs, string? parentJobId)
: base(lazyLoader, TokenGen.CreateToken(typeof(UpdateChaptersDownloadedJob)), JobType.UpdateChaptersDownloadedJob, recurrenceMs, parentJobId)
{
this.MangaId = mangaId;
}
protected override IEnumerable<Job> RunInternal(PgsqlContext context)
{
return Manga.Chapters.Select(c => new UpdateSingleChapterDownloadedJob(c, this));
}
}

View File

@ -0,0 +1,22 @@
using System.ComponentModel.DataAnnotations;
using Newtonsoft.Json;
namespace API.Schema.Jobs;
public class UpdateFilesDownloadedJob(ulong recurrenceMs, string mangaId, string? parentJobId = null, ICollection<string>? dependsOnJobsIds = null)
: Job(TokenGen.CreateToken(typeof(UpdateFilesDownloadedJob)), JobType.UpdateFilesDownloadedJob, recurrenceMs, parentJobId, dependsOnJobsIds)
{
[StringLength(64)]
[Required]
public string MangaId { get; init; } = mangaId;
protected override IEnumerable<Job> RunInternal(PgsqlContext context)
{
IQueryable<Chapter> chapters = context.Chapters.Where(c => c.ParentMangaId == MangaId);
foreach (Chapter chapter in chapters)
chapter.Downloaded = chapter.IsDownloaded();
context.SaveChanges();
return [];
}
}

View File

@ -0,0 +1,27 @@
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

@ -1,52 +0,0 @@
using System.ComponentModel.DataAnnotations;
using API.Schema.Contexts;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Newtonsoft.Json;
namespace API.Schema.Jobs;
public class UpdateSingleChapterDownloadedJob : Job
{
[StringLength(64)] [Required] public string ChapterId { get; init; }
private Chapter _chapter = null!;
[JsonIgnore]
public Chapter Chapter
{
get => LazyLoader.Load(this, ref _chapter);
init => _chapter = value;
}
public UpdateSingleChapterDownloadedJob(Chapter chapter, Job? parentJob = null, ICollection<Job>? dependsOnJobs = null)
: base(TokenGen.CreateToken(typeof(UpdateSingleChapterDownloadedJob)), JobType.UpdateSingleChapterDownloadedJob, 0, parentJob, dependsOnJobs)
{
this.ChapterId = chapter.ChapterId;
this.Chapter = chapter;
}
/// <summary>
/// EF ONLY!!!
/// </summary>
internal UpdateSingleChapterDownloadedJob(ILazyLoader lazyLoader, string chapterId, string? parentJobId)
: base(lazyLoader, TokenGen.CreateToken(typeof(UpdateSingleChapterDownloadedJob)), JobType.UpdateSingleChapterDownloadedJob, 0, parentJobId)
{
this.ChapterId = chapterId;
}
protected override IEnumerable<Job> RunInternal(PgsqlContext context)
{
Chapter.Downloaded = Chapter.CheckDownloaded();
try
{
context.SaveChanges();
}
catch (DbUpdateException e)
{
Log.Error(e);
}
return [];
}
}

View File

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

View File

@ -16,9 +16,4 @@ public class Link(string linkProvider, string linkUrl)
[Required]
[Url]
public string LinkUrl { get; init; } = linkUrl;
public override string ToString()
{
return $"{LinkId} {LinkProvider} {LinkUrl}";
}
}

View File

@ -14,9 +14,4 @@ public class LocalLibrary(string basePath, string libraryName)
[StringLength(512)]
[Required]
public string LibraryName { get; internal set; } = libraryName;
public override string ToString()
{
return $"{LocalLibraryId} {LibraryName} - {BasePath}";
}
}

View File

@ -1,10 +1,14 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Diagnostics.CodeAnalysis;
using System.Net;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.RegularExpressions;
using API.MangaDownloadClients;
using API.Schema.Jobs;
using API.Schema.MangaConnectors;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Newtonsoft.Json;
using static System.IO.UnixFileMode;
@ -16,101 +20,160 @@ public class Manga
[StringLength(64)]
[Required]
public string MangaId { get; init; }
[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)]
[StringLength(256)]
[Required]
public string MangaConnectorName { get; init; }
[JsonIgnore] public MangaConnector MangaConnector { get; init; } = null!;
public ICollection<Author> Authors { get; internal set; }= null!;
public ICollection<MangaTag> MangaTags { get; internal set; }= null!;
public ICollection<Link> Links { get; internal set; }= null!;
public ICollection<MangaAltTitle> AltTitles { get; internal set; } = null!;
[Required] public float IgnoreChaptersBefore { get; internal set; }
[StringLength(1024)] [Required] public string DirectoryName { get; private set; }
[JsonIgnore] [StringLength(512)] public string? CoverFileNameInCache { get; internal set; } = null;
public uint? Year { get; internal init; }
[StringLength(8)] public string? OriginalLanguage { get; internal init; }
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 set; }
[JsonIgnore]
[Url]
public string CoverUrl { get; internal set; }
[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]
[NotMapped]
public string? FullDirectoryPath => Library is not null ? Path.Join(Library.BasePath, DirectoryName) : null;
[NotMapped] public ICollection<string> ChapterIds => Chapters.Select(c => c.ChapterId).ToList();
private readonly ILazyLoader _lazyLoader = null!;
private ICollection<Chapter> _chapters = null!;
public string LibraryPath => Library is null ? TrangaSettings.downloadLocation : Library.BasePath;
[JsonIgnore]
public ICollection<Chapter> Chapters
{
get => _lazyLoader.Load(this, ref _chapters);
init => _chapters = value;
}
[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; }
public Manga(string idOnConnector, string name, string description, string websiteUrl, string coverUrl, MangaReleaseStatus releaseStatus,
MangaConnector mangaConnector, ICollection<Author> authors, ICollection<MangaTag> mangaTags, ICollection<Link> links, ICollection<MangaAltTitle> altTitles,
LocalLibrary? library = null, float ignoreChaptersBefore = 0f, uint? year = null, string? originalLanguage = null)
[JsonIgnore] public ICollection<Author>? Authors { get; internal set; }
[NotMapped]
[StringLength(64)]
[Required]
public IEnumerable<string> AuthorIds => Authors?.Select(a => a.AuthorId) ?? [];
[JsonIgnore] public ICollection<MangaTag>? MangaTags { get; internal set; }
[NotMapped]
[StringLength(64)]
[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.MangaTags = mangaTags;
this.Links = links;
this.AltTitles = altTitles;
this.IgnoreChaptersBefore = ignoreChaptersBefore;
this.DirectoryName = CleanDirectoryName(name);
this.Year = year;
this.OriginalLanguage = originalLanguage;
this.Chapters = [];
this.Library = library;
}
/// <summary>
/// EF ONLY!!!
/// </summary>
public Manga(ILazyLoader lazyLoader, 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)
public Manga(string idOnConnectorSite, string name, string description, string websiteUrl, string coverUrl,
string? coverFileNameInCache, uint year, string? originalLanguage, MangaReleaseStatus releaseStatus,
float ignoreChapterBefore, string mangaConnectorId)
{
this._lazyLoader = lazyLoader;
this.MangaId = mangaId;
this.IdOnConnectorSite = idOnConnectorSite;
this.Name = name;
this.Description = description;
this.WebsiteUrl = websiteUrl;
this.CoverUrl = coverUrl;
this.ReleaseStatus = releaseStatus;
this.MangaConnectorName = mangaConnectorName;
this.DirectoryName = directoryName;
this.LibraryId = libraryId;
this.IgnoreChaptersBefore = ignoreChaptersBefore;
this.Year = year;
this.OriginalLanguage = originalLanguage;
MangaId = TokenGen.CreateToken(typeof(Manga), mangaConnectorId, idOnConnectorSite);
IdOnConnectorSite = idOnConnectorSite;
Name = name;
Description = description;
WebsiteUrl = websiteUrl;
CoverUrl = coverUrl;
CoverFileNameInCache = coverFileNameInCache;
Year = year;
OriginalLanguage = originalLanguage;
ReleaseStatus = releaseStatus;
IgnoreChapterBefore = ignoreChapterBefore;
MangaConnectorId = mangaConnectorId;
DirectoryName = BuildFolderName(name);
}
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()
{
string? publicationFolder = FullDirectoryPath;
if (publicationFolder is null)
throw new DirectoryNotFoundException("Publication folder not found");
string publicationFolder = Path.Join(LibraryPath, this.DirectoryName);
if(!Directory.Exists(publicationFolder))
Directory.CreateDirectory(publicationFolder);
if(RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
@ -118,44 +181,5 @@ public class Manga
return publicationFolder;
}
//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();
}
public override string ToString()
{
return $"{MangaId} {Name}";
}
//TODO onchanges create job to update metadata files in archives, etc.
}

View File

@ -1,4 +1,5 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.EntityFrameworkCore;
namespace API.Schema;
@ -8,16 +9,11 @@ public class MangaAltTitle(string language, string title)
{
[StringLength(64)]
[Required]
public string AltTitleId { get; init; } = TokenGen.CreateToken("AltTitle");
public string AltTitleId { get; init; } = TokenGen.CreateToken("AltTitle", language, title);
[StringLength(8)]
[Required]
public string Language { get; init; } = language;
[StringLength(256)]
[Required]
public string Title { get; set; } = title;
public override string ToString()
{
return $"{AltTitleId} {Language} {Title}";
}
}

View File

@ -0,0 +1,191 @@
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

@ -0,0 +1,203 @@
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

@ -1,247 +0,0 @@
using System.Text.RegularExpressions;
using API.MangaDownloadClients;
using Newtonsoft.Json.Linq;
namespace API.Schema.MangaConnectors;
public class ComickIo : MangaConnector
{
//https://api.comick.io/docs/
//https://en.wikipedia.org/wiki/List_of_ISO_639_language_codes
public ComickIo() : base("ComickIo",
["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"],
["comick.io"],
"https://comick.io/static/icons/unicorn-64.png")
{
this.downloadClient = new HttpDownloadClient();
}
public override Manga[] SearchManga(string mangaSearchName)
{
Log.Info($"Searching Manga: {mangaSearchName}");
List<string> slugs = new();
int page = 1;
while(page < 50)
{
string requestUrl = $"https://api.comick.fun/v1.0/search/?type=comic&t=false&limit=100&showall=true&" +
$"page={page}&q={mangaSearchName}";
RequestResult result = downloadClient.MakeRequest(requestUrl, RequestType.Default);
if ((int)result.statusCode < 200 || (int)result.statusCode >= 300)
{
Log.Error("Request failed");
return [];
}
using StreamReader sr = new (result.result);
JArray data = JArray.Parse(sr.ReadToEnd());
if (data.Count < 1)
break;
slugs.AddRange(data.Select(token => token.Value<string>("slug")!));
page++;
}
Log.Debug($"Search {mangaSearchName} yielded {slugs.Count} slugs. Requesting mangas now...");
List<Manga> mangas = slugs.Select(GetMangaFromId).ToList()!;
Log.Info($"Search {mangaSearchName} yielded {mangas.Count} results.");
return mangas.ToArray();
}
private readonly Regex _getSlugFromTitleRex = new(@"https?:\/\/comick\.io\/comic\/(.+)(?:\/.*)*");
public override Manga? GetMangaFromUrl(string url)
{
Match m = _getSlugFromTitleRex.Match(url);
return m.Groups[1].Success ? GetMangaFromId(m.Groups[1].Value) : null;
}
public override Manga? GetMangaFromId(string mangaIdOnSite)
{
string requestUrl = $"https://api.comick.fun/comic/{mangaIdOnSite}";
RequestResult result = downloadClient.MakeRequest(requestUrl, RequestType.MangaInfo);
if ((int)result.statusCode < 200 || (int)result.statusCode >= 300)
{
Log.Error("Request failed");
return null;
}
using StreamReader sr = new (result.result);
JToken data = JToken.Parse(sr.ReadToEnd());
return ParseMangaFromJToken(data);
}
public override Chapter[] GetChapters(Manga manga, string? language = null)
{
Log.Info($"Getting Chapters: {manga.IdOnConnectorSite}");
List<string> chapterHids = new();
int page = 1;
while(page < 50)
{
string requestUrl = $"https://api.comick.fun/comic/{manga.IdOnConnectorSite}/chapters?limit=100&page={page}&lang={language}";
RequestResult result = downloadClient.MakeRequest(requestUrl, RequestType.MangaInfo);
if ((int)result.statusCode < 200 || (int)result.statusCode >= 300)
{
Log.Error("Request failed");
return [];
}
using StreamReader sr = new (result.result);
JToken data = JToken.Parse(sr.ReadToEnd());
JArray? chaptersArray = data["chapters"] as JArray;
if (chaptersArray?.Count < 1)
break;
chapterHids.AddRange(chaptersArray?.Select(token => token.Value<string>("hid")!)!);
page++;
}
Log.Debug($"Getting chapters for {manga.Name} yielded {chapterHids.Count} hids. Requesting chapters now...");
List<Chapter> chapters = chapterHids.Select(hid => ChapterFromHid(manga, hid)).ToList();
return chapters.ToArray();
}
private readonly Regex _hidFromUrl = new(@"https?:\/\/comick\.io\/comic\/.+\/([^-]+).*");
internal override string[] GetChapterImageUrls(Chapter chapter)
{
Match m = _hidFromUrl.Match(chapter.Url);
if (!m.Groups[1].Success)
return [];
string hid = m.Groups[1].Value;
string requestUrl = $"https://api.comick.fun/chapter/{hid}/get_images";
RequestResult result = downloadClient.MakeRequest(requestUrl, RequestType.MangaInfo);
if ((int)result.statusCode < 200 || (int)result.statusCode >= 300)
{
Log.Error("Request failed");
return [];
}
using StreamReader sr = new (result.result);
JArray data = JArray.Parse(sr.ReadToEnd());
return data.Select(token =>
{
string url = $"https://meo.comick.pictures/{token.Value<string>("b2key")}";
return url;
}).ToArray();
}
private Manga ParseMangaFromJToken(JToken json)
{
string? hid = json["comic"]?.Value<string>("hid");
string? slug = json["comic"]?.Value<string>("slug");
string? name = json["comic"]?.Value<string>("title");
string? description = json["comic"]?.Value<string>("desc");
string? originalLanguage = json["comic"]?.Value<string>("country");
string url = $"https://comick.io/comic/{slug}";
string? coverName = json["comic"]?["md_covers"]?.First?.Value<string>("b2key");
string coverUrl = $"https://meo.comick.pictures/{coverName}";
int? releaseStatusStr = json["comic"]?.Value<int>("status");
MangaReleaseStatus status = releaseStatusStr switch
{
1 => MangaReleaseStatus.Continuing,
2 => MangaReleaseStatus.Completed,
3 => MangaReleaseStatus.Cancelled,
4 => MangaReleaseStatus.OnHiatus,
_ => MangaReleaseStatus.Unreleased
};
uint? year = json["comic"]?.Value<uint?>("year");
JArray? altTitlesArray = json["comic"]?["md_titles"] as JArray;
//Cant let language be null, so fill with whatever.
byte whatever = 0;
List<MangaAltTitle> altTitles = altTitlesArray?
.Select(token => new MangaAltTitle(token.Value<string>("lang")??whatever++.ToString(), token.Value<string>("title")!))
.ToList()!;
JArray? authorsArray = json["authors"] as JArray;
JArray? artistsArray = json["artists"] as JArray;
List<Author> authors = authorsArray?.Concat(artistsArray!)
.Select(token => new Author(token.Value<string>("name")!))
.DistinctBy(a => a.AuthorId)
.ToList()!;
JArray? genreArray = json["comic"]?["md_comic_md_genres"] as JArray;
List<MangaTag> tags = genreArray?
.Select(token => new MangaTag(token["md_genres"]?.Value<string>("name")!))
.ToList()!;
JArray? linksArray = json["comic"]?["links"] as JArray;
List<Link> links = linksArray?
.ToObject<Dictionary<string,string>>()?
.Select(kv =>
{
string fullUrl = 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, fullUrl);
}).ToList()!;
if(hid is null)
throw new Exception("hid is null");
if(slug is null)
throw new Exception("slug is null");
if(name is null)
throw new Exception("name is null");
return new Manga(hid, name, description??"", url, coverUrl, status, this,
authors, tags, links, altTitles,
year: year, originalLanguage: originalLanguage);
}
private Chapter ChapterFromHid(Manga parentManga, string hid)
{
string requestUrl = $"https://api.comick.fun/chapter/{hid}";
RequestResult result = downloadClient.MakeRequest(requestUrl, RequestType.Default);
if ((int)result.statusCode < 200 || (int)result.statusCode >= 300)
{
Log.Error("Request failed");
throw new Exception("Request failed");
}
using StreamReader sr = new (result.result);
JToken data = JToken.Parse(sr.ReadToEnd());
string? canonical = data.Value<string>("canonical");
string? chapterNum = data["chapter"]?.Value<string>("chap");
string? volumeNumStr = data["chapter"]?.Value<string>("vol");
int? volumeNum = volumeNumStr is null ? null : int.Parse(volumeNumStr);
string? title = data["chapter"]?.Value<string>("title");
if(chapterNum is null)
throw new Exception("chapterNum is null");
string url = $"https://comick.io{canonical}";
return new Chapter(parentManga, url, chapterNum, volumeNum, title);
}
}

View File

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

View File

@ -11,14 +11,6 @@ namespace API.Schema.MangaConnectors;
[PrimaryKey("Name")]
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)]
[Required]
public string Name { get; init; } = name;
@ -34,41 +26,32 @@ public abstract class MangaConnector(string name, string[] supportedLanguages, s
[Required]
public bool Enabled { get; internal set; } = true;
public abstract Manga[] SearchManga(string mangaSearchName);
public abstract (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)[] GetManga(string publicationTitle = "");
public abstract Manga? GetMangaFromUrl(string url);
public abstract (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)? GetMangaFromUrl(string url);
public abstract Manga? GetMangaFromId(string mangaIdOnSite);
public abstract (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)? GetMangaFromId(string publicationId);
public abstract Chapter[] GetChapters(Manga manga, string? language = null);
public abstract Chapter[] GetChapters(Manga manga, string language="en");
[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);
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;
}
public bool ValidateUrl(string url) => BaseUris.Any(baseUri => Regex.IsMatch(url, "https?://" + baseUri + "/.*"));
}

View File

@ -1,6 +1,8 @@
using System.Text.RegularExpressions;
using System.Net;
using System.Text.Json.Nodes;
using System.Text.RegularExpressions;
using API.MangaDownloadClients;
using Newtonsoft.Json.Linq;
using JsonSerializer = System.Text.Json.JsonSerializer;
namespace API.Schema.MangaConnectors;
@ -9,327 +11,313 @@ public class MangaDex : MangaConnector
//https://api.mangadex.org/docs/3-enumerations/#language-codes--localization
//https://en.wikipedia.org/wiki/List_of_ISO_639_language_codes
//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();
}
private const int Limit = 100;
public override Manga[] SearchManga(string mangaSearchName)
public override (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)[] GetManga(string publicationTitle = "")
{
Log.Info($"Searching Manga: {mangaSearchName}");
List<Manga> mangas = new ();
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
HashSet<(Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)> retManga = new();
List<JsonNode> results = new();
int offset = 0;
int total = int.MaxValue;
while(offset < total)
//Request all search-results
while (offset < total) //As long as we haven't requested all "Pages"
{
//Request next Page
string requestUrl =
$"https://api.mangadex.org/manga?limit={Limit}&offset={offset}&title={mangaSearchName}" +
$"&contentRating%5B%5D=safe&contentRating%5B%5D=suggestive&contentRating%5B%5D=erotica" +
$"&includes%5B%5D=manga&includes%5B%5D=cover_art&includes%5B%5D=author&includes%5B%5D=artist&includes%5B%5D=tag'";
offset += Limit;
RequestResult result = downloadClient.MakeRequest(requestUrl, RequestType.MangaDexFeed);
if ((int)result.statusCode < 200 || (int)result.statusCode >= 300)
$"https://api.mangadex.org/manga?limit={limit}&title={publicationTitle}&offset={offset}" +
$"&includes[]=manga&includes[]=cover_art&includes[]=author&includes[]=artist&includes[]=tag";
RequestResult requestResult = downloadClient.MakeRequest(requestUrl, RequestType.MangaInfo);
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
{
Log.Error("Request failed");
return [];
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;
}
using StreamReader sr = new (result.result);
JObject jObject = JObject.Parse(sr.ReadToEnd());
if(result.ContainsKey("total"))
total = result["total"]!.GetValue<int>(); //Update the total number of Publications
else continue;
if (jObject.Value<string>("result") != "ok")
{
JArray? errors = jObject["errors"] as JArray;
Log.Error($"Request failed: {string.Join(',', errors?.Select(e => e.Value<string>("title")) ?? [])}");
return [];
}
total = jObject.Value<int>("total");
JArray? data = jObject.Value<JArray>("data");
if (data is null)
{
Log.Error("Data was null");
return [];
}
mangas.AddRange(data.Select(ParseMangaFromJToken));
if (result.ContainsKey("data"))
results.AddRange(result["data"]!.AsArray()!);//Manga-data-Array
}
Log.Info($"Search {mangaSearchName} yielded {mangas.Count} results.");
return mangas.ToArray();
foreach (JsonNode mangaNode in results)
{
if(MangaFromJsonObject(mangaNode.AsObject()) is { } manga)
retManga.Add(manga); //Add Publication (Manga) to result
}
return retManga.ToArray();
}
private static readonly Regex GetMangaIdFromUrl = new(@"https?:\/\/mangadex\.org\/title\/([a-z0-9-]+)\/?.*");
public override Manga? GetMangaFromUrl(string url)
public override (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)? GetMangaFromId(string publicationId)
{
Log.Info($"Getting Manga: {url}");
if (!UrlMatchesConnector(url))
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.Debug($"Url is not for Connector. {url}");
Log.Info($"{requestResult.statusCode}: {url}");
return null;
}
JsonObject? result = JsonSerializer.Deserialize<JsonObject>(requestResult.result);
if(result is not null)
return MangaFromJsonObject(result["data"]!.AsObject());
Log.Info($"result was null: {url}");
return null;
}
Match match = GetMangaIdFromUrl.Match(url);
if (!match.Success || !match.Groups[1].Success)
{
Log.Debug($"Url is not for Connector (Could not retrieve id). {url}");
return null;
}
string id = match.Groups[1].Value;
public override (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)? GetMangaFromUrl(string url)
{
Regex idRex = new (@"https:\/\/mangadex.org\/title\/([A-z0-9-]*)\/.*");
string id = idRex.Match(url).Groups[1].Value;
return GetMangaFromId(id);
}
public override Manga? GetMangaFromId(string mangaIdOnSite)
private (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)? MangaFromJsonObject(JsonObject manga)
{
Log.Info($"Getting Manga: {mangaIdOnSite}");
string requestUrl =
$"https://api.mangadex.org/manga/{mangaIdOnSite}" +
$"?includes%5B%5D=manga&includes%5B%5D=cover_art&includes%5B%5D=author&includes%5B%5D=artist&includes%5B%5D=tag'";
RequestResult result = downloadClient.MakeRequest(requestUrl, RequestType.MangaDexFeed);
if ((int)result.statusCode < 200 || (int)result.statusCode >= 300)
if (!manga.TryGetPropertyValue("id", out JsonNode? idNode))
{
Log.Error("Request failed");
Log.Info("id was null");
return null;
}
string publicationId = idNode!.GetValue<string>();
if (!manga.TryGetPropertyValue("attributes", out JsonNode? attributesNode))
{
Log.Info("attributes was null");
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;
}
using StreamReader sr = new (result.result);
JObject jObject = JObject.Parse(sr.ReadToEnd());
if (jObject.Value<string>("result") != "ok")
JsonNode? coverNode = relationshipsNode!.AsArray()
.FirstOrDefault(rel => rel!["type"]!.GetValue<string>().Equals("cover_art"));
if (coverNode is null)
{
JArray? errors = jObject["errors"] as JArray;
Log.Error($"Request failed: {string.Join(',', errors?.Select(e => e.Value<string>("title")) ?? [])}");
Log.Info("coverNode was null");
return null;
}
string fileName = coverNode["attributes"]!["fileName"]!.GetValue<string>();
string coverUrl = $"https://uploads.mangadex.org/covers/{publicationId}/{fileName}";
JObject? data = jObject["data"] as JObject;
if (data is null)
List<string> authorNames = new();
JsonNode?[] authorNodes = relationshipsNode.AsArray()
.Where(rel => rel!["type"]!.GetValue<string>().Equals("author") || rel!["type"]!.GetValue<string>().Equals("artist")).ToArray();
foreach (JsonNode? authorNode in authorNodes)
{
Log.Error("Data was null");
return null;
string authorName = authorNode!["attributes"]!["name"]!.GetValue<string>();
if(!authorNames.Contains(authorName))
authorNames.Add(authorName);
}
List<Author> authors = authorNames.Select(a => new Author(a)).ToList();
Manga manga = ParseMangaFromJToken(data);
return manga;
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 = null)
public override Chapter[] GetChapters(Manga manga, string language="en")
{
Log.Info($"Getting Chapters: {manga.IdOnConnectorSite}");
List<Chapter> chapters = new ();
int offset = 0;
int total = int.MaxValue;
while(offset < total)
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)
{
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&includeFutureUpdates=0&includes%5B%5D=";
offset += Limit;
RequestResult result = downloadClient.MakeRequest(requestUrl, RequestType.MangaDexFeed);
if ((int)result.statusCode < 200 || (int)result.statusCode >= 300)
//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.Error("Request failed");
return [];
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;
}
using StreamReader sr = new (result.result);
JObject jObject = JObject.Parse(sr.ReadToEnd());
if (jObject.Value<string>("result") != "ok")
total = result["total"]!.GetValue<int>();
JsonArray chaptersInResult = result["data"]!.AsArray();
//Loop through all Chapters in result and extract information from JSON
foreach (JsonNode? jsonNode in chaptersInResult)
{
JArray? errors = jObject["errors"] as JArray;
Log.Error($"Request failed: {string.Join(',', errors?.Select(e => e.Value<string>("title")) ?? [])}");
return [];
JsonObject chapter = (JsonObject)jsonNode!;
JsonObject attributes = chapter["attributes"]!.AsObject();
string chapterId = chapter["id"]!.GetValue<string>();
string url = $"https://mangadex.org/chapter/{chapterId}";
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}");
continue;
}
try
{
Chapter newChapter = new(manga, url, chapterNumber, volume, title);
if(!chapters.Contains(newChapter))
chapters.Add(newChapter);
}
catch (Exception e)
{
Log.Debug(e);
}
}
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 ordered by Chapter-Number
return chapters.ToArray();
}
private static readonly Regex GetChapterIdFromUrl = new(@"https?:\/\/mangadex\.org\/chapter\/([a-z0-9-]+)\/?.*");
internal override string[] GetChapterImageUrls(Chapter chapter)
{
Log.Info($"Getting Chapter Image-Urls: {chapter.Url}");
if (!UrlMatchesConnector(chapter.Url))
{//Request URLs for Chapter-Images
Match m = Regex.Match(chapter.Url, @"https?:\/\/mangadex.org\/chapter\/([0-9\-a-z]+)");
if (!m.Success)
{
Log.Debug($"Url is not for Connector. {chapter.Url}");
Log.Error($"Could not parse Chapter ID: {chapter.Url}");
return [];
}
Match match = GetChapterIdFromUrl.Match(chapter.Url);
if (!match.Success || !match.Groups[1].Success)
string url = $"https://api.mangadex.org/at-home/server/{m.Groups[1].Value}?forcePort443=false";
RequestResult requestResult =
downloadClient.MakeRequest(url, RequestType.MangaDexImage);
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
{
Log.Debug($"Url is not for Connector (Could not retrieve id). {chapter.Url}");
Log.Info($"{requestResult.statusCode}: {url}");
return [];
}
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)
JsonObject? result = JsonSerializer.Deserialize<JsonObject>(requestResult.result);
if (result is null)
{
Log.Error("Request failed");
Log.Info($"Result was null: {url}");
return [];
}
using StreamReader sr = new (result.result);
JObject jObject = JObject.Parse(sr.ReadToEnd());
if (jObject.Value<string>("result") != "ok")
{
JArray? errors = jObject["errors"] as JArray;
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);
string baseUrl = result["baseUrl"]!.GetValue<string>();
string hash = result["chapter"]!["hash"]!.GetValue<string>();
JsonArray imageFileNames = result["chapter"]!["data"]!.AsArray();
//Loop through all imageNames and construct urls (imageUrl)
List<string> imageUrls = new();
foreach (JsonNode? image in imageFileNames)
imageUrls.Add($"{baseUrl}/data/{hash}/{image!.GetValue<string>()}");
return imageUrls.ToArray();
}
}

View File

@ -0,0 +1,183 @@
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

@ -0,0 +1,233 @@
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

@ -0,0 +1,219 @@
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

@ -0,0 +1,223 @@
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

@ -0,0 +1,179 @@
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

@ -0,0 +1,259 @@
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

@ -0,0 +1,175 @@
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

@ -9,9 +9,4 @@ public class MangaTag(string tag)
[StringLength(64)]
[Required]
public string Tag { get; init; } = tag;
public override string ToString()
{
return $"{Tag}";
}
}

View File

@ -4,49 +4,25 @@ using Microsoft.EntityFrameworkCore;
namespace API.Schema;
[PrimaryKey("NotificationId")]
public class Notification
public class Notification(string title, string message = "", NotificationUrgency urgency = NotificationUrgency.Normal, DateTime? date = null)
{
[StringLength(64)]
[Required]
public string NotificationId { get; init; }
public string NotificationId { get; init; } = TokenGen.CreateToken("Notification");
[Required]
public NotificationUrgency Urgency { get; init; }
public NotificationUrgency Urgency { get; init; } = urgency;
[StringLength(128)]
[Required]
public string Title { get; init; }
public string Title { get; init; } = title;
[StringLength(512)]
[Required]
public string Message { get; init; }
public string Message { get; init; } = message;
[Required]
public DateTime Date { get; init; }
public DateTime Date { get; init; } = date ?? DateTime.UtcNow;
public Notification(string title, string message = "", NotificationUrgency urgency = NotificationUrgency.Normal, DateTime? date = null)
{
this.NotificationId = TokenGen.CreateToken("Notification");
this.Title = title;
this.Message = message;
this.Urgency = urgency;
this.Date = date ?? DateTime.UtcNow;
}
/// <summary>
/// EF ONLY!!!
/// </summary>
public Notification(string notificationId, string title, string message, NotificationUrgency urgency, DateTime date)
{
this.NotificationId = notificationId;
this.Title = title;
this.Message = message;
this.Urgency = urgency;
this.Date = date;
}
public override string ToString()
{
return $"{NotificationId} {Urgency} {Title}";
}
public Notification() : this("") { }
}

113
API/Schema/PgsqlContext.cs Normal file
View File

@ -0,0 +1,113 @@
using API.Schema.Jobs;
using API.Schema.LibraryConnectors;
using API.Schema.MangaConnectors;
using API.Schema.NotificationConnectors;
using Microsoft.EntityFrameworkCore;
namespace API.Schema;
public class PgsqlContext(DbContextOptions<PgsqlContext> options) : DbContext(options)
{
public DbSet<Job> Jobs { get; set; }
public DbSet<MangaConnector> MangaConnectors { get; set; }
public DbSet<Manga> Mangas { get; set; }
public DbSet<LocalLibrary> LocalLibraries { get; set; }
public DbSet<Chapter> Chapters { get; set; }
public DbSet<Author> Authors { get; set; }
public DbSet<Link> Links { get; set; }
public DbSet<MangaTag> Tags { get; set; }
public DbSet<MangaAltTitle> AltTitles { get; set; }
public DbSet<LibraryConnector> LibraryConnectors { get; set; }
public DbSet<NotificationConnector> NotificationConnectors { get; set; }
public DbSet<Notification> Notifications { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<MangaConnector>()
.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>()
.HasDiscriminator<JobType>(j => j.JobType)
.HasValue<MoveFileOrFolderJob>(JobType.MoveFileOrFolderJob)
.HasValue<DownloadAvailableChaptersJob>(JobType.DownloadAvailableChaptersJob)
.HasValue<DownloadSingleChapterJob>(JobType.DownloadSingleChapterJob)
.HasValue<DownloadMangaCoverJob>(JobType.DownloadMangaCoverJob)
.HasValue<UpdateMetadataJob>(JobType.UpdateMetaDataJob)
.HasValue<RetrieveChaptersJob>(JobType.RetrieveChaptersJob)
.HasValue<UpdateFilesDownloadedJob>(JobType.UpdateFilesDownloadedJob);
modelBuilder.Entity<Job>()
.HasMany<Job>()
.WithOne(j => j.ParentJob)
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<Job>()
.HasMany<Job>(j => j.DependsOnJobs)
.WithMany();
modelBuilder.Entity<UpdateMetadataJob>()
.Navigation(umj => umj.Manga)
.AutoInclude();
modelBuilder.Entity<Manga>()
.HasOne<MangaConnector>(m => m.MangaConnector)
.WithMany()
.HasForeignKey(m => m.MangaConnectorId)
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<Manga>()
.Navigation(m => m.MangaConnector)
.AutoInclude();
modelBuilder.Entity<Manga>()
.HasOne<LocalLibrary>(m => m.Library)
.WithMany()
.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)
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<Chapter>()
.Navigation(c => c.ParentManga)
.AutoInclude();
}
}

View File

@ -5,7 +5,7 @@ namespace API;
public static class TokenGen
{
private const int MinimumLength = 16;
private const int MinimumLength = 32;
private const int MaximumLength = 64;
private const string Chars = "abcdefghijklmnopqrstuvwxyz0123456789";

View File

@ -1,27 +1,15 @@
using API.Schema;
using API.Schema.Contexts;
using API.Schema.Jobs;
using API.Schema.MangaConnectors;
using API.Schema.NotificationConnectors;
using log4net;
using log4net.Config;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.ChangeTracking;
namespace API;
public static class Tranga
{
// ReSharper disable once InconsistentNaming
private const string TRANGA =
"\n\n" +
" _______ v2\n" +
"|_ _|.----..---.-..-----..-----..---.-.\n" +
" | | | _|| _ || || _ || _ |\n" +
" |___| |__| |___._||__|__||___ ||___._|\n" +
" |_____| \n\n";
public static Thread NotificationSenderThread { get; } = new (NotificationSender);
public static Thread JobStarterThread { get; } = new (JobStarter);
private static readonly ILog Log = LogManager.GetLogger(typeof(Tranga));
@ -30,7 +18,6 @@ public static class Tranga
{
BasicConfigurator.Configure();
Log.Info("Logger Configured.");
Log.Info(TRANGA);
}
private static void NotificationSender(object? serviceProviderObj)
@ -42,7 +29,12 @@ public static class Tranga
}
IServiceProvider serviceProvider = (IServiceProvider)serviceProviderObj!;
using IServiceScope scope = serviceProvider.CreateScope();
NotificationsContext context = scope.ServiceProvider.GetRequiredService<NotificationsContext>();
PgsqlContext? context = scope.ServiceProvider.GetService<PgsqlContext>();
if (context is null)
{
Log.Error("PgsqlContext is null");
return;
}
try
{
@ -69,9 +61,14 @@ public static class Tranga
private static void SendNotifications(IServiceProvider serviceProvider, NotificationUrgency urgency)
{
Log.Debug($"Sending notifications for {urgency}");
Log.Info($"Sending notifications for {urgency}");
using IServiceScope scope = serviceProvider.CreateScope();
NotificationsContext context = scope.ServiceProvider.GetRequiredService<NotificationsContext>();
PgsqlContext? context = scope.ServiceProvider.GetService<PgsqlContext>();
if (context is null)
{
Log.Error("PgsqlContext is null");
return;
}
List<Notification> notifications = context.Notifications.Where(n => n.Urgency == urgency).ToList();
if (!notifications.Any())
@ -93,10 +90,17 @@ public static class Tranga
Log.Error("Error sending notifications.", e);
}
}
private const string TRANGA =
"\n\n" +
" _______ \n" +
"|_ _|.----..---.-..-----..-----..---.-.\n" +
" | | | _|| _ || || _ || _ |\n" +
" |___| |__| |___._||__|__||___ ||___._|\n" +
" |_____| \n\n";
private static readonly Dictionary<Thread, Job> RunningJobs = new();
private static void JobStarter(object? serviceProviderObj)
{
Log.Info("JobStarter Thread running.");
if (serviceProviderObj is null)
{
Log.Error("serviceProviderObj is null");
@ -104,75 +108,61 @@ public static class Tranga
}
IServiceProvider serviceProvider = (IServiceProvider)serviceProviderObj;
using IServiceScope scope = serviceProvider.CreateScope();
PgsqlContext context = scope.ServiceProvider.GetRequiredService<PgsqlContext>();
PgsqlContext? context = scope.ServiceProvider.GetService<PgsqlContext>();
if (context is null)
{
Log.Error("PgsqlContext is null");
return;
}
Log.Info(TRANGA);
Log.Info("JobStarter Thread running.");
while (true)
{
Log.Debug("Starting Job-Cycle...");
DateTime cycleStart = DateTime.UtcNow;
Log.Debug("Loading Jobs...");
DateTime loadStart = DateTime.UtcNow;
context.Jobs.Load();
Log.Debug("Updating Entries...");
foreach (EntityEntry entityEntry in context.ChangeTracker.Entries().ToArray())
entityEntry.Reload();
Log.Debug($"Jobs Loaded! (took {DateTime.UtcNow.Subtract(loadStart).TotalMilliseconds}ms)");
//Update finished Jobs to new states
List<Job> completedJobs = context.Jobs.Local.Where(j => j.state == JobState.Completed).ToList();
foreach (Job completedJob in completedJobs)
if (completedJob.RecurrenceMs <= 0)
context.Jobs.Remove(completedJob);
List<Job> completedJobs = context.Jobs.Where(j => j.state >= JobState.Completed).ToList();
Log.Debug($"Completed jobs: {completedJobs.Count}");
foreach (Job job in completedJobs)
if (job.RecurrenceMs <= 0)
context.Jobs.Remove(job);
else
{
completedJob.state = JobState.CompletedWaiting;
completedJob.LastExecution = DateTime.UtcNow;
if (job.state >= JobState.Failed)
job.Enabled = false;
else
job.state = JobState.Waiting;
job.LastExecution = DateTime.UtcNow;
}
List<Job> failedJobs = context.Jobs.Local.Where(j => j.state == JobState.Failed).ToList();
foreach (Job failedJob in failedJobs)
List<Job> runJobs = context.Jobs.Where(j => j.state <= JobState.Running && j.Enabled == true).ToList()
.Where(j => j.NextExecution < DateTime.UtcNow).ToList();
IEnumerable<Job> orderedJobs = OrderJobs(runJobs, context).ToList();
Log.Debug($"Jobs Due: {runJobs.Count} Running: {RunningJobs.Count} Ordered: {orderedJobs.Count()}");
foreach (Job job in orderedJobs)
{
failedJob.Enabled = false;
failedJob.LastExecution = DateTime.UtcNow;
}
// If the job is already running, skip it
if (RunningJobs.Values.Any(j => j.JobId == job.JobId)) continue;
//Retrieve waiting and due Jobs
List<Job> runningJobs = context.Jobs.Local.Where(j => j.state == JobState.Running).ToList();
DateTime filterStart = DateTime.UtcNow;
Log.Debug("Filtering Jobs...");
List<MangaConnector> busyConnectors = GetBusyConnectors(runningJobs);
List<Job> waitingJobs = GetWaitingJobs(context.Jobs.Local.ToList());
List<Job> dueJobs = FilterDueJobs(waitingJobs);
List<Job> jobsWithoutBusyConnectors = FilterJobWithBusyConnectors(dueJobs, busyConnectors);
List<Job> jobsWithoutMissingDependencies = FilterJobDependencies(context, jobsWithoutBusyConnectors);
List<Job> jobsWithoutDownloading =
jobsWithoutMissingDependencies
.Where(j => j.JobType != JobType.DownloadSingleChapterJob)
.DistinctBy(j => j.JobType)
.ToList();
List<Job> firstChapterPerConnector =
jobsWithoutMissingDependencies
.Where(j => j.JobType == JobType.DownloadSingleChapterJob)
.OrderBy(j =>
//If a Job for that connector is already running, skip it
if (job is DownloadAvailableChaptersJob dncj)
{
if (RunningJobs.Values.Any(j =>
j is DownloadAvailableChaptersJob rdncj &&
context.Mangas.Find(rdncj.MangaId)?.MangaConnector == context.Mangas.Find(dncj.MangaId)?.MangaConnector))
{
DownloadSingleChapterJob dscj = (DownloadSingleChapterJob)j;
return dscj.Chapter;
})
.DistinctBy(j =>
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))
{
DownloadSingleChapterJob dscj = (DownloadSingleChapterJob)j;
return dscj.Chapter.ParentManga.MangaConnector;
})
.ToList();
continue;
}
}
List<Job> startJobs = jobsWithoutDownloading.Concat(firstChapterPerConnector).ToList();
Log.Debug($"Jobs Filtered! (took {DateTime.UtcNow.Subtract(filterStart).TotalMilliseconds}ms)");
//Start Jobs that are allowed to run (preconditions match)
foreach (Job job in startJobs)
{
Thread t = new(() =>
{
job.Run(serviceProvider);
@ -180,10 +170,6 @@ public static class Tranga
RunningJobs.Add(t, job);
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: {jobsWithoutMissingDependencies.Count}");
(Thread, Job)[] removeFromThreadsList = RunningJobs.Where(t => !t.Key.IsAlive)
.Select(t => (t.Key, t.Value)).ToArray();
@ -201,66 +187,88 @@ public static class Tranga
{
Log.Error("Failed saving Job changes.", e);
}
Log.Debug($"Job-Cycle over! (took {DateTime.UtcNow.Subtract(cycleStart).TotalMilliseconds}ms)");
Thread.Sleep(TrangaSettings.startNewJobTimeoutMs);
}
}
private static List<MangaConnector> GetBusyConnectors(List<Job> runningJobs)
private static IEnumerable<Job> OrderJobs(List<Job> jobs, PgsqlContext context)
{
HashSet<MangaConnector> busyConnectors = new();
foreach (Job runningJob in runningJobs)
Dictionary<JobType, List<Job>> jobsByType = new();
foreach (Job job in jobs)
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))
{
if(GetJobConnector(runningJob) is { } mangaConnector)
busyConnectors.Add(mangaConnector);
foreach (DownloadAvailableChaptersJob job in jobsByType[JobType.DownloadAvailableChaptersJob])
{
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();
}
private static List<Job> GetWaitingJobs(List<Job> jobs) =>
jobs
.Where(j =>
j.Enabled &&
(j.state == JobState.FirstExecution || j.state == JobState.CompletedWaiting))
.ToList();
private static List<Job> FilterDueJobs(List<Job> jobs) =>
jobs
.Where(j => j.NextExecution < DateTime.UtcNow)
.ToList();
private static List<Job> FilterJobDependencies(PgsqlContext context, List<Job> jobs) =>
jobs
.Where(j =>
if (jobsByType.ContainsKey(JobType.UpdateMetaDataJob))
{
foreach (UpdateMetadataJob job in jobsByType[JobType.UpdateMetaDataJob])
{
Log.Debug($"Loading Job Preconditions {j}...");
context.Entry(j).Collection(j => j.DependsOnJobs).Load();
Log.Debug($"Loaded Job Preconditions {j}!");
return j.DependenciesFulfilled;
})
.ToList();
private static List<Job> FilterJobWithBusyConnectors(List<Job> jobs, List<MangaConnector> busyConnectors) =>
jobs
.Where(j =>
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])
{
//Filter jobs with busy connectors
if (GetJobConnector(j) is { } mangaConnector)
return busyConnectors.Contains(mangaConnector) == false;
return true;
})
.ToList();
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))!;
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;
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;
}
}