7 Commits

Author SHA1 Message Date
314700aa8e Add TODO to remove migrations after some time 2025-06-30 22:05:49 +02:00
b299cc9109 Merge branch 'Jikan' into JobQueue-Sortable
# Conflicts:
#	API/Schema/Contexts/PgsqlContext.cs
2025-06-30 22:03:22 +02:00
7e9ba7090a Manga and Chapters are shared across Connectors 2025-06-30 22:01:10 +02:00
ea73d03b8f WIP: Manga can be linked to multiple Connectors
- PgsqlContext Adjustment
2025-06-30 14:42:24 +02:00
e9d9bebcd7 WIP: Manga can be linked to multiple Connectors 2025-06-30 14:24:17 +02:00
e5937d2654 Job is IComparable<Job> 2025-06-30 12:58:00 +02:00
e063cf1fd9 Debug: MatchJobsRunningAndWaiting
Some checks failed
Docker Image CI / build (push) Has been cancelled
UpdateCoverJobs not starting.
2025-06-28 23:15:51 +02:00
50 changed files with 3279 additions and 677 deletions

View File

@ -33,8 +33,4 @@
<PackageReference Include="System.Drawing.Common" Version="9.0.3" />
</ItemGroup>
<ItemGroup>
<Folder Include="Migrations\" />
</ItemGroup>
</Project>

View File

@ -37,7 +37,7 @@ public class JobController(PgsqlContext context, ILog Log) : Controller
[ProducesResponseType<Job[]>(Status200OK, "application/json")]
public IActionResult GetJobs([FromBody]string[] ids)
{
Job[] ret = context.Jobs.Where(job => ids.Contains(job.JobId)).ToArray();
Job[] ret = context.Jobs.Where(job => ids.Contains(job.Key)).ToArray();
return Ok(ret);
}
@ -103,11 +103,11 @@ public class JobController(PgsqlContext context, ILog Log) : Controller
/// <summary>
/// Create a new DownloadAvailableChaptersJob
/// </summary>
/// <param name="MangaId">ID of Manga</param>
/// <param name="MangaId">ID of Obj</param>
/// <param name="record">Job-Configuration</param>
/// <response code="201">Job-IDs</response>
/// <response code="400">Could not find ToLibrary with ID</response>
/// <response code="404">Could not find Manga with ID</response>
/// <response code="400">Could not find ToFileLibrary with ID</response>
/// <response code="404">Could not find Obj with ID</response>
/// <response code="500">Error during Database Operation</response>
[HttpPut("DownloadAvailableChaptersJob/{MangaId}")]
[ProducesResponseType<string[]>(Status201Created, "application/json")]
@ -122,7 +122,7 @@ public class JobController(PgsqlContext context, ILog Log) : Controller
{
try
{
LocalLibrary? l = context.LocalLibraries.Find(record.localLibraryId);
FileLibrary? l = context.LocalLibraries.Find(record.localLibraryId);
if (l is null)
return BadRequest();
m.Library = l;
@ -166,9 +166,9 @@ public class JobController(PgsqlContext context, ILog Log) : Controller
/// <summary>
/// Create a new UpdateChaptersDownloadedJob
/// </summary>
/// <param name="MangaId">ID of the Manga</param>
/// <param name="MangaId">ID of the Obj</param>
/// <response code="201">Job-IDs</response>
/// <response code="201">Could not find Manga with ID</response>
/// <response code="201">Could not find Obj with ID</response>
/// <response code="500">Error during Database Operation</response>
[HttpPut("UpdateFilesJob/{MangaId}")]
[ProducesResponseType<string[]>(Status201Created, "application/json")]
@ -183,7 +183,7 @@ public class JobController(PgsqlContext context, ILog Log) : Controller
}
/// <summary>
/// Create a new UpdateMetadataJob for all Manga
/// Create a new UpdateMetadataJob for all Obj
/// </summary>
/// <response code="201">Job-IDs</response>
/// <response code="500">Error during Database Operation</response>
@ -209,9 +209,9 @@ public class JobController(PgsqlContext context, ILog Log) : Controller
/// <summary>
/// Not Implemented: Create a new UpdateMetadataJob
/// </summary>
/// <param name="MangaId">ID of the Manga</param>
/// <param name="MangaId">ID of the Obj</param>
/// <response code="201">Job-IDs</response>
/// <response code="404">Could not find Manga with ID</response>
/// <response code="404">Could not find Obj with ID</response>
/// <response code="500">Error during Database Operation</response>
[HttpPut("UpdateMetadataJob/{MangaId}")]
[ProducesResponseType<string[]>(Status201Created, "application/json")]
@ -223,7 +223,7 @@ public class JobController(PgsqlContext context, ILog Log) : Controller
}
/// <summary>
/// Not Implemented: Create a new UpdateMetadataJob for all Manga
/// Not Implemented: Create a new UpdateMetadataJob for all Obj
/// </summary>
/// <response code="201">Job-IDs</response>
/// <response code="500">Error during Database Operation</response>
@ -241,7 +241,7 @@ public class JobController(PgsqlContext context, ILog Log) : Controller
{
context.Jobs.AddRange(jobs);
context.SaveChanges();
return new CreatedResult((string?)null, jobs.Select(j => j.JobId).ToArray());
return new CreatedResult((string?)null, jobs.Select(j => j.Key).ToArray());
}
catch (Exception e)
{
@ -279,15 +279,6 @@ public class JobController(PgsqlContext context, ILog Log) : Controller
}
}
private IQueryable<Job> GetChildJobs(string parentJobId)
{
IQueryable<Job> children = context.Jobs.Where(j => j.ParentJobId == parentJobId);
foreach (Job child in children)
foreach (Job grandChild in GetChildJobs(child.JobId))
children.Append(grandChild);
return children;
}
/// <summary>
/// Modify Job with ID
/// </summary>
@ -314,7 +305,7 @@ public class JobController(PgsqlContext context, ILog Log) : Controller
ret.Enabled = modifyJobRecord.Enabled ?? ret.Enabled;
context.SaveChanges();
return new AcceptedResult(ret.JobId, ret);
return new AcceptedResult(ret.Key, ret);
}
catch (Exception e)
{

View File

@ -1,5 +1,4 @@
using API.Schema;
using API.Schema.Contexts;
using API.Schema.Contexts;
using API.Schema.LibraryConnectors;
using Asp.Versioning;
using log4net;
@ -14,7 +13,7 @@ namespace API.Controllers;
public class LibraryConnectorController(LibraryContext context, ILog Log) : Controller
{
/// <summary>
/// Gets all configured ToLibrary-Connectors
/// Gets all configured ToFileLibrary-Connectors
/// </summary>
/// <response code="200"></response>
[HttpGet]
@ -26,9 +25,9 @@ public class LibraryConnectorController(LibraryContext context, ILog Log) : Cont
}
/// <summary>
/// Returns ToLibrary-Connector with requested ID
/// Returns ToFileLibrary-Connector with requested ID
/// </summary>
/// <param name="LibraryControllerId">ToLibrary-Connector-ID</param>
/// <param name="LibraryControllerId">ToFileLibrary-Connector-ID</param>
/// <response code="200"></response>
/// <response code="404">Connector with ID not found.</response>
[HttpGet("{LibraryControllerId}")]
@ -45,9 +44,9 @@ public class LibraryConnectorController(LibraryContext context, ILog Log) : Cont
}
/// <summary>
/// Creates a new ToLibrary-Connector
/// Creates a new ToFileLibrary-Connector
/// </summary>
/// <param name="libraryConnector">ToLibrary-Connector</param>
/// <param name="libraryConnector">ToFileLibrary-Connector</param>
/// <response code="201"></response>
/// <response code="500">Error during Database Operation</response>
[HttpPut]
@ -69,9 +68,9 @@ public class LibraryConnectorController(LibraryContext context, ILog Log) : Cont
}
/// <summary>
/// Deletes the ToLibrary-Connector with the requested ID
/// Deletes the ToFileLibrary-Connector with the requested ID
/// </summary>
/// <param name="LibraryControllerId">ToLibrary-Connector-ID</param>
/// <param name="LibraryControllerId">ToFileLibrary-Connector-ID</param>
/// <response code="200"></response>
/// <response code="404">Connector with ID not found.</response>
/// <response code="500">Error during Database Operation</response>

View File

@ -14,18 +14,18 @@ namespace API.Controllers;
public class LocalLibrariesController(PgsqlContext context, ILog Log) : Controller
{
[HttpGet]
[ProducesResponseType<LocalLibrary[]>(Status200OK, "application/json")]
[ProducesResponseType<FileLibrary[]>(Status200OK, "application/json")]
public IActionResult GetLocalLibraries()
{
return Ok(context.LocalLibraries);
}
[HttpGet("{LibraryId}")]
[ProducesResponseType<LocalLibrary>(Status200OK, "application/json")]
[ProducesResponseType<FileLibrary>(Status200OK, "application/json")]
[ProducesResponseType(Status404NotFound)]
public IActionResult GetLocalLibrary(string LibraryId)
{
LocalLibrary? library = context.LocalLibraries.Find(LibraryId);
FileLibrary? library = context.LocalLibraries.Find(LibraryId);
if (library is null)
return NotFound();
return Ok(library);
@ -38,7 +38,7 @@ public class LocalLibrariesController(PgsqlContext context, ILog Log) : Controll
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public IActionResult UpdateLocalLibrary(string LibraryId, [FromBody]NewLibraryRecord record)
{
LocalLibrary? library = context.LocalLibraries.Find(LibraryId);
FileLibrary? library = context.LocalLibraries.Find(LibraryId);
if (library is null)
return NotFound();
if (record.Validate() == false)
@ -68,7 +68,7 @@ public class LocalLibrariesController(PgsqlContext context, ILog Log) : Controll
{
try
{
LocalLibrary? library = context.LocalLibraries.Find(LibraryId);
FileLibrary? library = context.LocalLibraries.Find(LibraryId);
if (library is null)
return NotFound();
@ -96,7 +96,7 @@ public class LocalLibrariesController(PgsqlContext context, ILog Log) : Controll
{
try
{
LocalLibrary? library = context.LocalLibraries.Find(LibraryId);
FileLibrary? library = context.LocalLibraries.Find(LibraryId);
if (library is null)
return NotFound();
@ -116,7 +116,7 @@ public class LocalLibrariesController(PgsqlContext context, ILog Log) : Controll
}
[HttpPut]
[ProducesResponseType<LocalLibrary>(Status200OK, "application/json")]
[ProducesResponseType<FileLibrary>(Status200OK, "application/json")]
[ProducesResponseType(Status400BadRequest)]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public IActionResult CreateNewLibrary([FromBody]NewLibraryRecord library)
@ -125,11 +125,11 @@ public class LocalLibrariesController(PgsqlContext context, ILog Log) : Controll
return BadRequest();
try
{
LocalLibrary newLibrary = new (library.path, library.name);
context.LocalLibraries.Add(newLibrary);
FileLibrary newFileLibrary = new (library.path, library.name);
context.LocalLibraries.Add(newFileLibrary);
context.SaveChanges();
return Ok(newLibrary);
return Ok(newFileLibrary);
}
catch (Exception e)
{
@ -147,7 +147,7 @@ public class LocalLibrariesController(PgsqlContext context, ILog Log) : Controll
try
{
LocalLibrary? library = context.LocalLibraries.Find(LibraryId);
FileLibrary? library = context.LocalLibraries.Find(LibraryId);
if (library is null)
return NotFound();
context.Remove(library);

View File

@ -4,6 +4,7 @@ using API.Schema.Jobs;
using Asp.Versioning;
using log4net;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Net.Http.Headers;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Formats.Jpeg;
@ -20,7 +21,7 @@ namespace API.Controllers;
public class MangaController(PgsqlContext context, ILog Log) : Controller
{
/// <summary>
/// Returns all cached Manga
/// Returns all cached Obj
/// </summary>
/// <response code="200"></response>
[HttpGet]
@ -32,24 +33,24 @@ public class MangaController(PgsqlContext context, ILog Log) : Controller
}
/// <summary>
/// Returns all cached Manga with IDs
/// Returns all cached Obj with IDs
/// </summary>
/// <param name="ids">Array of Manga-IDs</param>
/// <param name="ids">Array of Obj-IDs</param>
/// <response code="200"></response>
[HttpPost("WithIDs")]
[ProducesResponseType<Manga[]>(Status200OK, "application/json")]
public IActionResult GetManga([FromBody]string[] ids)
{
Manga[] ret = context.Mangas.Where(m => ids.Contains(m.MangaId)).ToArray();
Manga[] ret = context.Mangas.Where(m => ids.Contains(m.Key)).ToArray();
return Ok(ret);
}
/// <summary>
/// Return Manga with ID
/// Return Obj with ID
/// </summary>
/// <param name="MangaId">Manga-ID</param>
/// <param name="MangaId">Obj-ID</param>
/// <response code="200"></response>
/// <response code="404">Manga with ID not found</response>
/// <response code="404">Obj with ID not found</response>
[HttpGet("{MangaId}")]
[ProducesResponseType<Manga>(Status200OK, "application/json")]
[ProducesResponseType(Status404NotFound)]
@ -62,11 +63,11 @@ public class MangaController(PgsqlContext context, ILog Log) : Controller
}
/// <summary>
/// Delete Manga with ID
/// Delete Obj with ID
/// </summary>
/// <param name="MangaId">Manga-ID</param>
/// <param name="MangaId">Obj-ID</param>
/// <response code="200"></response>
/// <response code="404">Manga with ID not found</response>
/// <response code="404">Obj with ID not found</response>
/// <response code="500">Error during Database Operation</response>
[HttpDelete("{MangaId}")]
[ProducesResponseType(Status200OK)]
@ -91,16 +92,45 @@ public class MangaController(PgsqlContext context, ILog Log) : Controller
}
}
/// <summary>
/// Returns Cover of Manga
/// Merge two Manga into one. THIS IS NOT REVERSIBLE!
/// </summary>
/// <param name="MangaId">Manga-ID</param>
/// <response code="200"></response>
/// <response code="404">MangaId not found</response>
/// <response code="500">Error during Database Operation</response>
[HttpPatch("{MangaIdFrom}/MergeInto/{MangaIdTo}")]
[ProducesResponseType<byte[]>(Status200OK,"image/jpeg")]
[ProducesResponseType(Status404NotFound)]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public IActionResult MergeIntoManga(string MangaIdFrom, string MangaIdTo)
{
if(context.Mangas.Find(MangaIdFrom) is not { } from)
return NotFound(MangaIdFrom);
if(context.Mangas.Find(MangaIdTo) is not { } to)
return NotFound(MangaIdTo);
try
{
to.MergeFrom(from, context);
return Ok();
}
catch (DbUpdateException e)
{
Log.Error(e);
return StatusCode(500, e.Message);
}
}
/// <summary>
/// Returns Cover of Obj
/// </summary>
/// <param name="MangaId">Obj-ID</param>
/// <param name="width">If width is provided, height needs to also be provided</param>
/// <param name="height">If height is provided, width needs to also be provided</param>
/// <response code="200">JPEG Image</response>
/// <response code="204">Cover not loaded</response>
/// <response code="400">The formatting-request was invalid</response>
/// <response code="404">Manga with ID not found</response>
/// <response code="404">Obj with ID not found</response>
/// <response code="503">Retry later, downloading cover</response>
[HttpGet("{MangaId}/Cover")]
[ProducesResponseType<byte[]>(Status200OK,"image/jpeg")]
@ -115,7 +145,7 @@ public class MangaController(PgsqlContext context, ILog Log) : Controller
if (!System.IO.File.Exists(m.CoverFileNameInCache))
{
List<Job> coverDownloadJobs = context.Jobs.Where(j => j.JobType == JobType.DownloadMangaCoverJob).ToList();
List<Job> coverDownloadJobs = context.Jobs.Where(j => j.JobType == JobType.DownloadMangaCoverJob).Include(j => ((DownloadMangaCoverJob)j).Manga).ToList();
if (coverDownloadJobs.Any(j => j is DownloadMangaCoverJob dmc && dmc.MangaId == MangaId && dmc.state < JobState.Completed))
{
Response.Headers.Append("Retry-After", $"{TrangaSettings.startNewJobTimeoutMs * coverDownloadJobs.Count() * 2 / 1000:D}");
@ -146,11 +176,11 @@ public class MangaController(PgsqlContext context, ILog Log) : Controller
}
/// <summary>
/// Returns all Chapters of Manga
/// Returns all Chapters of Obj
/// </summary>
/// <param name="MangaId">Manga-ID</param>
/// <param name="MangaId">Obj-ID</param>
/// <response code="200"></response>
/// <response code="404">Manga with ID not found</response>
/// <response code="404">Obj with ID not found</response>
[HttpGet("{MangaId}/Chapters")]
[ProducesResponseType<Chapter[]>(Status200OK, "application/json")]
[ProducesResponseType(Status404NotFound)]
@ -164,12 +194,12 @@ public class MangaController(PgsqlContext context, ILog Log) : Controller
}
/// <summary>
/// Returns all downloaded Chapters for Manga with ID
/// Returns all downloaded Chapters for Obj with ID
/// </summary>
/// <param name="MangaId">Manga-ID</param>
/// <param name="MangaId">Obj-ID</param>
/// <response code="200"></response>
/// <response code="204">No available chapters</response>
/// <response code="404">Manga with ID not found.</response>
/// <response code="404">Obj with ID not found.</response>
[HttpGet("{MangaId}/Chapters/Downloaded")]
[ProducesResponseType<Chapter[]>(Status200OK, "application/json")]
[ProducesResponseType(Status204NoContent)]
@ -187,12 +217,12 @@ public class MangaController(PgsqlContext context, ILog Log) : Controller
}
/// <summary>
/// Returns all Chapters not downloaded for Manga with ID
/// Returns all Chapters not downloaded for Obj with ID
/// </summary>
/// <param name="MangaId">Manga-ID</param>
/// <param name="MangaId">Obj-ID</param>
/// <response code="200"></response>
/// <response code="204">No available chapters</response>
/// <response code="404">Manga with ID not found.</response>
/// <response code="404">Obj with ID not found.</response>
[HttpGet("{MangaId}/Chapters/NotDownloaded")]
[ProducesResponseType<Chapter[]>(Status200OK, "application/json")]
[ProducesResponseType(Status204NoContent)]
@ -210,12 +240,12 @@ public class MangaController(PgsqlContext context, ILog Log) : Controller
}
/// <summary>
/// Returns the latest Chapter of requested Manga available on Website
/// Returns the latest Chapter of requested Obj available on Website
/// </summary>
/// <param name="MangaId">Manga-ID</param>
/// <param name="MangaId">Obj-ID</param>
/// <response code="200"></response>
/// <response code="204">No available chapters</response>
/// <response code="404">Manga with ID not found.</response>
/// <response code="404">Obj with ID not found.</response>
/// <response code="500">Could not retrieve the maximum chapter-number</response>
/// <response code="503">Retry after timeout, updating value</response>
[HttpGet("{MangaId}/Chapter/LatestAvailable")]
@ -232,7 +262,7 @@ public class MangaController(PgsqlContext context, ILog Log) : Controller
List<Chapter> chapters = m.Chapters.ToList();
if (chapters.Count == 0)
{
List<Job> retrieveChapterJobs = context.Jobs.Where(j => j.JobType == JobType.RetrieveChaptersJob).ToList();
List<Job> retrieveChapterJobs = context.Jobs.Where(j => j.JobType == JobType.RetrieveChaptersJob).Include(j => ((RetrieveChaptersJob)j).Manga).ToList();
if (retrieveChapterJobs.Any(j => j is RetrieveChaptersJob rcj && rcj.MangaId == MangaId && rcj.state < JobState.Completed))
{
Response.Headers.Append("Retry-After", $"{TrangaSettings.startNewJobTimeoutMs * retrieveChapterJobs.Count() * 2 / 1000:D}");
@ -249,12 +279,12 @@ public class MangaController(PgsqlContext context, ILog Log) : Controller
}
/// <summary>
/// Returns the latest Chapter of requested Manga that is downloaded
/// Returns the latest Chapter of requested Obj that is downloaded
/// </summary>
/// <param name="MangaId">Manga-ID</param>
/// <param name="MangaId">Obj-ID</param>
/// <response code="200"></response>
/// <response code="204">No available chapters</response>
/// <response code="404">Manga with ID not found.</response>
/// <response code="404">Obj with ID not found.</response>
/// <response code="500">Could not retrieve the maximum chapter-number</response>
/// <response code="503">Retry after timeout, updating value</response>
[HttpGet("{MangaId}/Chapter/LatestDownloaded")]
@ -271,7 +301,7 @@ public class MangaController(PgsqlContext context, ILog Log) : Controller
List<Chapter> chapters = m.Chapters.ToList();
if (chapters.Count == 0)
{
List<Job> retrieveChapterJobs = context.Jobs.Where(j => j.JobType == JobType.RetrieveChaptersJob).ToList();
List<Job> retrieveChapterJobs = context.Jobs.Where(j => j.JobType == JobType.RetrieveChaptersJob).Include(j => ((RetrieveChaptersJob)j).Manga).ToList();
if (retrieveChapterJobs.Any(j => j is RetrieveChaptersJob rcj && rcj.MangaId == MangaId && rcj.state < JobState.Completed))
{
Response.Headers.Append("Retry-After", $"{TrangaSettings.startNewJobTimeoutMs * retrieveChapterJobs.Count() * 2 / 1000:D}");
@ -288,12 +318,12 @@ public class MangaController(PgsqlContext context, ILog Log) : Controller
}
/// <summary>
/// Configure the cut-off for Manga
/// Configure the cut-off for Obj
/// </summary>
/// <param name="MangaId">Manga-ID</param>
/// <param name="MangaId">Obj-ID</param>
/// <param name="chapterThreshold">Threshold (Chapter Number)</param>
/// <response code="200"></response>
/// <response code="404">Manga with ID not found.</response>
/// <response code="404">Obj with ID not found.</response>
/// <response code="500">Error during Database Operation</response>
[HttpPatch("{MangaId}/IgnoreChaptersBefore")]
[ProducesResponseType(Status200OK)]
@ -319,10 +349,10 @@ public class MangaController(PgsqlContext context, ILog Log) : Controller
}
/// <summary>
/// Move Manga to different ToLibrary
/// Move Obj to different ToFileLibrary
/// </summary>
/// <param name="MangaId">Manga-ID</param>
/// <param name="LibraryId">ToLibrary-Id</param>
/// <param name="MangaId">Obj-ID</param>
/// <param name="LibraryId">ToFileLibrary-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>

View File

@ -95,7 +95,7 @@ public class NotificationConnectorController(NotificationsContext context, ILog
NotificationConnector gotifyConnector = new NotificationConnector(TokenGen.CreateToken("Gotify"),
gotifyData.endpoint,
new Dictionary<string, string>() { { "X-Gotify-Key", gotifyData.appToken } },
new Dictionary<string, string>() { { "X-Gotify-IDOnConnector", gotifyData.appToken } },
"POST",
$"{{\"message\": \"%text\", \"title\": \"%title\", \"priority\": {gotifyData.priority}}}");
return CreateConnector(gotifyConnector);

View File

@ -69,18 +69,18 @@ public class QueryController(PgsqlContext context, ILog Log) : Controller
/// <response code="200"></response>
/// <response code="404">AltTitle with ID not found</response>
[HttpGet("AltTitle/{AltTitleId}")]
[ProducesResponseType<MangaAltTitle>(Status200OK, "application/json")]
[ProducesResponseType<AltTitle>(Status200OK, "application/json")]
[ProducesResponseType(Status404NotFound)]
public IActionResult GetAltTitle(string AltTitleId)
{
MangaAltTitle? ret = context.AltTitles.Find(AltTitleId);
AltTitle? ret = context.AltTitles.Find(AltTitleId);
if (ret is null)
return NotFound();
return Ok(ret);
}*/
/// <summary>
/// Returns all Manga with Tag
/// Returns all Obj with Tag
/// </summary>
/// <param name="Tag"></param>
/// <response code="200"></response>

View File

@ -1,7 +1,5 @@
using API.Schema;
using API.Schema.Contexts;
using API.Schema.Jobs;
using API.Schema.MangaConnectors;
using Asp.Versioning;
using log4net;
using Microsoft.AspNetCore.Mvc;
@ -18,7 +16,7 @@ namespace API.Controllers;
public class SearchController(PgsqlContext context, ILog Log) : Controller
{
/// <summary>
/// Initiate a search for a Manga on a specific Connector
/// Initiate a search for a Obj on a specific Connector
/// </summary>
/// <param name="MangaConnectorName"></param>
/// <param name="Query"></param>
@ -38,9 +36,9 @@ public class SearchController(PgsqlContext context, ILog Log) : Controller
else if (connector.Enabled is false)
return StatusCode(Status406NotAcceptable);
Manga[] mangas = connector.SearchManga(Query);
(Manga, MangaConnectorId<Manga>)[] mangas = connector.SearchManga(Query);
List<Manga> retMangas = new();
foreach (Manga manga in mangas)
foreach ((Manga manga, MangaConnectorId<Manga> mcId) manga in mangas)
{
try
{
@ -58,7 +56,7 @@ public class SearchController(PgsqlContext context, ILog Log) : Controller
}
/// <summary>
/// Search for a known Manga
/// Search for a known Obj
/// </summary>
/// <param name="Query"></param>
/// <response code="200"></response>
@ -73,12 +71,12 @@ public class SearchController(PgsqlContext context, ILog Log) : Controller
}
/// <summary>
/// Returns Manga from MangaConnector associated with URL
/// Returns Obj from MangaConnector associated with URL
/// </summary>
/// <param name="url">Manga-Page URL</param>
/// <param name="url">Obj-Page URL</param>
/// <response code="200"></response>
/// <response code="300">Multiple connectors found for URL</response>
/// <response code="404">Manga not found</response>
/// <response code="404">Obj not found</response>
/// <response code="500">Error during Database Operation</response>
[HttpPost("Url")]
[ProducesResponseType<Manga>(Status200OK, "application/json")]
@ -104,12 +102,13 @@ public class SearchController(PgsqlContext context, ILog Log) : Controller
}
}
private Manga? AddMangaToContext(Manga manga)
private Manga? AddMangaToContext((Manga, MangaConnectorId<Manga>) manga) => AddMangaToContext(manga.Item1, manga.Item2, context);
internal static Manga? AddMangaToContext(Manga addManga, MangaConnectorId<Manga> addMcId, PgsqlContext context)
{
context.Mangas.Load();
context.Authors.Load();
context.Tags.Load();
context.MangaConnectors.Load();
Manga manga = context.Mangas.Find(addManga.Key) ?? addManga;
MangaConnectorId<Manga> mcId = context.MangaConnectorToManga.Find(addMcId.Key) ?? addMcId;
mcId.Obj = manga;
IEnumerable<MangaTag> mergedTags = manga.MangaTags.Select(mt =>
{
@ -120,26 +119,19 @@ public class SearchController(PgsqlContext context, ILog Log) : Controller
IEnumerable<Author> mergedAuthors = manga.Authors.Select(ma =>
{
Author? inDb = context.Authors.Find(ma.AuthorId);
Author? inDb = context.Authors.Find(ma.Key);
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));
if(context.MangaConnectorToManga.Find(addMcId.Key) is null)
context.MangaConnectorToManga.Add(mcId);
context.SaveChanges();
}
catch (DbUpdateException e)
{
Log.Error(e);
return null;
}
return manga;

View File

@ -1,12 +1,10 @@
using System.Net.Http.Headers;
using API.MangaDownloadClients;
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;
using Newtonsoft.Json.Linq;
using static Microsoft.AspNetCore.Http.StatusCodes;
@ -210,14 +208,14 @@ public class SettingsController(PgsqlContext context, ILog Log) : Controller
/// </summary>
/// <remarks>
/// Placeholders:
/// %M Manga Name
/// %M Obj Name
/// %V Volume
/// %C Chapter
/// %T Title
/// %A Author (first in list)
/// %I Chapter Internal ID
/// %i Manga Internal ID
/// %Y Year (Manga)
/// %i Obj Internal ID
/// %Y Year (Obj)
///
/// ?_(...) replace _ with a value from above:
/// Everything inside the braces will only be added if the value of %_ is not null
@ -235,14 +233,14 @@ public class SettingsController(PgsqlContext context, ILog Log) : Controller
/// </summary>
/// <remarks>
/// Placeholders:
/// %M Manga Name
/// %M Obj Name
/// %V Volume
/// %C Chapter
/// %T Title
/// %A Author (first in list)
/// %I Chapter Internal ID
/// %i Manga Internal ID
/// %Y Year (Manga)
/// %i Obj Internal ID
/// %Y Year (Obj)
///
/// ?_(...) replace _ with a value from above:
/// Everything inside the braces will only be added if the value of %_ is not null
@ -271,7 +269,7 @@ public class SettingsController(PgsqlContext context, ILog Log) : Controller
}
/// <summary>
/// Creates a UpdateCoverJob for all Manga
/// Creates a UpdateCoverJob for all Obj
/// </summary>
/// <response code="200">Array of JobIds</response>
/// <response code="500">Error during Database Operation</response>
@ -285,7 +283,7 @@ public class SettingsController(PgsqlContext context, ILog Log) : Controller
Tranga.RemoveStaleFiles(context);
List<UpdateCoverJob> newJobs = context.Mangas.ToList().Select(m => new UpdateCoverJob(m, 0)).ToList();
context.Jobs.AddRange(newJobs);
return Ok(newJobs.Select(j => j.JobId));
return Ok(newJobs.Select(j => j.Key));
}
catch (Exception e)
{

View File

@ -43,7 +43,7 @@ internal class ChromiumDownloadClient : DownloadClient
{
Thread.Sleep(TimeSpan.FromHours(1));
Log.Debug("Removing stale pages");
foreach ((IPage? key, DateTime value) in _openPages.Where(kv => kv.Value.Subtract(DateTime.Now) > TimeSpan.FromHours(1)))
foreach ((IPage key, DateTime _) in _openPages.Where(kv => kv.Value.Subtract(DateTime.Now) > TimeSpan.FromHours(1)))
{
Log.Debug($"Closing {key.Url}");
key.CloseAsync().Wait();

View File

@ -172,7 +172,7 @@ public class FlareSolverrDownloadClient : DownloadClient
jsonString = pre.InnerText;
return true;
}
catch (JsonReaderException)
catch (Exception)
{
return false;
}

View File

@ -0,0 +1,795 @@
// <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("20250630182650_OofV2.1")]
partial class OofV21
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "9.0.5")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("API.Schema.Author", b =>
{
b.Property<string>("Key")
.HasColumnType("text");
b.Property<string>("AuthorName")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("character varying(128)");
b.HasKey("Key");
b.ToTable("Authors");
});
modelBuilder.Entity("API.Schema.Chapter", b =>
{
b.Property<string>("Key")
.HasColumnType("text");
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<int?>("VolumeNumber")
.HasColumnType("integer");
b.HasKey("Key");
b.HasIndex("ParentMangaId");
b.ToTable("Chapters");
});
modelBuilder.Entity("API.Schema.FileLibrary", b =>
{
b.Property<string>("Key")
.HasColumnType("text");
b.Property<string>("BasePath")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("LibraryName")
.IsRequired()
.HasMaxLength(512)
.HasColumnType("character varying(512)");
b.HasKey("Key");
b.ToTable("LocalLibraries");
});
modelBuilder.Entity("API.Schema.Jobs.Job", b =>
{
b.Property<string>("Key")
.HasColumnType("text");
b.Property<bool>("Enabled")
.HasColumnType("boolean");
b.Property<byte>("JobType")
.HasColumnType("smallint");
b.Property<DateTime>("LastExecution")
.HasColumnType("timestamp with time zone");
b.Property<string>("ParentJobId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<decimal>("RecurrenceMs")
.HasColumnType("numeric(20,0)");
b.Property<byte>("state")
.HasColumnType("smallint");
b.HasKey("Key");
b.HasIndex("ParentJobId");
b.ToTable("Jobs");
b.HasDiscriminator<byte>("JobType");
b.UseTphMappingStrategy();
});
modelBuilder.Entity("API.Schema.Manga", b =>
{
b.Property<string>("Key")
.HasColumnType("text");
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<float>("IgnoreChaptersBefore")
.HasColumnType("real");
b.Property<string>("LibraryId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(512)
.HasColumnType("character varying(512)");
b.Property<string>("OriginalLanguage")
.HasMaxLength(8)
.HasColumnType("character varying(8)");
b.Property<byte>("ReleaseStatus")
.HasColumnType("smallint");
b.Property<long?>("Year")
.HasColumnType("bigint");
b.HasKey("Key");
b.HasIndex("LibraryId");
b.ToTable("Mangas");
});
modelBuilder.Entity("API.Schema.MangaConnectorId<API.Schema.Chapter>", b =>
{
b.Property<string>("Key")
.HasColumnType("text");
b.Property<string>("IdOnConnectorSite")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("MangaConnectorName")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("character varying(32)");
b.Property<string>("ObjId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("WebsiteUrl")
.HasMaxLength(512)
.HasColumnType("character varying(512)");
b.HasKey("Key");
b.HasIndex("MangaConnectorName");
b.HasIndex("ObjId");
b.ToTable("MangaConnectorToChapter");
});
modelBuilder.Entity("API.Schema.MangaConnectorId<API.Schema.Manga>", b =>
{
b.Property<string>("Key")
.HasColumnType("text");
b.Property<string>("IdOnConnectorSite")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("MangaConnectorName")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("character varying(32)");
b.Property<string>("ObjId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("WebsiteUrl")
.HasMaxLength(512)
.HasColumnType("character varying(512)");
b.HasKey("Key");
b.HasIndex("MangaConnectorName");
b.HasIndex("ObjId");
b.ToTable("MangaConnectorToManga");
});
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("text");
b.Property<string>("MangaIds")
.HasColumnType("text");
b.HasKey("AuthorIds", "MangaIds");
b.HasIndex("MangaIds");
b.ToTable("AuthorToManga");
});
modelBuilder.Entity("JobJob", b =>
{
b.Property<string>("DependsOnJobsKey")
.HasColumnType("text");
b.Property<string>("JobKey")
.HasColumnType("text");
b.HasKey("DependsOnJobsKey", "JobKey");
b.HasIndex("JobKey");
b.ToTable("JobJob");
});
modelBuilder.Entity("MangaTagToManga", b =>
{
b.Property<string>("MangaTagIds")
.HasColumnType("character varying(64)");
b.Property<string>("MangaIds")
.HasColumnType("text");
b.HasKey("MangaTagIds", "MangaIds");
b.HasIndex("MangaIds");
b.ToTable("MangaTagToManga");
});
modelBuilder.Entity("API.Schema.Jobs.DownloadAvailableChaptersJob", b =>
{
b.HasBaseType("API.Schema.Jobs.Job");
b.Property<string>("MangaId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.HasIndex("MangaId");
b.ToTable("Jobs", t =>
{
t.Property("MangaId")
.HasColumnName("DownloadAvailableChaptersJob_MangaId");
});
b.HasDiscriminator().HasValue((byte)1);
});
modelBuilder.Entity("API.Schema.Jobs.DownloadMangaCoverJob", b =>
{
b.HasBaseType("API.Schema.Jobs.Job");
b.Property<string>("MangaId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.HasIndex("MangaId");
b.HasDiscriminator().HasValue((byte)4);
});
modelBuilder.Entity("API.Schema.Jobs.DownloadSingleChapterJob", b =>
{
b.HasBaseType("API.Schema.Jobs.Job");
b.Property<string>("ChapterId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.HasIndex("ChapterId");
b.HasDiscriminator().HasValue((byte)0);
});
modelBuilder.Entity("API.Schema.Jobs.MoveFileOrFolderJob", b =>
{
b.HasBaseType("API.Schema.Jobs.Job");
b.Property<string>("FromLocation")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("ToLocation")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.HasDiscriminator().HasValue((byte)3);
});
modelBuilder.Entity("API.Schema.Jobs.MoveMangaLibraryJob", b =>
{
b.HasBaseType("API.Schema.Jobs.Job");
b.Property<string>("MangaId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("ToLibraryId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.HasIndex("MangaId");
b.HasIndex("ToLibraryId");
b.ToTable("Jobs", t =>
{
t.Property("MangaId")
.HasColumnName("MoveMangaLibraryJob_MangaId");
});
b.HasDiscriminator().HasValue((byte)7);
});
modelBuilder.Entity("API.Schema.Jobs.RetrieveChaptersJob", b =>
{
b.HasBaseType("API.Schema.Jobs.Job");
b.Property<string>("Language")
.IsRequired()
.HasMaxLength(8)
.HasColumnType("character varying(8)");
b.Property<string>("MangaId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.HasIndex("MangaId");
b.ToTable("Jobs", t =>
{
t.Property("MangaId")
.HasColumnName("RetrieveChaptersJob_MangaId");
});
b.HasDiscriminator().HasValue((byte)5);
});
modelBuilder.Entity("API.Schema.Jobs.UpdateChaptersDownloadedJob", b =>
{
b.HasBaseType("API.Schema.Jobs.Job");
b.Property<string>("MangaId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.HasIndex("MangaId");
b.ToTable("Jobs", t =>
{
t.Property("MangaId")
.HasColumnName("UpdateChaptersDownloadedJob_MangaId");
});
b.HasDiscriminator().HasValue((byte)6);
});
modelBuilder.Entity("API.Schema.Jobs.UpdateCoverJob", b =>
{
b.HasBaseType("API.Schema.Jobs.Job");
b.Property<string>("MangaId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.HasIndex("MangaId");
b.ToTable("Jobs", t =>
{
t.Property("MangaId")
.HasColumnName("UpdateCoverJob_MangaId");
});
b.HasDiscriminator().HasValue((byte)9);
});
modelBuilder.Entity("API.Schema.MangaConnectors.ComickIo", b =>
{
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
b.HasDiscriminator().HasValue("ComickIo");
});
modelBuilder.Entity("API.Schema.MangaConnectors.Global", b =>
{
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
b.HasDiscriminator().HasValue("Global");
});
modelBuilder.Entity("API.Schema.MangaConnectors.MangaDex", b =>
{
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
b.HasDiscriminator().HasValue("MangaDex");
});
modelBuilder.Entity("API.Schema.Chapter", b =>
{
b.HasOne("API.Schema.Manga", "ParentManga")
.WithMany("Chapters")
.HasForeignKey("ParentMangaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("ParentManga");
});
modelBuilder.Entity("API.Schema.Jobs.Job", b =>
{
b.HasOne("API.Schema.Jobs.Job", "ParentJob")
.WithMany()
.HasForeignKey("ParentJobId")
.OnDelete(DeleteBehavior.Cascade);
b.Navigation("ParentJob");
});
modelBuilder.Entity("API.Schema.Manga", b =>
{
b.HasOne("API.Schema.FileLibrary", "Library")
.WithMany()
.HasForeignKey("LibraryId")
.OnDelete(DeleteBehavior.SetNull);
b.OwnsMany("API.Schema.AltTitle", "AltTitles", b1 =>
{
b1.Property<string>("Key")
.HasColumnType("text");
b1.Property<string>("Language")
.IsRequired()
.HasMaxLength(8)
.HasColumnType("character varying(8)");
b1.Property<string>("MangaKey")
.IsRequired()
.HasColumnType("text");
b1.Property<string>("Title")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b1.HasKey("Key");
b1.HasIndex("MangaKey");
b1.ToTable("AltTitle");
b1.WithOwner()
.HasForeignKey("MangaKey");
});
b.OwnsMany("API.Schema.Link", "Links", b1 =>
{
b1.Property<string>("Key")
.HasColumnType("text");
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>("MangaKey")
.IsRequired()
.HasColumnType("text");
b1.HasKey("Key");
b1.HasIndex("MangaKey");
b1.ToTable("Link");
b1.WithOwner()
.HasForeignKey("MangaKey");
});
b.Navigation("AltTitles");
b.Navigation("Library");
b.Navigation("Links");
});
modelBuilder.Entity("API.Schema.MangaConnectorId<API.Schema.Chapter>", b =>
{
b.HasOne("API.Schema.MangaConnectors.MangaConnector", "MangaConnector")
.WithMany()
.HasForeignKey("MangaConnectorName")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("API.Schema.Chapter", "Obj")
.WithMany("MangaConnectorIds")
.HasForeignKey("ObjId")
.OnDelete(DeleteBehavior.NoAction)
.IsRequired();
b.Navigation("MangaConnector");
b.Navigation("Obj");
});
modelBuilder.Entity("API.Schema.MangaConnectorId<API.Schema.Manga>", b =>
{
b.HasOne("API.Schema.MangaConnectors.MangaConnector", "MangaConnector")
.WithMany()
.HasForeignKey("MangaConnectorName")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("API.Schema.Manga", "Obj")
.WithMany("MangaConnectorIds")
.HasForeignKey("ObjId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("MangaConnector");
b.Navigation("Obj");
});
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("DependsOnJobsKey")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("API.Schema.Jobs.Job", null)
.WithMany()
.HasForeignKey("JobKey")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("MangaTagToManga", b =>
{
b.HasOne("API.Schema.Manga", null)
.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.FileLibrary", "ToFileLibrary")
.WithMany()
.HasForeignKey("ToLibraryId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Manga");
b.Navigation("ToFileLibrary");
});
modelBuilder.Entity("API.Schema.Jobs.RetrieveChaptersJob", b =>
{
b.HasOne("API.Schema.Manga", "Manga")
.WithMany()
.HasForeignKey("MangaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Manga");
});
modelBuilder.Entity("API.Schema.Jobs.UpdateChaptersDownloadedJob", b =>
{
b.HasOne("API.Schema.Manga", "Manga")
.WithMany()
.HasForeignKey("MangaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Manga");
});
modelBuilder.Entity("API.Schema.Jobs.UpdateCoverJob", b =>
{
b.HasOne("API.Schema.Manga", "Manga")
.WithMany()
.HasForeignKey("MangaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Manga");
});
modelBuilder.Entity("API.Schema.Chapter", b =>
{
b.Navigation("MangaConnectorIds");
});
modelBuilder.Entity("API.Schema.Manga", b =>
{
b.Navigation("Chapters");
b.Navigation("MangaConnectorIds");
});
#pragma warning restore 612, 618
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -24,25 +24,23 @@ namespace API.Migrations.pgsql
modelBuilder.Entity("API.Schema.Author", b =>
{
b.Property<string>("AuthorId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("Key")
.HasColumnType("text");
b.Property<string>("AuthorName")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("character varying(128)");
b.HasKey("AuthorId");
b.HasKey("Key");
b.ToTable("Authors");
});
modelBuilder.Entity("API.Schema.Chapter", b =>
{
b.Property<string>("ChapterId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("Key")
.HasColumnType("text");
b.Property<string>("ChapterNumber")
.IsRequired()
@ -57,38 +55,49 @@ namespace API.Migrations.pgsql
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("IdOnConnectorSite")
.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.HasKey("Key");
b.HasIndex("ParentMangaId");
b.ToTable("Chapters");
});
modelBuilder.Entity("API.Schema.FileLibrary", b =>
{
b.Property<string>("Key")
.HasColumnType("text");
b.Property<string>("BasePath")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("LibraryName")
.IsRequired()
.HasMaxLength(512)
.HasColumnType("character varying(512)");
b.HasKey("Key");
b.ToTable("LocalLibraries");
});
modelBuilder.Entity("API.Schema.Jobs.Job", b =>
{
b.Property<string>("JobId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("Key")
.HasColumnType("text");
b.Property<bool>("Enabled")
.HasColumnType("boolean");
@ -109,7 +118,7 @@ namespace API.Migrations.pgsql
b.Property<byte>("state")
.HasColumnType("smallint");
b.HasKey("JobId");
b.HasKey("Key");
b.HasIndex("ParentJobId");
@ -120,32 +129,10 @@ namespace API.Migrations.pgsql
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>("Key")
.HasColumnType("text");
b.Property<string>("CoverFileNameInCache")
.HasMaxLength(512)
@ -165,11 +152,6 @@ namespace API.Migrations.pgsql
.HasMaxLength(1024)
.HasColumnType("character varying(1024)");
b.Property<string>("IdOnConnectorSite")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<float>("IgnoreChaptersBefore")
.HasColumnType("real");
@ -177,11 +159,6 @@ namespace API.Migrations.pgsql
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("MangaConnectorName")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("character varying(32)");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(512)
@ -194,21 +171,80 @@ namespace API.Migrations.pgsql
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.HasKey("Key");
b.HasIndex("LibraryId");
b.ToTable("Mangas");
});
modelBuilder.Entity("API.Schema.MangaConnectorId<API.Schema.Chapter>", b =>
{
b.Property<string>("Key")
.HasColumnType("text");
b.Property<string>("IdOnConnectorSite")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("MangaConnectorName")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("character varying(32)");
b.Property<string>("ObjId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("WebsiteUrl")
.HasMaxLength(512)
.HasColumnType("character varying(512)");
b.HasKey("Key");
b.HasIndex("MangaConnectorName");
b.ToTable("Mangas");
b.HasIndex("ObjId");
b.ToTable("MangaConnectorToChapter");
});
modelBuilder.Entity("API.Schema.MangaConnectorId<API.Schema.Manga>", b =>
{
b.Property<string>("Key")
.HasColumnType("text");
b.Property<string>("IdOnConnectorSite")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("MangaConnectorName")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("character varying(32)");
b.Property<string>("ObjId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("WebsiteUrl")
.HasMaxLength(512)
.HasColumnType("character varying(512)");
b.HasKey("Key");
b.HasIndex("MangaConnectorName");
b.HasIndex("ObjId");
b.ToTable("MangaConnectorToManga");
});
modelBuilder.Entity("API.Schema.MangaConnectors.MangaConnector", b =>
@ -296,10 +332,10 @@ namespace API.Migrations.pgsql
modelBuilder.Entity("AuthorToManga", b =>
{
b.Property<string>("AuthorIds")
.HasColumnType("character varying(64)");
.HasColumnType("text");
b.Property<string>("MangaIds")
.HasColumnType("character varying(64)");
.HasColumnType("text");
b.HasKey("AuthorIds", "MangaIds");
@ -310,15 +346,15 @@ namespace API.Migrations.pgsql
modelBuilder.Entity("JobJob", b =>
{
b.Property<string>("DependsOnJobsJobId")
.HasColumnType("character varying(64)");
b.Property<string>("DependsOnJobsKey")
.HasColumnType("text");
b.Property<string>("JobId")
.HasColumnType("character varying(64)");
b.Property<string>("JobKey")
.HasColumnType("text");
b.HasKey("DependsOnJobsJobId", "JobId");
b.HasKey("DependsOnJobsKey", "JobKey");
b.HasIndex("JobId");
b.HasIndex("JobKey");
b.ToTable("JobJob");
});
@ -329,7 +365,7 @@ namespace API.Migrations.pgsql
.HasColumnType("character varying(64)");
b.Property<string>("MangaIds")
.HasColumnType("character varying(64)");
.HasColumnType("text");
b.HasKey("MangaTagIds", "MangaIds");
@ -546,22 +582,44 @@ namespace API.Migrations.pgsql
modelBuilder.Entity("API.Schema.Manga", b =>
{
b.HasOne("API.Schema.LocalLibrary", "Library")
b.HasOne("API.Schema.FileLibrary", "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.AltTitle", "AltTitles", b1 =>
{
b1.Property<string>("Key")
.HasColumnType("text");
b1.Property<string>("Language")
.IsRequired()
.HasMaxLength(8)
.HasColumnType("character varying(8)");
b1.Property<string>("MangaKey")
.IsRequired()
.HasColumnType("text");
b1.Property<string>("Title")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b1.HasKey("Key");
b1.HasIndex("MangaKey");
b1.ToTable("AltTitle");
b1.WithOwner()
.HasForeignKey("MangaKey");
});
b.OwnsMany("API.Schema.Link", "Links", b1 =>
{
b1.Property<string>("LinkId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b1.Property<string>("Key")
.HasColumnType("text");
b1.Property<string>("LinkProvider")
.IsRequired()
@ -573,48 +631,18 @@ namespace API.Migrations.pgsql
.HasMaxLength(2048)
.HasColumnType("character varying(2048)");
b1.Property<string>("MangaId")
b1.Property<string>("MangaKey")
.IsRequired()
.HasColumnType("character varying(64)");
.HasColumnType("text");
b1.HasKey("LinkId");
b1.HasKey("Key");
b1.HasIndex("MangaId");
b1.HasIndex("MangaKey");
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");
.HasForeignKey("MangaKey");
});
b.Navigation("AltTitles");
@ -622,8 +650,44 @@ namespace API.Migrations.pgsql
b.Navigation("Library");
b.Navigation("Links");
});
modelBuilder.Entity("API.Schema.MangaConnectorId<API.Schema.Chapter>", b =>
{
b.HasOne("API.Schema.MangaConnectors.MangaConnector", "MangaConnector")
.WithMany()
.HasForeignKey("MangaConnectorName")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("API.Schema.Chapter", "Obj")
.WithMany("MangaConnectorIds")
.HasForeignKey("ObjId")
.OnDelete(DeleteBehavior.NoAction)
.IsRequired();
b.Navigation("MangaConnector");
b.Navigation("Obj");
});
modelBuilder.Entity("API.Schema.MangaConnectorId<API.Schema.Manga>", b =>
{
b.HasOne("API.Schema.MangaConnectors.MangaConnector", "MangaConnector")
.WithMany()
.HasForeignKey("MangaConnectorName")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("API.Schema.Manga", "Obj")
.WithMany("MangaConnectorIds")
.HasForeignKey("ObjId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("MangaConnector");
b.Navigation("Obj");
});
modelBuilder.Entity("API.Schema.MetadataFetchers.MetadataEntry", b =>
@ -664,13 +728,13 @@ namespace API.Migrations.pgsql
{
b.HasOne("API.Schema.Jobs.Job", null)
.WithMany()
.HasForeignKey("DependsOnJobsJobId")
.HasForeignKey("DependsOnJobsKey")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("API.Schema.Jobs.Job", null)
.WithMany()
.HasForeignKey("JobId")
.HasForeignKey("JobKey")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
@ -731,7 +795,7 @@ namespace API.Migrations.pgsql
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("API.Schema.LocalLibrary", "ToLibrary")
b.HasOne("API.Schema.FileLibrary", "ToFileLibrary")
.WithMany()
.HasForeignKey("ToLibraryId")
.OnDelete(DeleteBehavior.Cascade)
@ -739,7 +803,7 @@ namespace API.Migrations.pgsql
b.Navigation("Manga");
b.Navigation("ToLibrary");
b.Navigation("ToFileLibrary");
});
modelBuilder.Entity("API.Schema.Jobs.RetrieveChaptersJob", b =>
@ -775,9 +839,16 @@ namespace API.Migrations.pgsql
b.Navigation("Manga");
});
modelBuilder.Entity("API.Schema.Chapter", b =>
{
b.Navigation("MangaConnectorIds");
});
modelBuilder.Entity("API.Schema.Manga", b =>
{
b.Navigation("Chapters");
b.Navigation("MangaConnectorIds");
});
#pragma warning restore 612, 618
}

View File

@ -1,5 +1,6 @@
using System.Reflection;
using API;
using API.Controllers;
using API.Schema;
using API.Schema.Contexts;
using API.Schema.Jobs;
@ -108,7 +109,27 @@ app.UseMiddleware<RequestTimeMiddleware>();
using (IServiceScope scope = app.Services.CreateScope())
{
PgsqlContext context = scope.ServiceProvider.GetRequiredService<PgsqlContext>();
//TODO Remove after migrations complete
if (context.Database.GetMigrations().Contains("20250630182650_OofV2.1") == false)
{
IQueryable<(string, string)> mangas = context.Database.SqlQuery<(string, string)>($"SELECT MangaConnectorName, IdOnConnectorSite as ID FROM Mangas");
context.Database.Migrate();
foreach ((string mangaConnectorName, string idOnConnectorSite) manga in mangas)
{
if(context.MangaConnectors.Find(manga.mangaConnectorName) is not { } mangaConnector)
continue;
if(mangaConnector.GetMangaFromId(manga.idOnConnectorSite) is not { } result)
continue;
if (SearchController.AddMangaToContext(result.Item1, result.Item2, context) is { } added)
{
RetrieveChaptersJob retrieveChaptersJob = new (added, "en", 0);
UpdateChaptersDownloadedJob update = new(added, 0, null, [retrieveChaptersJob]);
context.Jobs.AddRange([retrieveChaptersJob, update]);
}
}
} else
context.Database.Migrate();
MangaConnector[] connectors =
[
@ -119,7 +140,7 @@ using (IServiceScope scope = app.Services.CreateScope())
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.LocalLibraries.Add(new FileLibrary(TrangaSettings.downloadLocation, "Default FileLibrary"));
context.Jobs.AddRange(context.Jobs.Where(j => j.JobType == JobType.DownloadAvailableChaptersJob)
.Include(downloadAvailableChaptersJob => ((DownloadAvailableChaptersJob)downloadAvailableChaptersJob).Manga)

17
API/Schema/AltTitle.cs Normal file
View File

@ -0,0 +1,17 @@
using System.ComponentModel.DataAnnotations;
using Microsoft.EntityFrameworkCore;
namespace API.Schema;
[PrimaryKey("Key")]
public class AltTitle(string language, string title) : Identifiable(TokenGen.CreateToken("AltTitle"))
{
[StringLength(8)]
[Required]
public string Language { get; init; } = language;
[StringLength(256)]
[Required]
public string Title { get; init; } = title;
public override string ToString() => $"{base.ToString()} {Language} {Title}";
}

View File

@ -3,18 +3,12 @@ using Microsoft.EntityFrameworkCore;
namespace API.Schema;
[PrimaryKey("AuthorId")]
public class Author(string authorName)
[PrimaryKey("Key")]
public class Author(string authorName) : Identifiable(TokenGen.CreateToken(typeof(Author), authorName))
{
[StringLength(64)]
[Required]
public string AuthorId { get; init; } = TokenGen.CreateToken(typeof(Author), authorName);
[StringLength(128)]
[Required]
public string AuthorName { get; init; } = authorName;
public override string ToString()
{
return $"{AuthorId} {AuthorName}";
}
public override string ToString() => $"{base.ToString()} {AuthorName}";
}

View File

@ -4,24 +4,43 @@ using System.Text;
using System.Text.RegularExpressions;
using System.Xml.Linq;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Newtonsoft.Json;
namespace API.Schema;
[PrimaryKey("ChapterId")]
public class Chapter : IComparable<Chapter>
[PrimaryKey("Key")]
public class Chapter : Identifiable, IComparable<Chapter>
{
[StringLength(64)] [Required] public string ChapterId { get; init; }
[StringLength(64)] [Required] public string ParentMangaId { get; init; } = null!;
private Manga? _parentManga;
[StringLength(256)]public string? IdOnConnectorSite { get; init; }
public string ParentMangaId { get; init; }
[JsonIgnore] public Manga ParentManga { get; init; } = null!;
[JsonIgnore]
public Manga ParentManga
{
get => _lazyLoader.Load(this, ref _parentManga) ?? throw new InvalidOperationException();
init
{
ParentMangaId = value.Key;
_parentManga = value;
}
}
[NotMapped]
public Dictionary<string, string> IdsOnMangaConnectors =>
MangaConnectorIds.ToDictionary(id => id.MangaConnectorName, id => id.IdOnConnectorSite);
private ICollection<MangaConnectorId<Chapter>>? _mangaConnectorIds;
[JsonIgnore]
public ICollection<MangaConnectorId<Chapter>> MangaConnectorIds
{
get => _lazyLoader.Load(this, ref _mangaConnectorIds) ?? throw new InvalidOperationException();
init => _mangaConnectorIds = value;
}
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; }
@ -29,15 +48,16 @@ public class Chapter : IComparable<Chapter>
[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? idOnConnectorSite = null, string? title = null)
private readonly ILazyLoader _lazyLoader = null!;
public Chapter(Manga parentManga, string chapterNumber,
int? volumeNumber, string? title = null)
: base(TokenGen.CreateToken(typeof(Chapter), parentManga.Key, chapterNumber))
{
this.ChapterId = TokenGen.CreateToken(typeof(Chapter), parentManga.MangaId, chapterNumber);
this.IdOnConnectorSite = idOnConnectorSite;
this.ParentMangaId = parentManga.MangaId;
this.ParentManga = parentManga;
this.MangaConnectorIds = [];
this.VolumeNumber = volumeNumber;
this.ChapterNumber = chapterNumber;
this.Url = url;
this.Title = title;
this.FileName = GetArchiveFilePath();
this.Downloaded = false;
@ -46,14 +66,12 @@ public class Chapter : IComparable<Chapter>
/// <summary>
/// EF ONLY!!!
/// </summary>
internal Chapter(string chapterId, string parentMangaId, int? volumeNumber, string chapterNumber, string url, string? idOnConnectorSite, string? title, string fileName, bool downloaded)
internal Chapter(ILazyLoader lazyLoader, string key, int? volumeNumber, string chapterNumber, string? title, string fileName, bool downloaded)
: base(key)
{
this.ChapterId = chapterId;
this.IdOnConnectorSite = idOnConnectorSite;
this.ParentMangaId = parentMangaId;
this._lazyLoader = lazyLoader;
this.VolumeNumber = volumeNumber;
this.ChapterNumber = chapterNumber;
this.Url = url;
this.Title = title;
this.FileName = fileName;
this.Downloaded = downloaded;
@ -78,14 +96,14 @@ public class Chapter : IComparable<Chapter>
public bool CheckDownloaded() => File.Exists(FullArchiveFilePath);
/// Placeholders:
/// %M Manga Name
/// %M Obj Name
/// %V Volume
/// %C Chapter
/// %T Title
/// %A Author (first in list)
/// %I Chapter Internal ID
/// %i Manga Internal ID
/// %Y Year (Manga)
/// %i Obj Internal ID
/// %Y Year (Obj)
private static readonly Regex NullableRex = new(@"\?([a-zA-Z])\(([^\)]*)\)|(.+?)");
private static readonly Regex ReplaceRexx = new(@"%([a-zA-Z])|(.+?)");
private string GetArchiveFilePath()
@ -108,8 +126,6 @@ public class Chapter : IComparable<Chapter>
'C' => ChapterNumber is null,
'T' => Title is null,
'A' => ParentManga?.Authors?.FirstOrDefault()?.AuthorName is null,
'I' => ChapterId is null,
'i' => ParentManga?.MangaId is null,
'Y' => ParentManga?.Year is null,
_ => true
};
@ -136,8 +152,6 @@ public class Chapter : IComparable<Chapter>
'C' => ChapterNumber,
'T' => Title,
'A' => ParentManga?.Authors?.FirstOrDefault()?.AuthorName,
'I' => ChapterId,
'i' => ParentManga?.MangaId,
'Y' => ParentManga?.Year.ToString(),
_ => null
};
@ -192,8 +206,5 @@ public class Chapter : IComparable<Chapter>
return comicInfo.ToString();
}
public override string ToString()
{
return $"{ChapterId} Vol.{VolumeNumber} Ch.{ChapterNumber} - {Title}";
}
public override string ToString() => $"{base.ToString()} Vol.{VolumeNumber} Ch.{ChapterNumber} - {Title}";
}

View File

@ -12,10 +12,12 @@ public class PgsqlContext(DbContextOptions<PgsqlContext> options) : DbContext(op
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<FileLibrary> LocalLibraries { get; set; }
public DbSet<Chapter> Chapters { get; set; }
public DbSet<Author> Authors { get; set; }
public DbSet<MangaTag> Tags { get; set; }
public DbSet<MangaConnectorId<Manga>> MangaConnectorToManga { get; set; }
public DbSet<MangaConnectorId<Chapter>> MangaConnectorToChapter { get; set; }
public DbSet<MetadataEntry> MetadataEntries { get; set; }
private ILog Log => LogManager.GetLogger(GetType());
@ -43,7 +45,6 @@ public class PgsqlContext(DbContextOptions<PgsqlContext> options) : DbContext(op
.HasValue<UpdateCoverJob>(JobType.UpdateCoverJob)
.HasValue<UpdateChaptersDownloadedJob>(JobType.UpdateChaptersDownloadedJob);
//Job specification
modelBuilder.Entity<DownloadAvailableChaptersJob>()
.HasOne<Manga>(j => j.Manga)
.WithMany()
@ -77,12 +78,12 @@ public class PgsqlContext(DbContextOptions<PgsqlContext> options) : DbContext(op
.Navigation(j => j.Manga)
.EnableLazyLoading();
modelBuilder.Entity<MoveMangaLibraryJob>()
.HasOne<LocalLibrary>(j => j.ToLibrary)
.HasOne<FileLibrary>(j => j.ToFileLibrary)
.WithMany()
.HasForeignKey(j => j.ToLibraryId)
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<MoveMangaLibraryJob>()
.Navigation(j => j.ToLibrary)
.Navigation(j => j.ToFileLibrary)
.EnableLazyLoading();
modelBuilder.Entity<RetrieveChaptersJob>()
.HasOne<Manga>(j => j.Manga)
@ -105,15 +106,17 @@ public class PgsqlContext(DbContextOptions<PgsqlContext> options) : DbContext(op
modelBuilder.Entity<Job>()
.HasOne<Job>(childJob => childJob.ParentJob)
.WithMany()
.HasForeignKey(childjob => childjob.ParentJobId)
.HasForeignKey(childJob => childJob.ParentJobId)
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<Job>()
.Navigation(childJob => childJob.ParentJob)
.EnableLazyLoading();
//Job might be dependent on other Jobs
modelBuilder.Entity<Job>()
.HasMany<Job>(root => root.DependsOnJobs)
.WithMany();
modelBuilder.Entity<Job>()
.Navigation(j => j.DependsOnJobs)
.AutoInclude(false)
.EnableLazyLoading();
//MangaConnector Types
@ -122,15 +125,6 @@ public class PgsqlContext(DbContextOptions<PgsqlContext> options) : DbContext(op
.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)
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<Manga>()
.Navigation(m => m.MangaConnector)
.AutoInclude();
//Manga has many Chapters
modelBuilder.Entity<Manga>()
@ -138,16 +132,29 @@ public class PgsqlContext(DbContextOptions<PgsqlContext> options) : DbContext(op
.WithOne(c => c.ParentManga)
.HasForeignKey(c => c.ParentMangaId)
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<Chapter>()
.Navigation(c => c.ParentManga)
.AutoInclude();
modelBuilder.Entity<Manga>()
.Navigation(m => m.Chapters)
.AutoInclude(false)
.EnableLazyLoading();
modelBuilder.Entity<Chapter>()
.Navigation(c => c.ParentManga)
.EnableLazyLoading();
//Chapter has MangaConnectorIds
modelBuilder.Entity<Chapter>()
.HasMany<MangaConnectorId<Chapter>>(c => c.MangaConnectorIds)
.WithOne(id => id.Obj)
.HasForeignKey(id => id.ObjId)
.OnDelete(DeleteBehavior.NoAction);
modelBuilder.Entity<MangaConnectorId<Chapter>>()
.HasOne<MangaConnector>(id => id.MangaConnector)
.WithMany()
.HasForeignKey(id => id.MangaConnectorName)
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<MangaConnectorId<Chapter>>()
.Navigation(entry => entry.MangaConnector)
.EnableLazyLoading();
//Manga owns MangaAltTitles
modelBuilder.Entity<Manga>()
.OwnsMany<MangaAltTitle>(m => m.AltTitles)
.OwnsMany<AltTitle>(m => m.AltTitles)
.WithOwner();
modelBuilder.Entity<Manga>()
.Navigation(m => m.AltTitles)
@ -159,40 +166,58 @@ public class PgsqlContext(DbContextOptions<PgsqlContext> options) : DbContext(op
modelBuilder.Entity<Manga>()
.Navigation(m => m.Links)
.AutoInclude();
//Manga has many Tags associated with many Manga
//Manga has many Tags associated with many Obj
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)),
r => r.HasOne(typeof(Manga)).WithMany().HasForeignKey("MangaIds").HasPrincipalKey(nameof(Manga.Key)),
j => j.HasKey("MangaTagIds", "MangaIds")
);
modelBuilder.Entity<Manga>()
.Navigation(m => m.MangaTags)
.AutoInclude();
//Manga has many Authors associated with many Manga
//Manga has many Authors associated with many Obj
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)),
l=> l.HasOne(typeof(Author)).WithMany().HasForeignKey("AuthorIds").HasPrincipalKey(nameof(Author.Key)),
r => r.HasOne(typeof(Manga)).WithMany().HasForeignKey("MangaIds").HasPrincipalKey(nameof(Manga.Key)),
j => j.HasKey("AuthorIds", "MangaIds")
);
modelBuilder.Entity<Manga>()
.Navigation(m => m.Authors)
.AutoInclude();
//Manga has many MangaIds
modelBuilder.Entity<Manga>()
.HasMany<MangaConnectorId<Manga>>(m => m.MangaConnectorIds)
.WithOne(id => id.Obj)
.HasForeignKey(id => id.ObjId)
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<Manga>()
.Navigation(m => m.MangaConnectorIds)
.EnableLazyLoading();
modelBuilder.Entity<MangaConnectorId<Manga>>()
.HasOne<MangaConnector>(id => id.MangaConnector)
.WithMany()
.HasForeignKey(id => id.MangaConnectorName)
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<MangaConnectorId<Manga>>()
.Navigation(entry => entry.MangaConnector)
.EnableLazyLoading();
//LocalLibrary has many Mangas
modelBuilder.Entity<LocalLibrary>()
//FileLibrary has many Mangas
modelBuilder.Entity<FileLibrary>()
.HasMany<Manga>()
.WithOne(m => m.Library)
.HasForeignKey(m => m.LibraryId)
.OnDelete(DeleteBehavior.SetNull);
modelBuilder.Entity<Manga>()
.Navigation(m => m.Library)
.AutoInclude();
.EnableLazyLoading();
modelBuilder.Entity<MetadataFetcher>()
.HasDiscriminator<string>(nameof(MetadataEntry))

19
API/Schema/FileLibrary.cs Normal file
View File

@ -0,0 +1,19 @@
using System.ComponentModel.DataAnnotations;
using Microsoft.EntityFrameworkCore;
namespace API.Schema;
[PrimaryKey("Key")]
public class FileLibrary(string basePath, string libraryName)
: Identifiable(TokenGen.CreateToken(typeof(FileLibrary), basePath))
{
[StringLength(256)]
[Required]
public string BasePath { get; internal set; } = basePath;
[StringLength(512)]
[Required]
public string LibraryName { get; internal set; } = libraryName;
public override string ToString() => $"{base.ToString()} {LibraryName} - {BasePath}";
}

View File

@ -0,0 +1,11 @@
using Microsoft.EntityFrameworkCore;
namespace API.Schema;
[PrimaryKey("Key")]
public abstract class Identifiable(string key)
{
public string Key { get; init; } = key;
public override string ToString() => Key;
}

View File

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

View File

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

View File

@ -3,7 +3,6 @@ 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 SixLabors.ImageSharp;
@ -14,31 +13,33 @@ using static System.IO.UnixFileMode;
namespace API.Schema.Jobs;
public class DownloadSingleChapterJob : Job
public class DownloadSingleChapterJob : JobWithDownloading
{
[StringLength(64)] [Required] public string ChapterId { get; init; }
private Chapter _chapter = null!;
[StringLength(64)] [Required] public string ChapterId { get; init; } = null!;
private Chapter? _chapter;
[JsonIgnore]
public Chapter Chapter
{
get => LazyLoader.Load(this, ref _chapter);
init => _chapter = value;
get => LazyLoader.Load(this, ref _chapter) ?? throw new InvalidOperationException();
init
{
ChapterId = value.Key;
_chapter = value;
}
}
public DownloadSingleChapterJob(Chapter chapter, Job? parentJob = null, ICollection<Job>? dependsOnJobs = null)
: base(TokenGen.CreateToken(typeof(DownloadSingleChapterJob)), JobType.DownloadSingleChapterJob, 0, parentJob, dependsOnJobs)
{
this.ChapterId = chapter.ChapterId;
this.Chapter = chapter;
}
/// <summary>
/// EF ONLY!!!
/// </summary>
internal DownloadSingleChapterJob(ILazyLoader lazyLoader, string jobId, ulong recurrenceMs, string chapterId, string? parentJobId)
: base(lazyLoader, jobId, JobType.DownloadSingleChapterJob, recurrenceMs, parentJobId)
internal DownloadSingleChapterJob(ILazyLoader lazyLoader, string key, string chapterId, ulong recurrenceMs, string? parentJobId)
: base(lazyLoader, key, JobType.DownloadSingleChapterJob, recurrenceMs, parentJobId)
{
this.ChapterId = chapterId;
}
@ -50,13 +51,16 @@ public class DownloadSingleChapterJob : Job
Log.Info("Chapter was already downloaded.");
return [];
}
string[] imageUrls = Chapter.ParentManga.MangaConnector.GetChapterImageUrls(Chapter);
//TODO MangaConnector Selection
MangaConnectorId<Chapter> mcId = Chapter.MangaConnectorIds.First();
string[] imageUrls = mcId.MangaConnector.GetChapterImageUrls(mcId);
if (imageUrls.Length < 1)
{
Log.Info($"No imageUrls for chapter {ChapterId}");
Log.Info($"No imageUrls for chapter {Chapter}");
return [];
}
context.Entry(Chapter.ParentManga).Reference<LocalLibrary>(m => m.Library).Load(); //Need to explicitly load, because we are not accessing navigation directly...
string saveArchiveFilePath = Chapter.FullArchiveFilePath;
Log.Debug($"Chapter path: {saveArchiveFilePath}");
@ -88,7 +92,7 @@ public class DownloadSingleChapterJob : Job
string tempFolder = Directory.CreateTempSubdirectory("trangatemp").FullName;
Log.Debug($"Created temp folder: {tempFolder}");
Log.Info($"Downloading images: {ChapterId}");
Log.Info($"Downloading images: {Chapter}");
int chapterNum = 0;
//Download all Images to temporary Folder
foreach (string imageUrl in imageUrls)
@ -105,10 +109,10 @@ public class DownloadSingleChapterJob : Job
CopyCoverFromCacheToDownloadLocation(Chapter.ParentManga);
Log.Debug($"Creating ComicInfo.xml {ChapterId}");
Log.Debug($"Creating ComicInfo.xml {Chapter}");
File.WriteAllText(Path.Join(tempFolder, "ComicInfo.xml"), Chapter.GetComicInfoXmlString());
Log.Debug($"Packaging images to archive {ChapterId}");
Log.Debug($"Packaging images to archive {Chapter}");
//ZIP-it and ship-it
ZipFile.CreateFromDirectory(tempFolder, saveArchiveFilePath);
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
@ -123,7 +127,7 @@ public class DownloadSingleChapterJob : Job
if (j.JobType != JobType.UpdateChaptersDownloadedJob)
return false;
UpdateChaptersDownloadedJob job = (UpdateChaptersDownloadedJob)j;
return job.MangaId == this.Chapter.ParentMangaId;
return job.MangaId == Chapter.ParentMangaId;
}))
return [];
@ -179,8 +183,11 @@ public class DownloadSingleChapterJob : Job
return;
}
//TODO MangaConnector Selection
MangaConnectorId<Manga> mcId = manga.MangaConnectorIds.First();
Log.Info($"Copying cover to {publicationFolder}");
string? fileInCache = manga.CoverFileNameInCache ?? manga.MangaConnector.SaveCoverImageToCache(manga);
string? fileInCache = manga.CoverFileNameInCache ?? mcId.MangaConnector.SaveCoverImageToCache(mcId);
if (fileInCache is null)
{
Log.Error($"File {fileInCache} does not exist");

View File

@ -8,19 +8,15 @@ using Newtonsoft.Json;
namespace API.Schema.Jobs;
[PrimaryKey("JobId")]
public abstract class Job
[PrimaryKey("Key")]
public abstract class Job : Identifiable, IComparable<Job>
{
[StringLength(64)]
[Required]
public string JobId { get; init; }
[StringLength(64)] public string? ParentJobId { get; private set; }
[JsonIgnore] public Job? ParentJob { get; internal set; }
private ICollection<Job> _dependsOnJobs = null!;
private ICollection<Job>? _dependsOnJobs;
[JsonIgnore] public ICollection<Job> DependsOnJobs
{
get => LazyLoader.Load(this, ref _dependsOnJobs);
get => LazyLoader.Load(this, ref _dependsOnJobs) ?? throw new InvalidOperationException();
init => _dependsOnJobs = value;
}
@ -37,14 +33,14 @@ public abstract class Job
[JsonIgnore] [NotMapped] internal bool IsCompleted => state is >= (JobState)128 and < (JobState)192;
[NotMapped] [JsonIgnore] protected ILog Log { get; init; }
[NotMapped] [JsonIgnore] protected ILazyLoader LazyLoader { get; init; }
[NotMapped] [JsonIgnore] protected ILazyLoader LazyLoader { get; init; } = null!;
protected Job(string jobId, JobType jobType, ulong recurrenceMs, Job? parentJob = null, ICollection<Job>? dependsOnJobs = null)
protected Job(string key, JobType jobType, ulong recurrenceMs, Job? parentJob = null, ICollection<Job>? dependsOnJobs = null)
: base(key)
{
this.JobId = jobId;
this.JobType = jobType;
this.RecurrenceMs = recurrenceMs;
this.ParentJobId = parentJob?.JobId;
this.ParentJobId = parentJob?.Key;
this.ParentJob = parentJob;
this.DependsOnJobs = dependsOnJobs ?? [];
@ -54,10 +50,10 @@ public abstract class Job
/// <summary>
/// EF ONLY!!!
/// </summary>
protected internal Job(ILazyLoader lazyLoader, string jobId, JobType jobType, ulong recurrenceMs, string? parentJobId)
protected internal Job(ILazyLoader lazyLoader, string key, JobType jobType, ulong recurrenceMs, string? parentJobId)
: base(key)
{
this.LazyLoader = lazyLoader;
this.JobId = jobId;
this.JobType = jobType;
this.RecurrenceMs = recurrenceMs;
this.ParentJobId = parentJobId;
@ -68,7 +64,7 @@ public abstract class Job
public IEnumerable<Job> Run(PgsqlContext context, ref bool running)
{
Log.Info($"Running job {JobId}");
Log.Info($"Running job {this}");
DateTime jobStart = DateTime.UtcNow;
Job[]? ret = null;
@ -78,7 +74,7 @@ public abstract class Job
context.SaveChanges();
running = true;
ret = RunInternal(context).ToArray();
Log.Info($"Job {JobId} completed. Generated {ret.Length} new jobs.");
Log.Info($"Job {this} completed. Generated {ret.Length} new jobs.");
this.state = this.RecurrenceMs > 0 ? JobState.CompletedWaiting : JobState.Completed;
this.LastExecution = DateTime.UtcNow;
context.SaveChanges();
@ -87,7 +83,7 @@ public abstract class Job
{
if (e is not DbUpdateException)
{
Log.Error($"Failed to run job {JobId}", e);
Log.Error($"Failed to run job {this}", e);
this.state = JobState.Failed;
this.Enabled = false;
this.LastExecution = DateTime.UtcNow;
@ -95,7 +91,7 @@ public abstract class Job
}
else
{
Log.Error($"Failed to update Database {JobId}", e);
Log.Error($"Failed to update Database {this}", e);
}
}
@ -109,28 +105,45 @@ public abstract class Job
}
catch (DbUpdateException e)
{
Log.Error($"Failed to update Database {JobId}", e);
Log.Error($"Failed to update Database {this}", e);
}
Log.Info($"Finished Job {JobId}! (took {DateTime.UtcNow.Subtract(jobStart).TotalMilliseconds}ms)");
Log.Info($"Finished Job {this}! (took {DateTime.UtcNow.Subtract(jobStart).TotalMilliseconds}ms)");
return ret ?? [];
}
protected abstract IEnumerable<Job> RunInternal(PgsqlContext context);
public List<Job> GetDependenciesAndSelf()
{
List<Job> ret = GetDependencies();
ret.Add(this);
return ret;
}
public List<Job> GetDependencies()
{
List<Job> ret = new ();
foreach (Job job in DependsOnJobs)
{
ret.AddRange(job.GetDependenciesAndSelf());
}
ret.Add(this);
return ret;
}
public override string ToString()
public int CompareTo(Job? other)
{
return $"{JobId}";
if (other is null)
return -1;
// Sort by missing dependencies
if (this.GetDependencies().Count(job => !job.IsCompleted) <
other.GetDependencies().Count(job => !job.IsCompleted))
return -1;
// Sort by NextExecution-time
if (this.NextExecution < other.NextExecution)
return -1;
return 1;
}
public override string ToString() => base.ToString();
}

View File

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

View File

@ -23,8 +23,8 @@ public class MoveFileOrFolderJob : Job
/// <summary>
/// EF ONLY!!!
/// </summary>
internal MoveFileOrFolderJob(ILazyLoader lazyLoader, string jobId, ulong recurrenceMs, string fromLocation, string toLocation, string? parentJobId)
: base(lazyLoader, jobId, JobType.MoveFileOrFolderJob, recurrenceMs, parentJobId)
internal MoveFileOrFolderJob(ILazyLoader lazyLoader, string key, ulong recurrenceMs, string fromLocation, string toLocation, string? parentJobId)
: base(lazyLoader, key, JobType.MoveFileOrFolderJob, recurrenceMs, parentJobId)
{
this.FromLocation = fromLocation;
this.ToLocation = toLocation;

View File

@ -8,33 +8,45 @@ namespace API.Schema.Jobs;
public class MoveMangaLibraryJob : Job
{
[StringLength(64)] [Required] public string MangaId { get; init; }
private Manga _manga = null!;
[StringLength(64)] [Required] public string MangaId { get; init; } = null!;
private Manga? _manga;
[JsonIgnore]
public Manga Manga
{
get => LazyLoader.Load(this, ref _manga);
init => _manga = value;
get => LazyLoader.Load(this, ref _manga) ?? throw new InvalidOperationException();
init
{
MangaId = value.Key;
_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)
[StringLength(64)] [Required] public string ToLibraryId { get; private set; } = null!;
private FileLibrary? _toFileLibrary;
[JsonIgnore]
public FileLibrary ToFileLibrary
{
get => LazyLoader.Load(this, ref _toFileLibrary) ?? throw new InvalidOperationException();
init
{
ToLibraryId = value.Key;
_toFileLibrary = value;
}
}
public MoveMangaLibraryJob(Manga manga, FileLibrary toFileLibrary, Job? parentJob = null, ICollection<Job>? dependsOnJobs = null)
: base(TokenGen.CreateToken(typeof(MoveMangaLibraryJob)), JobType.MoveMangaLibraryJob, 0, parentJob, dependsOnJobs)
{
this.MangaId = manga.MangaId;
this.Manga = manga;
this.ToLibraryId = toLibrary.LocalLibraryId;
this.ToLibrary = toLibrary;
this.ToFileLibrary = toFileLibrary;
}
/// <summary>
/// EF ONLY!!!
/// </summary>
internal MoveMangaLibraryJob(ILazyLoader lazyLoader, string jobId, ulong recurrenceMs, string mangaId, string toLibraryId, string? parentJobId)
: base(lazyLoader, jobId, JobType.MoveMangaLibraryJob, recurrenceMs, parentJobId)
internal MoveMangaLibraryJob(ILazyLoader lazyLoader, string key, ulong recurrenceMs, string mangaId, string toLibraryId, string? parentJobId)
: base(lazyLoader, key, JobType.MoveMangaLibraryJob, recurrenceMs, parentJobId)
{
this.MangaId = mangaId;
this.ToLibraryId = toLibraryId;
@ -42,9 +54,9 @@ public class MoveMangaLibraryJob : Job
protected override IEnumerable<Job> RunInternal(PgsqlContext context)
{
context.Entry(Manga).Reference<LocalLibrary>(m => m.Library).Load();
context.Entry(Manga).Reference<FileLibrary>(m => m.Library).Load();
Dictionary<Chapter, string> oldPath = Manga.Chapters.ToDictionary(c => c, c => c.FullArchiveFilePath);
Manga.Library = ToLibrary;
Manga.Library = ToFileLibrary;
try
{
context.SaveChanges();

View File

@ -6,24 +6,27 @@ using Newtonsoft.Json;
namespace API.Schema.Jobs;
public class RetrieveChaptersJob : Job
public class RetrieveChaptersJob : JobWithDownloading
{
[StringLength(64)] [Required] public string MangaId { get; init; }
private Manga _manga = null!;
[StringLength(64)] [Required] public string MangaId { get; init; } = null!;
private Manga? _manga;
[JsonIgnore]
public Manga Manga
{
get => LazyLoader.Load(this, ref _manga);
init => _manga = value;
get => LazyLoader.Load(this, ref _manga) ?? throw new InvalidOperationException();
init
{
MangaId = value.Key;
_manga = value;
}
}
[StringLength(8)] [Required] public string Language { get; private set; }
public RetrieveChaptersJob(Manga manga, string language, ulong recurrenceMs, Job? parentJob = null, ICollection<Job>? dependsOnJobs = null)
: base(TokenGen.CreateToken(typeof(RetrieveChaptersJob)), JobType.RetrieveChaptersJob, recurrenceMs, parentJob, dependsOnJobs)
{
this.MangaId = manga.MangaId;
this.Manga = manga;
this.Language = language;
}
@ -31,8 +34,8 @@ public class RetrieveChaptersJob : Job
/// <summary>
/// EF ONLY!!!
/// </summary>
internal RetrieveChaptersJob(ILazyLoader lazyLoader, string jobId, ulong recurrenceMs, string mangaId, string language, string? parentJobId)
: base(lazyLoader, jobId, JobType.RetrieveChaptersJob, recurrenceMs, parentJobId)
internal RetrieveChaptersJob(ILazyLoader lazyLoader, string key, string mangaId, ulong recurrenceMs, string language, string? parentJobId)
: base(lazyLoader, key, JobType.RetrieveChaptersJob, recurrenceMs, parentJobId)
{
this.MangaId = mangaId;
this.Language = language;
@ -40,15 +43,21 @@ public class RetrieveChaptersJob : Job
protected override IEnumerable<Job> RunInternal(PgsqlContext context)
{
//TODO MangaConnector Selection
MangaConnectorId<Manga> mcId = Manga.MangaConnectorIds.First();
// This gets all chapters that are not downloaded
Chapter[] allChapters = Manga.MangaConnector.GetChapters(Manga, Language).DistinctBy(c => c.ChapterId).ToArray();
Chapter[] newChapters = allChapters.Where(chapter => Manga.Chapters.Select(c => c.ChapterId).Contains(chapter.ChapterId) == false).ToArray();
(Chapter, MangaConnectorId<Chapter>)[] allChapters = mcId.MangaConnector.GetChapters(mcId, Language).DistinctBy(c => c.Item1.Key).ToArray();
(Chapter, MangaConnectorId<Chapter>)[] newChapters = allChapters.Where(chapter => Manga.Chapters.Any(ch => chapter.Item1.Key == ch.Key && ch.Downloaded) == false).ToArray();
Log.Info($"{Manga.Chapters.Count} existing + {newChapters.Length} new chapters.");
try
{
foreach (Chapter newChapter in newChapters)
Manga.Chapters.Add(newChapter);
foreach ((Chapter chapter, MangaConnectorId<Chapter> mcId) newChapter in newChapters)
{
Manga.Chapters.Add(newChapter.chapter);
context.MangaConnectorToChapter.Add(newChapter.mcId);
}
context.SaveChanges();
}
catch (DbUpdateException e)

View File

@ -8,36 +8,38 @@ namespace API.Schema.Jobs;
public class UpdateChaptersDownloadedJob : Job
{
[StringLength(64)] [Required] public string MangaId { get; init; }
private Manga _manga = null!;
[StringLength(64)] [Required] public string MangaId { get; init; } = null!;
private Manga? _manga;
[JsonIgnore]
public Manga Manga
{
get => LazyLoader.Load(this, ref _manga);
init => _manga = value;
get => LazyLoader.Load(this, ref _manga) ?? throw new InvalidOperationException();
init
{
MangaId = value.Key;
_manga = value;
}
}
public UpdateChaptersDownloadedJob(Manga manga, ulong recurrenceMs, Job? parentJob = null, ICollection<Job>? dependsOnJobs = null)
: base(TokenGen.CreateToken(typeof(UpdateChaptersDownloadedJob)), JobType.UpdateChaptersDownloadedJob, recurrenceMs, parentJob, dependsOnJobs)
{
this.MangaId = manga.MangaId;
this.Manga = manga;
}
/// <summary>
/// EF ONLY!!!
/// </summary>
internal UpdateChaptersDownloadedJob(ILazyLoader lazyLoader, string jobId, ulong recurrenceMs, string mangaId, string? parentJobId)
: base(lazyLoader, jobId, JobType.UpdateChaptersDownloadedJob, recurrenceMs, parentJobId)
internal UpdateChaptersDownloadedJob(ILazyLoader lazyLoader, string key, ulong recurrenceMs, string mangaId, string? parentJobId)
: base(lazyLoader, key, JobType.UpdateChaptersDownloadedJob, recurrenceMs, parentJobId)
{
this.MangaId = mangaId;
}
protected override IEnumerable<Job> RunInternal(PgsqlContext context)
{
context.Entry(Manga).Reference<LocalLibrary>(m => m.Library).Load();
context.Entry(Manga).Reference<FileLibrary>(m => m.Library).Load();
foreach (Chapter mangaChapter in Manga.Chapters)
{
mangaChapter.Downloaded = mangaChapter.CheckDownloaded();

View File

@ -8,30 +8,32 @@ namespace API.Schema.Jobs;
public class UpdateCoverJob : Job
{
[StringLength(64)] [Required] public string MangaId { get; init; }
private Manga _manga = null!;
[StringLength(64)] [Required] public string MangaId { get; init; } = null!;
private Manga? _manga;
[JsonIgnore]
public Manga Manga
{
get => LazyLoader.Load(this, ref _manga);
init => _manga = value;
get => LazyLoader.Load(this, ref _manga) ?? throw new InvalidOperationException();
init
{
MangaId = value.Key;
_manga = value;
}
}
public UpdateCoverJob(Manga manga, ulong recurrenceMs, Job? parentJob = null, ICollection<Job>? dependsOnJobs = null)
: base(TokenGen.CreateToken(typeof(UpdateCoverJob)), JobType.UpdateCoverJob, recurrenceMs, parentJob, dependsOnJobs)
{
this.MangaId = manga.MangaId;
this.Manga = manga;
}
/// <summary>
/// EF ONLY!!!
/// </summary>
internal UpdateCoverJob(ILazyLoader lazyLoader, string jobId, ulong recurrenceMs, string mangaId, string? parentJobId)
: base(lazyLoader, jobId, JobType.UpdateCoverJob, recurrenceMs, parentJobId)
internal UpdateCoverJob(ILazyLoader lazyLoader, string key, string mangaId, ulong recurrenceMs, string? parentJobId)
: base(lazyLoader, key, JobType.UpdateCoverJob, recurrenceMs, parentJobId)
{
this.MangaId = mangaId;
}

View File

@ -44,8 +44,9 @@ public class Kavita : LibraryConnector
{
}
}
catch (HttpRequestException e)
catch (HttpRequestException)
{
}
return "";
}
@ -53,13 +54,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/ToFileLibrary/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/ToFileLibrary/scan?libraryId={lib.id}", "Bearer", Auth))
return true;
return false;
}
@ -70,7 +71,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/ToFileLibrary/libraries", "Bearer", Auth);
if (data == Stream.Null)
{
Log.Info("No libraries found");
@ -90,7 +91,7 @@ public class Kavita : LibraryConnector
JsonObject? jObject = (JsonObject?)jsonNode;
if(jObject is null)
continue;
int libraryId = jObject!["id"]!.GetValue<int>();
int libraryId = jObject["id"]!.GetValue<int>();
string libraryName = jObject["name"]!.GetValue<string>();
ret.Add(new KavitaLibrary(libraryId, libraryName));
}

View File

@ -3,12 +3,9 @@ using Microsoft.EntityFrameworkCore;
namespace API.Schema;
[PrimaryKey("LinkId")]
public class Link(string linkProvider, string linkUrl)
[PrimaryKey("Key")]
public class Link(string linkProvider, string linkUrl) : Identifiable(TokenGen.CreateToken(typeof(Link), linkProvider, linkUrl))
{
[StringLength(64)]
[Required]
public string LinkId { get; init; } = TokenGen.CreateToken(typeof(Link), linkProvider, linkUrl);
[StringLength(64)]
[Required]
public string LinkProvider { get; init; } = linkProvider;
@ -17,8 +14,5 @@ public class Link(string linkProvider, string linkUrl)
[Url]
public string LinkUrl { get; init; } = linkUrl;
public override string ToString()
{
return $"{LinkId} {LinkProvider} {LinkUrl}";
}
public override string ToString() => $"{base.ToString()} {LinkProvider} {LinkUrl}";
}

View File

@ -1,22 +0,0 @@
using System.ComponentModel.DataAnnotations;
namespace API.Schema;
public class LocalLibrary(string basePath, string libraryName)
{
[StringLength(64)]
[Required]
public string LocalLibraryId { get; init; } = TokenGen.CreateToken(typeof(LocalLibrary), basePath);
[StringLength(256)]
[Required]
public string BasePath { get; internal set; } = basePath;
[StringLength(512)]
[Required]
public string LibraryName { get; internal set; } = libraryName;
public override string ToString()
{
return $"{LocalLibraryId} {LibraryName} - {BasePath}";
}
}

View File

@ -2,7 +2,8 @@ using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Runtime.InteropServices;
using System.Text;
using API.Schema.MangaConnectors;
using API.Schema.Contexts;
using API.Schema.Jobs;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Newtonsoft.Json;
@ -10,36 +11,33 @@ using static System.IO.UnixFileMode;
namespace API.Schema;
[PrimaryKey("MangaId")]
public class Manga
[PrimaryKey("Key")]
public class Manga : Identifiable
{
[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)]
[Required]
public string MangaConnectorName { get; init; }
[JsonIgnore] public MangaConnector MangaConnector { get; init; } = null!;
[StringLength(64)] public string? LibraryId { get; private set; }
private FileLibrary? _library;
[JsonIgnore]
public FileLibrary? Library
{
get => _lazyLoader.Load(this, ref _library);
set
{
LibraryId = value?.Key;
_library = value;
}
}
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!;
public ICollection<AltTitle> 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;
[JsonIgnore] [StringLength(512)] public string? CoverFileNameInCache { get; internal set; }
public uint? Year { get; internal init; }
[StringLength(8)] public string? OriginalLanguage { get; internal init; }
@ -47,31 +45,37 @@ public class Manga
[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!;
[NotMapped] public ICollection<string> ChapterIds => Chapters.Select(c => c.Key).ToList();
private ICollection<Chapter>? _chapters;
[JsonIgnore]
public ICollection<Chapter> Chapters
{
get => _lazyLoader.Load(this, ref _chapters);
get => _lazyLoader.Load(this, ref _chapters) ?? throw new InvalidOperationException();
init => _chapters = value;
}
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)
[NotMapped] public Dictionary<string, string> IdsOnMangaConnectors =>
MangaConnectorIds.ToDictionary(id => id.MangaConnectorName, id => id.IdOnConnectorSite);
private ICollection<MangaConnectorId<Manga>>? _mangaConnectorIds;
[JsonIgnore]
public ICollection<MangaConnectorId<Manga>> MangaConnectorIds
{
get => _lazyLoader.Load(this, ref _mangaConnectorIds) ?? throw new InvalidOperationException();
private set => _mangaConnectorIds = value;
}
private readonly ILazyLoader _lazyLoader = null!;
public Manga(string name, string description, string coverUrl, MangaReleaseStatus releaseStatus,
ICollection<Author> authors, ICollection<MangaTag> mangaTags, ICollection<Link> links, ICollection<AltTitle> altTitles,
FileLibrary? library = null, float ignoreChaptersBefore = 0f, uint? year = null, string? originalLanguage = null)
:base(TokenGen.CreateToken(typeof(Manga), 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;
@ -86,18 +90,16 @@ public class Manga
/// <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(ILazyLoader lazyLoader, string key, string name, string description, string coverUrl,
MangaReleaseStatus releaseStatus,
string directoryName, float ignoreChaptersBefore, string? libraryId, uint? year, string? originalLanguage)
: base(key)
{
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;
@ -154,8 +156,53 @@ public class Manga
return sb.ToString();
}
public override string ToString()
/// <summary>
///
/// </summary>
/// <param name="other"></param>
/// <param name="context"></param>
/// <exception cref="DbUpdateException"></exception>
public void MergeFrom(Manga other, PgsqlContext context)
{
return $"{MangaId} {Name}";
try
{
context.Mangas.Remove(other);
List<Job> newJobs = new();
this.MangaConnectorIds = this.MangaConnectorIds
.UnionBy(other.MangaConnectorIds, id => id.MangaConnectorName)
.ToList();
foreach (Chapter otherChapter in other.Chapters)
{
string oldPath = otherChapter.FullArchiveFilePath;
Chapter newChapter = new(this, otherChapter.ChapterNumber, otherChapter.VolumeNumber,
otherChapter.Title);
this.Chapters.Add(newChapter);
string newPath = newChapter.FullArchiveFilePath;
newJobs.Add(new MoveFileOrFolderJob(oldPath, newPath));
}
if (other.Chapters.Count > 0)
newJobs.Add(new UpdateChaptersDownloadedJob(this, 0, null, newJobs));
context.Jobs.AddRange(newJobs);
context.SaveChanges();
}
catch (DbUpdateException e)
{
throw new DbUpdateException(e.Message, e.InnerException, e.Entries);
}
}
public override string ToString() => $"{base.ToString()} {Name}";
}
public enum MangaReleaseStatus : byte
{
Continuing = 0,
Completed = 1,
OnHiatus = 2,
Cancelled = 3,
Unreleased = 4
}

View File

@ -1,23 +0,0 @@
using System.ComponentModel.DataAnnotations;
using Microsoft.EntityFrameworkCore;
namespace API.Schema;
[PrimaryKey("AltTitleId")]
public class MangaAltTitle(string language, string title)
{
[StringLength(64)]
[Required]
public string AltTitleId { get; init; } = TokenGen.CreateToken("AltTitle");
[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,67 @@
using System.ComponentModel.DataAnnotations;
using API.Schema.MangaConnectors;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Newtonsoft.Json;
namespace API.Schema;
[PrimaryKey("Key")]
public class MangaConnectorId<T> : Identifiable where T : Identifiable
{
[StringLength(64)] [Required] public string ObjId { get; private set; } = null!;
[JsonIgnore] private T? _obj;
[JsonIgnore]
public T Obj
{
get => _lazyLoader.Load(this, ref _obj) ?? throw new InvalidOperationException();
internal set
{
ObjId = value.Key;
_obj = value;
}
}
[StringLength(32)] [Required] public string MangaConnectorName { get; private set; } = null!;
[JsonIgnore] private MangaConnector? _mangaConnector;
[JsonIgnore]
public MangaConnector MangaConnector
{
get => _lazyLoader.Load(this, ref _mangaConnector) ?? throw new InvalidOperationException();
init
{
MangaConnectorName = value.Name;
_mangaConnector = value;
}
}
[StringLength(256)] [Required] public string IdOnConnectorSite { get; init; }
[Url] [StringLength(512)] public string? WebsiteUrl { get; internal init; }
private readonly ILazyLoader _lazyLoader = null!;
public MangaConnectorId(T obj, MangaConnector mangaConnector, string idOnConnectorSite, string? websiteUrl)
: base(TokenGen.CreateToken(typeof(MangaConnectorId<T>), mangaConnector.Name, idOnConnectorSite))
{
this.Obj = obj;
this.MangaConnector = mangaConnector;
this.IdOnConnectorSite = idOnConnectorSite;
this.WebsiteUrl = websiteUrl;
}
/// <summary>
/// EF CORE ONLY!!!
/// </summary>
public MangaConnectorId(ILazyLoader lazyLoader, string key, string objId, string mangaConnectorName, string idOnConnectorSite, string? websiteUrl)
: base(key)
{
this._lazyLoader = lazyLoader;
this.ObjId = objId;
this.MangaConnectorName = mangaConnectorName;
this.IdOnConnectorSite = idOnConnectorSite;
this.WebsiteUrl = websiteUrl;
}
public override string ToString() => $"{base.ToString()} {_obj}";
}

View File

@ -17,9 +17,9 @@ public class ComickIo : MangaConnector
this.downloadClient = new HttpDownloadClient();
}
public override Manga[] SearchManga(string mangaSearchName)
public override (Manga, MangaConnectorId<Manga>)[] SearchManga(string mangaSearchName)
{
Log.Info($"Searching Manga: {mangaSearchName}");
Log.Info($"Searching Obj: {mangaSearchName}");
List<string> slugs = new();
int page = 1;
@ -46,20 +46,26 @@ public class ComickIo : MangaConnector
}
Log.Debug($"Search {mangaSearchName} yielded {slugs.Count} slugs. Requesting mangas now...");
List<Manga> mangas = slugs.Select(GetMangaFromId).ToList()!;
List<(Manga, MangaConnectorId<Manga>)> mangas = new ();
foreach (string slug in slugs)
{
if(GetMangaFromId(slug) is { } entry)
mangas.Add(entry);
}
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)
public override (Manga, MangaConnectorId<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)
public override (Manga, MangaConnectorId<Manga>)? GetMangaFromId(string mangaIdOnSite)
{
string requestUrl = $"https://api.comick.fun/comic/{mangaIdOnSite}";
@ -75,14 +81,15 @@ public class ComickIo : MangaConnector
return ParseMangaFromJToken(data);
}
public override Chapter[] GetChapters(Manga manga, string? language = null)
public override (Chapter, MangaConnectorId<Chapter>)[] GetChapters(MangaConnectorId<Manga> mangaConnectorId,
string? language = null)
{
Log.Info($"Getting Chapters: {manga.IdOnConnectorSite}");
List<Chapter> chapters = new();
Log.Info($"Getting Chapters: {mangaConnectorId.IdOnConnectorSite}");
List<(Chapter, MangaConnectorId<Chapter>)> chapters = new();
int page = 1;
while(page < 50)
{
string requestUrl = $"https://api.comick.fun/comic/{manga.IdOnConnectorSite}/chapters?limit=100&page={page}&lang={language}";
string requestUrl = $"https://api.comick.fun/comic/{mangaConnectorId.IdOnConnectorSite}/chapters?limit=100&page={page}&lang={language}";
RequestResult result = downloadClient.MakeRequest(requestUrl, RequestType.MangaInfo);
if ((int)result.statusCode < 200 || (int)result.statusCode >= 300)
@ -98,7 +105,7 @@ public class ComickIo : MangaConnector
if (chaptersArray is null || chaptersArray.Count < 1)
break;
chapters.AddRange(ParseChapters(manga, chaptersArray));
chapters.AddRange(ParseChapters(mangaConnectorId, chaptersArray));
page++;
}
@ -107,11 +114,22 @@ public class ComickIo : MangaConnector
}
private readonly Regex _hidFromUrl = new(@"https?:\/\/comick\.io\/comic\/.+\/([^-]+).*");
internal override string[] GetChapterImageUrls(Chapter chapter)
internal override string[] GetChapterImageUrls(MangaConnectorId<Chapter> chapterId)
{
Match m = _hidFromUrl.Match(chapter.Url);
if (!m.Groups[1].Success)
Log.Info($"Getting Chapter Image-Urls: {chapterId.Obj}");
if (chapterId.WebsiteUrl is null || !UrlMatchesConnector(chapterId.WebsiteUrl))
{
Log.Debug($"Url is not for Connector. {chapterId.WebsiteUrl}");
return [];
}
Match m = _hidFromUrl.Match(chapterId.WebsiteUrl);
if (!m.Groups[1].Success)
{
Log.Debug($"Could not parse hid from url. {chapterId.WebsiteUrl}");
return [];
}
string hid = m.Groups[1].Value;
@ -133,7 +151,7 @@ public class ComickIo : MangaConnector
}).ToArray();
}
private Manga ParseMangaFromJToken(JToken json)
private (Manga manga, MangaConnectorId<Manga> id) ParseMangaFromJToken(JToken json)
{
string? hid = json["comic"]?.Value<string>("hid");
string? slug = json["comic"]?.Value<string>("slug");
@ -156,15 +174,15 @@ public class ComickIo : MangaConnector
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")!))
List<AltTitle> altTitles = altTitlesArray?
.Select(token => new AltTitle(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)
.DistinctBy(a => a.Key)
.ToList()!;
JArray? genreArray = json["comic"]?["md_comic_md_genres"] as JArray;
@ -192,7 +210,7 @@ public class ComickIo : MangaConnector
"al" => "AniList",
"ap" => "Anime Planet",
"bw" => "BookWalker",
"mu" => "Manga Updates",
"mu" => "Obj Updates",
"nu" => "Novel Updates",
"kt" => "Kitsu.io",
"amz" => "Amazon",
@ -211,14 +229,14 @@ public class ComickIo : MangaConnector
if(name is null)
throw new Exception("name is null");
return new Manga(hid, name, description??"", url, coverUrl, status, this,
authors, tags, links, altTitles,
Manga manga = new (name, description??"", coverUrl, status, authors, tags, links, altTitles,
year: year, originalLanguage: originalLanguage);
return (manga, new MangaConnectorId<Manga>(manga, this, hid, url));
}
private List<Chapter> ParseChapters(Manga parentManga, JArray chaptersArray)
private List<(Chapter, MangaConnectorId<Chapter>)> ParseChapters(MangaConnectorId<Manga> mcIdManga, JArray chaptersArray)
{
List<Chapter> chapters = new ();
List<(Chapter, MangaConnectorId<Chapter>)> chapters = new ();
foreach (JToken chapter in chaptersArray)
{
string? chapterNum = chapter.Value<string>("chap");
@ -226,12 +244,14 @@ public class ComickIo : MangaConnector
int? volumeNum = volumeNumStr is null ? null : int.Parse(volumeNumStr);
string? title = chapter.Value<string>("title");
string? hid = chapter.Value<string>("hid");
string url = $"https://comick.io/comic/{parentManga.IdOnConnectorSite}/{hid}";
string url = $"https://comick.io/comic/{mcIdManga.IdOnConnectorSite}/{hid}";
if(chapterNum is null || hid is null)
continue;
chapters.Add(new (parentManga, url, chapterNum, volumeNum, hid, title));
Chapter ch = new (mcIdManga.Obj, chapterNum, volumeNum, title);
chapters.Add((ch, new (ch, this, hid, url)));
}
return chapters;
}

View File

@ -10,14 +10,14 @@ public class Global : MangaConnector
this.context = context;
}
public override Manga[] SearchManga(string mangaSearchName)
public override (Manga, MangaConnectorId<Manga>)[] SearchManga(string mangaSearchName)
{
//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();
//Create Task for each MangaConnector to search simultaneously
Task<(Manga, MangaConnectorId<Manga>)[]>[] tasks =
enabledConnectors.Select(c => new Task<(Manga, MangaConnectorId<Manga>)[]>(() => c.SearchManga(mangaSearchName))).ToArray();
foreach (var task in tasks)
task.Start();
@ -28,28 +28,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, MangaConnectorId<Manga>)[] ret = tasks.Select(t => t.IsCompletedSuccessfully ? t.Result : []).ToArray().SelectMany(i => i).ToArray();
return ret;
}
public override Manga? GetMangaFromUrl(string url)
public override (Manga, MangaConnectorId<Manga>)? GetMangaFromUrl(string url)
{
MangaConnector? mc = context.MangaConnectors.ToArray().FirstOrDefault(c => c.UrlMatchesConnector(url));
return mc?.GetMangaFromUrl(url) ?? null;
}
public override Manga? GetMangaFromId(string mangaIdOnSite)
public override (Manga, MangaConnectorId<Manga>)? GetMangaFromId(string mangaIdOnSite)
{
return null;
}
public override Chapter[] GetChapters(Manga manga, string? language = null)
public override (Chapter, MangaConnectorId<Chapter>)[] GetChapters(MangaConnectorId<Manga> manga,
string? language = null)
{
return manga.MangaConnector.GetChapters(manga, language);
}
internal override string[] GetChapterImageUrls(Chapter chapter)
internal override string[] GetChapterImageUrls(MangaConnectorId<Chapter> chapterId)
{
return chapter.ParentManga.MangaConnector.GetChapterImageUrls(chapter);
return chapterId.MangaConnector.GetChapterImageUrls(chapterId);
}
}

View File

@ -34,35 +34,36 @@ 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, MangaConnectorId<Manga>)[] SearchManga(string mangaSearchName);
public abstract Manga? GetMangaFromUrl(string url);
public abstract (Manga, MangaConnectorId<Manga>)? GetMangaFromUrl(string url);
public abstract Manga? GetMangaFromId(string mangaIdOnSite);
public abstract (Manga, MangaConnectorId<Manga>)? GetMangaFromId(string mangaIdOnSite);
public abstract Chapter[] GetChapters(Manga manga, string? language = null);
public abstract (Chapter, MangaConnectorId<Chapter>)[] GetChapters(MangaConnectorId<Manga> mangaId,
string? language = null);
internal abstract string[] GetChapterImageUrls(Chapter chapter);
internal abstract string[] GetChapterImageUrls(MangaConnectorId<Chapter> chapterId);
public bool UrlMatchesConnector(string url) => BaseUris.Any(baseUri => Regex.IsMatch(url, "https?://" + baseUri + "/.*"));
internal string? SaveCoverImageToCache(Manga manga, int retries = 3)
internal string? SaveCoverImageToCache(MangaConnectorId<Manga> mangaId, 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}";
Match match = urlRex.Match(mangaId.Obj.CoverUrl);
string filename = $"{match.Groups[1].Value}-{mangaId.Key}.{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}");
RequestResult coverResult = downloadClient.MakeRequest(mangaId.Obj.CoverUrl, RequestType.MangaCover, $"https://{match.Groups[1].Value}");
if ((int)coverResult.statusCode < 200 || (int)coverResult.statusCode >= 300)
return SaveCoverImageToCache(manga, --retries);
return SaveCoverImageToCache(mangaId, --retries);
using MemoryStream ms = new();
coverResult.result.CopyTo(ms);

View File

@ -18,10 +18,10 @@ public class MangaDex : MangaConnector
}
private const int Limit = 100;
public override Manga[] SearchManga(string mangaSearchName)
public override (Manga, MangaConnectorId<Manga>)[] SearchManga(string mangaSearchName)
{
Log.Info($"Searching Manga: {mangaSearchName}");
List<Manga> mangas = new ();
Log.Info($"Searching Obj: {mangaSearchName}");
List<(Manga, MangaConnectorId<Manga>)> mangas = new ();
int offset = 0;
int total = int.MaxValue;
@ -67,9 +67,9 @@ public class MangaDex : MangaConnector
}
private static readonly Regex GetMangaIdFromUrl = new(@"https?:\/\/mangadex\.org\/title\/([a-z0-9-]+)\/?.*");
public override Manga? GetMangaFromUrl(string url)
public override (Manga, MangaConnectorId<Manga>)? GetMangaFromUrl(string url)
{
Log.Info($"Getting Manga: {url}");
Log.Info($"Getting Obj: {url}");
if (!UrlMatchesConnector(url))
{
Log.Debug($"Url is not for Connector. {url}");
@ -87,9 +87,9 @@ public class MangaDex : MangaConnector
return GetMangaFromId(id);
}
public override Manga? GetMangaFromId(string mangaIdOnSite)
public override (Manga, MangaConnectorId<Manga>)? GetMangaFromId(string mangaIdOnSite)
{
Log.Info($"Getting Manga: {mangaIdOnSite}");
Log.Info($"Getting Obj: {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'";
@ -118,14 +118,13 @@ public class MangaDex : MangaConnector
return null;
}
Manga manga = ParseMangaFromJToken(data);
return manga;
return ParseMangaFromJToken(data);
}
public override Chapter[] GetChapters(Manga manga, string? language = null)
public override (Chapter, MangaConnectorId<Chapter>)[] GetChapters(MangaConnectorId<Manga> manga, string? language = null)
{
Log.Info($"Getting Chapters: {manga.IdOnConnectorSite}");
List<Chapter> chapters = new ();
List<(Chapter, MangaConnectorId<Chapter>)> chapters = new ();
int offset = 0;
int total = int.MaxValue;
@ -166,24 +165,24 @@ public class MangaDex : MangaConnector
chapters.AddRange(data.Select(d => ParseChapterFromJToken(manga, d)));
}
Log.Info($"Request for chapters for {manga.Name} yielded {chapters.Count} results.");
Log.Info($"Request for chapters for {manga.Obj.Name} yielded {chapters.Count} results.");
return chapters.ToArray();
}
private static readonly Regex GetChapterIdFromUrl = new(@"https?:\/\/mangadex\.org\/chapter\/([a-z0-9-]+)\/?.*");
internal override string[] GetChapterImageUrls(Chapter chapter)
internal override string[] GetChapterImageUrls(MangaConnectorId<Chapter> chapterId)
{
Log.Info($"Getting Chapter Image-Urls: {chapter.Url}");
if (!UrlMatchesConnector(chapter.Url))
Log.Info($"Getting Chapter Image-Urls: {chapterId.Obj}");
if (chapterId.WebsiteUrl is null || !UrlMatchesConnector(chapterId.WebsiteUrl))
{
Log.Debug($"Url is not for Connector. {chapter.Url}");
Log.Debug($"Url is not for Connector. {chapterId.WebsiteUrl}");
return [];
}
Match match = GetChapterIdFromUrl.Match(chapter.Url);
Match match = GetChapterIdFromUrl.Match(chapterId.WebsiteUrl);
if (!match.Success || !match.Groups[1].Success)
{
Log.Debug($"Url is not for Connector (Could not retrieve id). {chapter.Url}");
Log.Debug($"Url is not for Connector (Could not retrieve id). {chapterId.WebsiteUrl}");
return [];
}
@ -223,7 +222,7 @@ public class MangaDex : MangaConnector
return urls.ToArray();
}
private Manga ParseMangaFromJToken(JToken jToken)
private (Manga manga, MangaConnectorId<Manga> id) ParseMangaFromJToken(JToken jToken)
{
string? id = jToken.Value<string>("id");
if(id is null)
@ -267,7 +266,7 @@ public class MangaDex : MangaConnector
"al" => "AniList",
"ap" => "Anime Planet",
"bw" => "BookWalker",
"mu" => "Manga Updates",
"mu" => "Obj Updates",
"nu" => "Novel Updates",
"kt" => "Kitsu.io",
"amz" => "Amazon",
@ -279,14 +278,14 @@ public class MangaDex : MangaConnector
return new Link(key, url);
}).ToList()!;
List<MangaAltTitle> altTitles = (altTitlesJArray??[])
List<AltTitle> 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());
return new AltTitle(p.Name, p.Value.ToString());
}).Where(x => x is not null).ToList()!;
List<MangaTag> tags = (tagsJArray??[])
@ -313,26 +312,27 @@ public class MangaDex : MangaConnector
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,
Manga manga = new Manga(name, description, coverUrl, releaseStatus, authors, tags, links,altTitles,
null, 0f, year, originalLanguage);
return (manga, new MangaConnectorId<Manga>(manga, this, id, websiteUrl));
}
private Chapter ParseChapterFromJToken(Manga parentManga, JToken jToken)
private (Chapter chapter, MangaConnectorId<Chapter> id) ParseChapterFromJToken(MangaConnectorId<Manga> mcIdManga, JToken jToken)
{
string? id = jToken.Value<string>("id");
JToken? attributes = jToken["attributes"];
string? chapter = attributes?.Value<string>("chapter");
string? chapterStr = attributes?.Value<string>("chapter");
string? volumeStr = attributes?.Value<string>("volume");
int? volume = null;
int? volumeNumber = null;
string? title = attributes?.Value<string>("title");
if(id is null || chapter is null)
if(id is null || chapterStr is null)
throw new Exception("jToken was not in expected format");
if(volumeStr is not null)
volume = int.Parse(volumeStr);
volumeNumber = int.Parse(volumeStr);
string url = $"https://mangadex.org/chapter/{id}";
return new Chapter(parentManga, url, chapter, volume, id, title);
string websiteUrl = $"https://mangadex.org/chapter/{id}";
Chapter chapter = new (mcIdManga.Obj, chapterStr, volumeNumber, title);
return (chapter, new MangaConnectorId<Chapter>(chapter, this, id, websiteUrl));
}
}

View File

@ -1,10 +0,0 @@
namespace API.Schema;
public enum MangaReleaseStatus : byte
{
Continuing = 0,
Completed = 1,
OnHiatus = 2,
Cancelled = 3,
Unreleased = 4
}

View File

@ -17,7 +17,7 @@ public class MetadataEntry
public MetadataEntry(MetadataFetcher fetcher, Manga manga, string identifier)
{
this.Manga = manga;
this.MangaId = manga.MangaId;
this.MangaId = manga.Key;
this.MetadataFetcher = fetcher;
this.MetadataFetcherName = fetcher.MetadataFetcherName;
this.Identifier = identifier;

View File

@ -64,7 +64,7 @@ public class MyAnimeList : MetadataFetcher
dbManga.Name = resultData.Titles.First().Title;
dbManga.Description = resultData.Synopsis;
dbManga.AltTitles.Clear();
dbManga.AltTitles = resultData.Titles.Select(t => new MangaAltTitle(t.Type, t.Title)).ToList();
dbManga.AltTitles = resultData.Titles.Select(t => new AltTitle(t.Type, t.Title)).ToList();
dbManga.Authors.Clear();
dbManga.Authors = resultData.Authors.Select(a => new Author(a.Name)).ToList();

View File

@ -139,18 +139,7 @@ public static class Tranga
List<Job> dueJobs = waitingJobs.FilterDueJobs();
List<Job> jobsWithoutDependencies = dueJobs.FilterJobDependencies();
List<Job> jobsWithoutDownloading = jobsWithoutDependencies.FilterJobsWithoutDownloading();
//Match running and waiting jobs per Connector
Dictionary<string, Dictionary<JobType, List<Job>>> runningJobsPerConnector =
runningJobs.GetJobsPerJobTypeAndConnector();
Dictionary<string, Dictionary<JobType, List<Job>>> waitingJobsPerConnector =
jobsWithoutDependencies.GetJobsPerJobTypeAndConnector();
List<Job> jobsNotHeldBackByConnector =
MatchJobsRunningAndWaiting(runningJobsPerConnector, waitingJobsPerConnector);
List<Job> startJobs = jobsWithoutDownloading.Concat(jobsNotHeldBackByConnector).ToList();
List<Job> startJobs = dueJobs;
Log.Debug($"Jobs Filtered! (took {DateTime.UtcNow.Subtract(filterStart).TotalMilliseconds}ms)");
@ -162,7 +151,7 @@ public static class Tranga
{
using IServiceScope jobScope = serviceProvider.CreateScope();
PgsqlContext jobContext = jobScope.ServiceProvider.GetRequiredService<PgsqlContext>();
if (jobContext.Jobs.Find(job.JobId) is not { } inContext)
if (jobContext.Jobs.Find(job.Key) is not { } inContext)
return;
inContext.Run(jobContext, ref running); //FIND the job IN THE NEW CONTEXT!!!!!!! SO WE DON'T GET TRACKING PROBLEMS AND AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
});
@ -176,14 +165,12 @@ public static class Tranga
$"Waiting: {waitingJobs.Count} Due: {dueJobs.Count}\n" +
$"{string.Join("\n", dueJobs.Select(s => "\t- " + s))}\n" +
$"of which {jobsWithoutDependencies.Count} without missing dependencies, of which\n" +
$"\t{jobsWithoutDownloading.Count} without downloading\n" +
$"\t{jobsNotHeldBackByConnector.Count} not held back by Connector\n" +
$"{startJobs.Count} were started:\n" +
$"{string.Join("\n", startJobs.Select(s => "\t- " + s))}");
if (Log.IsDebugEnabled && dueJobs.Count < 1)
if(waitingJobs.MinBy(j => j.NextExecution) is { } nextJob)
Log.Debug($"Next job in {nextJob.NextExecution.Subtract(DateTime.UtcNow)} (at {nextJob.NextExecution}): {nextJob.JobId}");
Log.Debug($"Next job in {nextJob.NextExecution.Subtract(DateTime.UtcNow)} (at {nextJob.NextExecution}): {nextJob.Key}");
(Thread, Job)[] removeFromThreadsList = RunningJobs.Where(t => !t.Key.IsAlive)
.Select(t => (t.Key, t.Value)).ToArray();
@ -255,36 +242,20 @@ public static class Tranga
return ret;
}
private static Dictionary<string, Dictionary<JobType, List<Job>>> GetJobsPerJobTypeAndConnector(this List<Job> jobs)
{
DateTime start = DateTime.UtcNow;
Dictionary<string, Dictionary<JobType, List<Job>>> ret = new();
foreach (Job job in jobs)
{
if(GetJobConnectorName(job) is not { } connector)
continue;
if (!ret.ContainsKey(connector))
ret.Add(connector, new());
if (!ret[connector].ContainsKey(job.JobType))
ret[connector].Add(job.JobType, new());
ret[connector][job.JobType].Add(job);
}
DateTime end = DateTime.UtcNow;
Log.Debug($"Fetching connector per Job for jobs took {end.Subtract(start).TotalMilliseconds}ms");
return ret;
}
private static List<Job> MatchJobsRunningAndWaiting(Dictionary<string, Dictionary<JobType, List<Job>>> running,
Dictionary<string, Dictionary<JobType, List<Job>>> waiting)
{
Log.Debug($"Matching {running.Count} running Jobs to {waiting.Count} waiting Jobs. Busy Connectors: {string.Join(", ", running.Select(r => r.Key))}");
DateTime start = DateTime.UtcNow;
List<Job> ret = new();
//Foreach MangaConnector
foreach ((string connector, Dictionary<JobType, List<Job>> jobTypeJobsWaiting) in waiting)
{
//Check if MangaConnector has a Job running
if (running.TryGetValue(connector, out Dictionary<JobType, List<Job>>? jobTypeJobsRunning))
{ //MangaConnector has running Jobs
//Match per JobType
{
//MangaConnector has running Jobs
//Match per JobType (MangaConnector can have 1 Job per Type running at the same time)
foreach ((JobType jobType, List<Job> jobsWaiting) in jobTypeJobsWaiting)
{
if(jobTypeJobsRunning.ContainsKey(jobType))
@ -299,9 +270,13 @@ public static class Tranga
}
}
else
{ //MangaConnector has no running Jobs
{
//MangaConnector has no running Jobs
foreach ((JobType jobType, List<Job> jobsWaiting) in jobTypeJobsWaiting)
{
if(ret.Any(j => j.JobType == jobType))
//Already a job of type to be started
continue;
if (jobType is not JobType.DownloadSingleChapterJob)
//If it is not a DownloadSingleChapterJob, just add the first
ret.Add(jobsWaiting.First());
@ -315,18 +290,4 @@ public static class Tranga
Log.Debug($"Getting eligible jobs (not held back by Connector) took {end.Subtract(start).TotalMilliseconds}ms");
return ret;
}
private static string? GetJobConnectorName(Job job)
{
if (job is DownloadAvailableChaptersJob dacj)
return dacj.Manga.MangaConnectorName;
if (job is DownloadMangaCoverJob dmcj)
return dmcj.Manga.MangaConnectorName;
if (job is DownloadSingleChapterJob dscj)
return dscj.Chapter.ParentManga.MangaConnectorName;
if (job is RetrieveChaptersJob rcj)
return rcj.Manga.MangaConnectorName;
return null;
}
}

View File

@ -8,7 +8,7 @@ namespace API;
public static class TrangaSettings
{
public static string downloadLocation { get; private set; } = (RuntimeInformation.IsOSPlatform(OSPlatform.Linux) ? "/Manga" : Path.Join(Directory.GetCurrentDirectory(), "Downloads"));
public static string downloadLocation { get; private set; } = (RuntimeInformation.IsOSPlatform(OSPlatform.Linux) ? "/Obj" : Path.Join(Directory.GetCurrentDirectory(), "Downloads"));
public static string workingDirectory { get; private set; } = Path.Join(RuntimeInformation.IsOSPlatform(OSPlatform.Linux) ? "/usr/share" : Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "tranga-api");
[JsonIgnore]
internal static readonly string DefaultUserAgent = $"Tranga/2.0 ({Enum.GetName(Environment.OSVersion.Platform)}; {(Environment.Is64BitOperatingSystem ? "x64" : "")})";
@ -18,14 +18,14 @@ public static class TrangaSettings
public static string flareSolverrUrl { get; private set; } = string.Empty;
/// <summary>
/// Placeholders:
/// %M Manga Name
/// %M Obj Name
/// %V Volume
/// %C Chapter
/// %T Title
/// %A Author (first in list)
/// %I Chapter Internal ID
/// %i Manga Internal ID
/// %Y Year (Manga)
/// %i Obj Internal ID
/// %Y Year (Obj)
///
/// ?_(...) replace _ with a value from above:
/// Everything inside the braces will only be added if the value of %_ is not null

BIN
DB-Layout.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

460
DB-Layout.uxf Normal file
View File

@ -0,0 +1,460 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<diagram program="umlet" version="15.1">
<zoom_level>10</zoom_level>
<element>
<id>UMLClass</id>
<coordinates>
<x>1160</x>
<y>680</y>
<w>100</w>
<h>40</h>
</coordinates>
<panel_attributes>Manga</panel_attributes>
<additional_attributes/>
</element>
<element>
<id>UMLClass</id>
<coordinates>
<x>900</x>
<y>680</y>
<w>140</w>
<h>40</h>
</coordinates>
<panel_attributes>MangaConnector</panel_attributes>
<additional_attributes/>
</element>
<element>
<id>UMLClass</id>
<coordinates>
<x>500</x>
<y>800</y>
<w>80</w>
<h>40</h>
</coordinates>
<panel_attributes>/Job/</panel_attributes>
<additional_attributes/>
</element>
<element>
<id>UMLClass</id>
<coordinates>
<x>680</x>
<y>800</y>
<w>160</w>
<h>40</h>
</coordinates>
<panel_attributes>/JobWithDownload/</panel_attributes>
<additional_attributes/>
</element>
<element>
<id>UMLClass</id>
<coordinates>
<x>680</x>
<y>680</y>
<w>160</w>
<h>40</h>
</coordinates>
<panel_attributes>RetrieveChaptersJob
</panel_attributes>
<additional_attributes/>
</element>
<element>
<id>Relation</id>
<coordinates>
<x>570</x>
<y>810</y>
<w>130</w>
<h>30</h>
</coordinates>
<panel_attributes>lt=&lt;&lt;-</panel_attributes>
<additional_attributes>10.0;10.0;110.0;10.0</additional_attributes>
</element>
<element>
<id>Relation</id>
<coordinates>
<x>750</x>
<y>710</y>
<w>30</w>
<h>110</h>
</coordinates>
<panel_attributes>lt=&lt;&lt;-</panel_attributes>
<additional_attributes>10.0;90.0;10.0;10.0</additional_attributes>
</element>
<element>
<id>Relation</id>
<coordinates>
<x>1170</x>
<y>710</y>
<w>30</w>
<h>230</h>
</coordinates>
<panel_attributes>lt=-</panel_attributes>
<additional_attributes>10.0;210.0;10.0;10.0</additional_attributes>
</element>
<element>
<id>Relation</id>
<coordinates>
<x>1030</x>
<y>820</y>
<w>150</w>
<h>140</h>
</coordinates>
<panel_attributes>lt=-</panel_attributes>
<additional_attributes>130.0;120.0;70.0;120.0;70.0;10.0;10.0;10.0</additional_attributes>
</element>
<element>
<id>Relation</id>
<coordinates>
<x>960</x>
<y>710</y>
<w>30</w>
<h>110</h>
</coordinates>
<panel_attributes>lt=-</panel_attributes>
<additional_attributes>10.0;90.0;10.0;10.0</additional_attributes>
</element>
<element>
<id>Relation</id>
<coordinates>
<x>830</x>
<y>810</y>
<w>90</w>
<h>30</h>
</coordinates>
<panel_attributes>lt=-</panel_attributes>
<additional_attributes>70.0;10.0;10.0;10.0</additional_attributes>
</element>
<element>
<id>UMLClass</id>
<coordinates>
<x>1410</x>
<y>680</y>
<w>100</w>
<h>40</h>
</coordinates>
<panel_attributes>FileLibrary</panel_attributes>
<additional_attributes/>
</element>
<element>
<id>Relation</id>
<coordinates>
<x>1250</x>
<y>690</y>
<w>180</w>
<h>30</h>
</coordinates>
<panel_attributes>lt=-</panel_attributes>
<additional_attributes>160.0;10.0;10.0;10.0</additional_attributes>
</element>
<element>
<id>UMLClass</id>
<coordinates>
<x>1410</x>
<y>620</y>
<w>100</w>
<h>40</h>
</coordinates>
<panel_attributes>Link</panel_attributes>
<additional_attributes/>
</element>
<element>
<id>UMLClass</id>
<coordinates>
<x>1410</x>
<y>560</y>
<w>100</w>
<h>40</h>
</coordinates>
<panel_attributes>Author</panel_attributes>
<additional_attributes/>
</element>
<element>
<id>UMLClass</id>
<coordinates>
<x>1410</x>
<y>500</y>
<w>100</w>
<h>40</h>
</coordinates>
<panel_attributes>MangaTag</panel_attributes>
<additional_attributes/>
</element>
<element>
<id>Relation</id>
<coordinates>
<x>1200</x>
<y>510</y>
<w>230</w>
<h>190</h>
</coordinates>
<panel_attributes>lt=-</panel_attributes>
<additional_attributes>210.0;10.0;10.0;10.0;10.0;170.0</additional_attributes>
</element>
<element>
<id>Relation</id>
<coordinates>
<x>1230</x>
<y>570</y>
<w>200</w>
<h>130</h>
</coordinates>
<panel_attributes>lt=-</panel_attributes>
<additional_attributes>180.0;10.0;10.0;10.0;10.0;110.0</additional_attributes>
</element>
<element>
<id>Relation</id>
<coordinates>
<x>1250</x>
<y>630</y>
<w>180</w>
<h>70</h>
</coordinates>
<panel_attributes>lt=-</panel_attributes>
<additional_attributes>160.0;10.0;90.0;10.0;90.0;50.0;10.0;50.0</additional_attributes>
</element>
<element>
<id>UMLClass</id>
<coordinates>
<x>1410</x>
<y>440</y>
<w>100</w>
<h>40</h>
</coordinates>
<panel_attributes>AltTitle</panel_attributes>
<additional_attributes/>
</element>
<element>
<id>Relation</id>
<coordinates>
<x>1170</x>
<y>450</y>
<w>260</w>
<h>250</h>
</coordinates>
<panel_attributes>lt=-</panel_attributes>
<additional_attributes>240.0;10.0;10.0;10.0;10.0;230.0</additional_attributes>
</element>
<element>
<id>UMLClass</id>
<coordinates>
<x>1380</x>
<y>800</y>
<w>160</w>
<h>40</h>
</coordinates>
<panel_attributes>MangaMetadataEntry</panel_attributes>
<additional_attributes/>
</element>
<element>
<id>Relation</id>
<coordinates>
<x>1230</x>
<y>710</y>
<w>170</w>
<h>130</h>
</coordinates>
<panel_attributes>lt=-</panel_attributes>
<additional_attributes>150.0;110.0;10.0;110.0;10.0;10.0</additional_attributes>
</element>
<element>
<id>UMLClass</id>
<coordinates>
<x>1650</x>
<y>800</y>
<w>140</w>
<h>40</h>
</coordinates>
<panel_attributes>MetadataFetcher</panel_attributes>
<additional_attributes/>
</element>
<element>
<id>Relation</id>
<coordinates>
<x>1530</x>
<y>810</y>
<w>140</w>
<h>30</h>
</coordinates>
<panel_attributes>lt=-</panel_attributes>
<additional_attributes>120.0;10.0;10.0;10.0</additional_attributes>
</element>
<element>
<id>UMLUseCase</id>
<coordinates>
<x>1660</x>
<y>680</y>
<w>120</w>
<h>40</h>
</coordinates>
<panel_attributes>Path</panel_attributes>
<additional_attributes/>
</element>
<element>
<id>Relation</id>
<coordinates>
<x>1500</x>
<y>690</y>
<w>180</w>
<h>30</h>
</coordinates>
<panel_attributes>lt=-</panel_attributes>
<additional_attributes>160.0;10.0;10.0;10.0</additional_attributes>
</element>
<element>
<id>UMLClass</id>
<coordinates>
<x>900</x>
<y>800</y>
<w>140</w>
<h>40</h>
</coordinates>
<panel_attributes>MangaConnectorID</panel_attributes>
<additional_attributes/>
</element>
<element>
<id>Relation</id>
<coordinates>
<x>1030</x>
<y>690</y>
<w>150</w>
<h>140</h>
</coordinates>
<panel_attributes>lt=-</panel_attributes>
<additional_attributes>130.0;10.0;70.0;10.0;70.0;120.0;10.0;120.0</additional_attributes>
</element>
<element>
<id>UMLClass</id>
<coordinates>
<x>1160</x>
<y>920</y>
<w>100</w>
<h>40</h>
</coordinates>
<panel_attributes>Chapter
</panel_attributes>
<additional_attributes/>
</element>
<element>
<id>UMLClass</id>
<coordinates>
<x>460</x>
<y>680</y>
<w>160</w>
<h>40</h>
</coordinates>
<panel_attributes>UpdateChapters
DownloadedJob</panel_attributes>
<additional_attributes/>
</element>
<element>
<id>Relation</id>
<coordinates>
<x>530</x>
<y>710</y>
<w>30</w>
<h>110</h>
</coordinates>
<panel_attributes>lt=&lt;&lt;-</panel_attributes>
<additional_attributes>10.0;90.0;10.0;10.0</additional_attributes>
</element>
<element>
<id>UMLClass</id>
<coordinates>
<x>1970</x>
<y>640</y>
<w>110</w>
<h>40</h>
</coordinates>
<panel_attributes>lw=2
Komga</panel_attributes>
<additional_attributes/>
</element>
<element>
<id>UMLClass</id>
<coordinates>
<x>1970</x>
<y>710</y>
<w>110</w>
<h>40</h>
</coordinates>
<panel_attributes>lw=2
Kavita</panel_attributes>
<additional_attributes/>
</element>
<element>
<id>UMLPackage</id>
<coordinates>
<x>1930</x>
<y>600</y>
<w>190</w>
<h>170</h>
</coordinates>
<panel_attributes>Library</panel_attributes>
<additional_attributes/>
</element>
<element>
<id>Relation</id>
<coordinates>
<x>1770</x>
<y>690</y>
<w>180</w>
<h>30</h>
</coordinates>
<panel_attributes>lt=-</panel_attributes>
<additional_attributes>160.0;10.0;10.0;10.0</additional_attributes>
</element>
<element>
<id>UMLClass</id>
<coordinates>
<x>710</x>
<y>910</y>
<w>100</w>
<h>30</h>
</coordinates>
<panel_attributes>/Identifiable/</panel_attributes>
<additional_attributes/>
</element>
<element>
<id>Relation</id>
<coordinates>
<x>750</x>
<y>830</y>
<w>30</w>
<h>100</h>
</coordinates>
<panel_attributes>lt=&lt;&lt;-</panel_attributes>
<additional_attributes>10.0;80.0;10.0;10.0</additional_attributes>
</element>
<element>
<id>Relation</id>
<coordinates>
<x>530</x>
<y>830</y>
<w>200</w>
<h>120</h>
</coordinates>
<panel_attributes>lt=&lt;&lt;-</panel_attributes>
<additional_attributes>180.0;100.0;10.0;100.0;10.0;10.0</additional_attributes>
</element>
<element>
<id>Relation</id>
<coordinates>
<x>750</x>
<y>930</y>
<w>480</w>
<h>110</h>
</coordinates>
<panel_attributes>lt=&lt;&lt;-</panel_attributes>
<additional_attributes>10.0;10.0;10.0;90.0;460.0;90.0;460.0;30.0</additional_attributes>
</element>
<element>
<id>Relation</id>
<coordinates>
<x>800</x>
<y>670</y>
<w>380</w>
<h>280</h>
</coordinates>
<panel_attributes>lt=&lt;&lt;-</panel_attributes>
<additional_attributes>10.0;260.0;260.0;260.0;260.0;10.0;360.0;10.0</additional_attributes>
</element>
</diagram>

View File

@ -156,6 +156,8 @@ Tranga is using a code-first Entity-Framework Core approach. If you modify the d
**A broad overview of where is what:**<br />
![Image](DB-Layout.png)
- `Program.cs` Configuration for ASP.NET, Swagger (also in `NamedSwaggerGenOptions.cs`)
- `Tranga.cs` Worker-Logic
- `Schema/` Entity-Framework

View File

@ -1,9 +1,11 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:Boolean x:Key="/Default/UserDictionary/Words/=altnames/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=authorsartists/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=comick/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Gotify/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=jikan/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=jjob/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=kitsu/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Komga/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=lunasea/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=mangakatana/@EntryIndexedValue">True</s:Boolean>
@ -11,5 +13,6 @@
<s:Boolean x:Key="/Default/UserDictionary/Words/=Mangasee/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Mangaworld/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Ntfy/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=solverr/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Taskmanager/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Tranga/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>