27 Commits

Author SHA1 Message Date
c5689557b3 Fix MangaConnectorId Chapters Cascade 2025-07-22 22:34:38 +02:00
c044532564 Add Maintenance Controller
- CleanupNoDownloadManga
2025-07-22 21:53:27 +02:00
383258ac87 Add Query to get similar Manga by name 2025-07-22 21:53:14 +02:00
de36ce9c16 Fix MangaDex null 2025-07-22 21:09:31 +02:00
eba79abf51 Fix merging of Manga
Fix ComickIo empty lists
2025-07-22 20:24:53 +02:00
ae20ad47a8 Additional Query to get only downloading Manga 2025-07-22 18:04:49 +02:00
691506b2d8 Fix Merge of Chapters 2025-07-22 18:00:50 +02:00
356a22d72e Fix Merge of Manga 2025-07-22 17:59:25 +02:00
5d8f203a35 Add Queries for MangaConnectorIds 2025-07-22 15:56:57 +02:00
17995d1603 Fix DownloadChapterFromMangaconnectorWorker trying to download even if Library is not set 2025-07-22 13:49:18 +02:00
d2f9ab64aa Fix return of GET MangaConnector 2025-07-21 19:52:03 +02:00
d79dd8c3d5 Fix CancellationToken Source crashing all Workers after 10 Minutes of runtime 2025-07-21 19:32:29 +02:00
2b527e15b0 Fix NotificationConnectors 2025-07-21 16:39:18 +02:00
305b9d900c NamedSwaggerGenOptions.cs 2025-07-21 14:01:59 +02:00
413fb0e69e More Logging 2025-07-21 13:47:21 +02:00
64b89d4537 Do not use a Thread to Periodically check for Due workers.
Each Periodic Worker has it's own Thread that waits for execution.
2025-07-21 13:45:39 +02:00
fab70501ce Fix Concurrency of DownloadClient LastExecutedRateLimit 2025-07-21 13:06:35 +02:00
0a1021b488 Add UpdateCoversWorker 2025-07-21 12:45:29 +02:00
18c6f64405 Download Cover when adding Manga 2025-07-21 12:38:00 +02:00
5202bfe782 Remove non-periodic workers after they finish 2025-07-21 12:35:18 +02:00
8408407a8e Search for Manga on different MangaConnector 2025-07-21 12:32:20 +02:00
a091250195 Context Load Navigations and Collections 2025-07-21 12:24:54 +02:00
a352495866 GET Workers return IDs 2025-07-21 11:53:23 +02:00
3a46d0fd24 Disable LazyLoading
Remove MangaConnectors from Database
2025-07-21 11:42:21 +02:00
6034937c23 Indent TrangaSettings 2025-07-21 11:15:17 +02:00
454f468fd4 Move Configuration of Workers to separate method 2025-07-20 19:19:06 +02:00
1ee442ea2e Rename methods for workers from old Job 2025-07-20 19:12:28 +02:00
38 changed files with 698 additions and 597 deletions

View File

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

View File

@@ -0,0 +1,36 @@
using API.MangaConnectors;
using API.Schema.MangaContext;
using Asp.Versioning;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using static Microsoft.AspNetCore.Http.StatusCodes;
namespace API.Controllers;
[ApiVersion(2)]
[ApiController]
[Route("v{v:apiVersion}/[controller]")]
public class MaintenanceController(MangaContext mangaContext) : Controller
{
/// <summary>
/// Removes all <see cref="Manga"/> not marked for Download on any <see cref="MangaConnector"/>
/// </summary>
/// <response code="200"></response>
/// <response code="500">Error during Database Operation</response>
[HttpPost("CleanupNoDownloadManga")]
[ProducesResponseType(Status200OK)]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public IActionResult CleanupNoDownloadManga()
{
Manga[] noDownloads = mangaContext.Mangas
.Include(m => m.MangaConnectorIds)
.Where(m => !m.MangaConnectorIds.Any(id => id.UseForDownload))
.ToArray();
mangaContext.Mangas.RemoveRange(noDownloads);
if(mangaContext.Sync() is { success: false } result)
return StatusCode(Status500InternalServerError, result.exceptionMessage);
return Ok();
}
}

View File

@@ -1,5 +1,5 @@
using API.Schema.MangaContext; using API.MangaConnectors;
using API.Schema.MangaContext.MangaConnectors; using API.Schema.MangaContext;
using Asp.Versioning; using Asp.Versioning;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using static Microsoft.AspNetCore.Http.StatusCodes; using static Microsoft.AspNetCore.Http.StatusCodes;
@@ -20,7 +20,7 @@ public class MangaConnectorController(MangaContext context) : Controller
[ProducesResponseType<MangaConnector[]>(Status200OK, "application/json")] [ProducesResponseType<MangaConnector[]>(Status200OK, "application/json")]
public IActionResult GetConnectors() public IActionResult GetConnectors()
{ {
return Ok(context.MangaConnectors.Select(c => c.Name).ToArray()); return Ok(Tranga.MangaConnectors.ToArray());
} }
/// <summary> /// <summary>
@@ -34,7 +34,7 @@ public class MangaConnectorController(MangaContext context) : Controller
[ProducesResponseType(Status404NotFound)] [ProducesResponseType(Status404NotFound)]
public IActionResult GetConnector(string MangaConnectorName) public IActionResult GetConnector(string MangaConnectorName)
{ {
if(context.MangaConnectors.Find(MangaConnectorName) is not { } connector) if(Tranga.MangaConnectors.FirstOrDefault(c => c.Name.Equals(MangaConnectorName, StringComparison.InvariantCultureIgnoreCase)) is not { } connector)
return NotFound(); return NotFound();
return Ok(connector); return Ok(connector);
@@ -48,8 +48,7 @@ public class MangaConnectorController(MangaContext context) : Controller
[ProducesResponseType<MangaConnector[]>(Status200OK, "application/json")] [ProducesResponseType<MangaConnector[]>(Status200OK, "application/json")]
public IActionResult GetEnabledConnectors() public IActionResult GetEnabledConnectors()
{ {
return Ok(Tranga.MangaConnectors.Where(c => c.Enabled).ToArray());
return Ok(context.MangaConnectors.Where(c => c.Enabled).ToArray());
} }
/// <summary> /// <summary>
@@ -61,7 +60,7 @@ public class MangaConnectorController(MangaContext context) : Controller
public IActionResult GetDisabledConnectors() public IActionResult GetDisabledConnectors()
{ {
return Ok(context.MangaConnectors.Where(c => c.Enabled == false).ToArray()); return Ok(Tranga.MangaConnectors.Where(c => c.Enabled == false).ToArray());
} }
/// <summary> /// <summary>
@@ -78,7 +77,7 @@ public class MangaConnectorController(MangaContext context) : Controller
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")] [ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public IActionResult SetEnabled(string MangaConnectorName, bool Enabled) public IActionResult SetEnabled(string MangaConnectorName, bool Enabled)
{ {
if(context.MangaConnectors.Find(MangaConnectorName) is not { } connector) if(Tranga.MangaConnectors.FirstOrDefault(c => c.Name.Equals(MangaConnectorName, StringComparison.InvariantCultureIgnoreCase)) is not { } connector)
return NotFound(); return NotFound();
connector.Enabled = Enabled; connector.Enabled = Enabled;

View File

@@ -1,8 +1,10 @@
using API.Schema.MangaContext; using API.MangaConnectors;
using API.Schema.MangaContext.MangaConnectors; using API.Schema.MangaContext;
using API.Workers; using API.Workers;
using Asp.Versioning; using Asp.Versioning;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.ChangeTracking;
using Microsoft.Net.Http.Headers; using Microsoft.Net.Http.Headers;
using SixLabors.ImageSharp; using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Formats.Jpeg; using SixLabors.ImageSharp.Formats.Jpeg;
@@ -21,12 +23,12 @@ public class MangaController(MangaContext context) : Controller
/// <summary> /// <summary>
/// Returns all cached <see cref="Manga"/> /// Returns all cached <see cref="Manga"/>
/// </summary> /// </summary>
/// <response code="200"></response> /// <response code="200"><see cref="Manga"/> Keys/IDs</response>
[HttpGet] [HttpGet]
[ProducesResponseType<Manga[]>(Status200OK, "application/json")] [ProducesResponseType<string[]>(Status200OK, "application/json")]
public IActionResult GetAllManga() public IActionResult GetAllManga()
{ {
Manga[] ret = context.Mangas.ToArray(); string[] ret = context.Mangas.Select(m => m.Key).ToArray();
return Ok(ret); return Ok(ret);
} }
@@ -39,7 +41,7 @@ public class MangaController(MangaContext context) : Controller
[ProducesResponseType<Manga[]>(Status200OK, "application/json")] [ProducesResponseType<Manga[]>(Status200OK, "application/json")]
public IActionResult GetManga([FromBody]string[] MangaIds) public IActionResult GetManga([FromBody]string[] MangaIds)
{ {
Manga[] ret = context.Mangas.Where(m => MangaIds.Contains(m.Key)).ToArray(); Manga[] ret = context.MangaIncludeAll().Where(m => MangaIds.Contains(m.Key)).ToArray();
return Ok(ret); return Ok(ret);
} }
@@ -54,7 +56,7 @@ public class MangaController(MangaContext context) : Controller
[ProducesResponseType(Status404NotFound)] [ProducesResponseType(Status404NotFound)]
public IActionResult GetManga(string MangaId) public IActionResult GetManga(string MangaId)
{ {
if (context.Mangas.Find(MangaId) is not { } manga) if (context.MangaIncludeAll().FirstOrDefault(m => m.Key == MangaId) is not { } manga)
return NotFound(nameof(MangaId)); return NotFound(nameof(MangaId));
return Ok(manga); return Ok(manga);
} }
@@ -64,7 +66,7 @@ public class MangaController(MangaContext context) : Controller
/// </summary> /// </summary>
/// <param name="MangaId"><see cref="Manga"/>.Key</param> /// <param name="MangaId"><see cref="Manga"/>.Key</param>
/// <response code="200"></response> /// <response code="200"></response>
/// <response code="404"><<see cref="Manga"/> with <paramref name="MangaId"/> not found</response> /// <response code="404"><see cref="Manga"/> with <paramref name="MangaId"/> not found</response>
/// <response code="500">Error during Database Operation</response> /// <response code="500">Error during Database Operation</response>
[HttpDelete("{MangaId}")] [HttpDelete("{MangaId}")]
[ProducesResponseType(Status200OK)] [ProducesResponseType(Status200OK)]
@@ -100,6 +102,15 @@ public class MangaController(MangaContext context) : Controller
if (context.Mangas.Find(MangaIdInto) is not { } into) if (context.Mangas.Find(MangaIdInto) is not { } into)
return NotFound(nameof(MangaIdInto)); return NotFound(nameof(MangaIdInto));
foreach (CollectionEntry collectionEntry in context.Entry(from).Collections)
collectionEntry.Load();
context.Entry(from).Navigation(nameof(Manga.Library)).Load();
foreach (CollectionEntry collectionEntry in context.Entry(into).Collections)
collectionEntry.Load();
context.Entry(into).Navigation(nameof(Manga.Library)).Load();
BaseWorker[] newJobs = into.MergeFrom(from, context); BaseWorker[] newJobs = into.MergeFrom(from, context);
Tranga.AddWorkers(newJobs); Tranga.AddWorkers(newJobs);
@@ -173,6 +184,8 @@ public class MangaController(MangaContext context) : Controller
if (context.Mangas.Find(MangaId) is not { } manga) if (context.Mangas.Find(MangaId) is not { } manga)
return NotFound(nameof(MangaId)); return NotFound(nameof(MangaId));
context.Entry(manga).Collection(m => m.Chapters).Load();
Chapter[] chapters = manga.Chapters.ToArray(); Chapter[] chapters = manga.Chapters.ToArray();
return Ok(chapters); return Ok(chapters);
} }
@@ -193,6 +206,8 @@ public class MangaController(MangaContext context) : Controller
if (context.Mangas.Find(MangaId) is not { } manga) if (context.Mangas.Find(MangaId) is not { } manga)
return NotFound(nameof(MangaId)); return NotFound(nameof(MangaId));
context.Entry(manga).Collection(m => m.Chapters).Load();
List<Chapter> chapters = manga.Chapters.Where(c => c.Downloaded).ToList(); List<Chapter> chapters = manga.Chapters.Where(c => c.Downloaded).ToList();
if (chapters.Count == 0) if (chapters.Count == 0)
return NoContent(); return NoContent();
@@ -216,6 +231,8 @@ public class MangaController(MangaContext context) : Controller
if (context.Mangas.Find(MangaId) is not { } manga) if (context.Mangas.Find(MangaId) is not { } manga)
return NotFound(nameof(MangaId)); return NotFound(nameof(MangaId));
context.Entry(manga).Collection(m => m.Chapters).Load();
List<Chapter> chapters = manga.Chapters.Where(c => c.Downloaded == false).ToList(); List<Chapter> chapters = manga.Chapters.Where(c => c.Downloaded == false).ToList();
if (chapters.Count == 0) if (chapters.Count == 0)
return NoContent(); return NoContent();
@@ -243,6 +260,8 @@ public class MangaController(MangaContext context) : Controller
if (context.Mangas.Find(MangaId) is not { } manga) if (context.Mangas.Find(MangaId) is not { } manga)
return NotFound(nameof(MangaId)); return NotFound(nameof(MangaId));
context.Entry(manga).Collection(m => m.Chapters).Load();
List<Chapter> chapters = manga.Chapters.ToList(); List<Chapter> chapters = manga.Chapters.ToList();
if (chapters.Count == 0) if (chapters.Count == 0)
{ {
@@ -258,6 +277,10 @@ public class MangaController(MangaContext context) : Controller
if (max is null) if (max is null)
return StatusCode(Status500InternalServerError, "Max chapter could not be found"); return StatusCode(Status500InternalServerError, "Max chapter could not be found");
foreach (CollectionEntry collectionEntry in context.Entry(max).Collections)
collectionEntry.Load();
context.Entry(max).Navigation(nameof(Chapter.ParentManga)).Load();
return Ok(max); return Ok(max);
} }
@@ -281,6 +304,8 @@ public class MangaController(MangaContext context) : Controller
if (context.Mangas.Find(MangaId) is not { } manga) if (context.Mangas.Find(MangaId) is not { } manga)
return NotFound(nameof(MangaId)); return NotFound(nameof(MangaId));
context.Entry(manga).Collection(m => m.Chapters).Load();
List<Chapter> chapters = manga.Chapters.ToList(); List<Chapter> chapters = manga.Chapters.ToList();
if (chapters.Count == 0) if (chapters.Count == 0)
{ {
@@ -296,6 +321,10 @@ public class MangaController(MangaContext context) : Controller
if (max is null) if (max is null)
return StatusCode(Status412PreconditionFailed, "Max chapter could not be found"); return StatusCode(Status412PreconditionFailed, "Max chapter could not be found");
foreach (CollectionEntry collectionEntry in context.Entry(max).Collections)
collectionEntry.Load();
context.Entry(max).Navigation(nameof(Chapter.ParentManga)).Load();
return Ok(max); return Ok(max);
} }
@@ -333,13 +362,17 @@ public class MangaController(MangaContext context) : Controller
[HttpPost("{MangaId}/ChangeLibrary/{LibraryId}")] [HttpPost("{MangaId}/ChangeLibrary/{LibraryId}")]
[ProducesResponseType(Status202Accepted)] [ProducesResponseType(Status202Accepted)]
[ProducesResponseType(Status404NotFound)] [ProducesResponseType(Status404NotFound)]
public IActionResult MoveFolder(string MangaId, string LibraryId) public IActionResult ChangeLibrary(string MangaId, string LibraryId)
{ {
if (context.Mangas.Find(MangaId) is not { } manga) if (context.Mangas.Find(MangaId) is not { } manga)
return NotFound(nameof(MangaId)); return NotFound(nameof(MangaId));
if(context.FileLibraries.Find(LibraryId) is not { } library) if(context.FileLibraries.Find(LibraryId) is not { } library)
return NotFound(nameof(LibraryId)); return NotFound(nameof(LibraryId));
foreach (CollectionEntry collectionEntry in context.Entry(manga).Collections)
collectionEntry.Load();
context.Entry(manga).Navigation(nameof(Manga.Library)).Load();
MoveMangaLibraryWorker moveLibrary = new(manga, library); MoveMangaLibraryWorker moveLibrary = new(manga, library);
Tranga.AddWorkers([moveLibrary]); Tranga.AddWorkers([moveLibrary]);
@@ -368,23 +401,80 @@ public class MangaController(MangaContext context) : Controller
{ {
if (context.Mangas.Find(MangaId) is null) if (context.Mangas.Find(MangaId) is null)
return NotFound(nameof(MangaId)); return NotFound(nameof(MangaId));
if(context.MangaConnectors.Find(MangaConnectorName) is null) if(!Tranga.TryGetMangaConnector(MangaConnectorName, out MangaConnector? mangaConnector))
return NotFound(nameof(MangaConnectorName)); return NotFound(nameof(MangaConnectorName));
if (context.MangaConnectorToManga.FirstOrDefault(id => id.MangaConnectorName == MangaConnectorName && id.ObjId == MangaId) is not { } mcId) if (context.MangaConnectorToManga
.FirstOrDefault(id => id.MangaConnectorName == MangaConnectorName && id.ObjId == MangaId)
is not { } mcId)
{
if(IsRequested) if(IsRequested)
return StatusCode(Status428PreconditionRequired, "Don't know how to download this Manga from MangaConnector"); return StatusCode(Status428PreconditionRequired, "Don't know how to download this Manga from MangaConnector");
else else
return StatusCode(Status412PreconditionFailed, "Not linked anyways."); return StatusCode(Status412PreconditionFailed, "Not linked anyways.");
}
mcId.UseForDownload = IsRequested; mcId.UseForDownload = IsRequested;
if(context.Sync() is { success: false } result) if(context.Sync() is { success: false } result)
return StatusCode(Status500InternalServerError, result.exceptionMessage); return StatusCode(Status500InternalServerError, result.exceptionMessage);
DownloadCoverFromMangaconnectorWorker downloadCover = new(mcId); DownloadCoverFromMangaconnectorWorker downloadCover = new(mcId);
RetrieveMangaChaptersFromMangaconnectorWorker retrieveChapters = new(mcId, Tranga.Settings.DownloadLanguage); RetrieveMangaChaptersFromMangaconnectorWorker retrieveChapters = new(mcId, Tranga.Settings.DownloadLanguage);
Tranga.AddWorkers([downloadCover, retrieveChapters]); Tranga.AddWorkers([downloadCover, retrieveChapters]);
return Ok(); return Ok();
} }
/// <summary>
/// Initiate a search for <see cref="Manga"/> on a different <see cref="MangaConnector"/>
/// </summary>
/// <param name="MangaId"><see cref="Manga"/> with <paramref name="MangaId"/></param>
/// <param name="MangaConnectorName"><see cref="MangaConnector"/>.Name</param>
/// <response code="200"></response>
/// <response code="404"><see cref="MangaConnector"/> with Name not found</response>
/// <response code="412"><see cref="MangaConnector"/> with Name is disabled</response>
[HttpPost("{MangaId}/SearchOn/{MangaConnectorName}")]
[ProducesResponseType<Manga[]>(Status200OK, "application/json")]
[ProducesResponseType(Status404NotFound)]
[ProducesResponseType(Status406NotAcceptable)]
public IActionResult SearchOnDifferentConnector(string MangaId, string MangaConnectorName)
{
if (context.Mangas.Find(MangaId) is not { } manga)
return NotFound(nameof(MangaId));
return new SearchController(context).SearchManga(MangaConnectorName, manga.Name);
}
/// <summary>
/// Returns all <see cref="Manga"/> which where Authored by <see cref="Author"/> with <paramref name="AuthorId"/>
/// </summary>
/// <param name="AuthorId"><see cref="Author"/>.Key</param>
/// <response code="200"></response>
/// <response code="404"><see cref="Author"/> with <paramref name="AuthorId"/></response>
[HttpGet("WithAuthorId/{AuthorId}")]
[ProducesResponseType<Manga[]>(Status200OK, "application/json")]
public IActionResult GetMangaWithAuthorIds(string AuthorId)
{
if (context.Authors.Find(AuthorId) is not { } author)
return NotFound();
return Ok(context.Mangas.Where(m => m.Authors.Contains(author)));
}
/// <summary>
/// Returns all <see cref="Manga"/> with <see cref="Tag"/>
/// </summary>
/// <param name="Tag"><see cref="Tag"/>.Tag</param>
/// <response code="200"></response>
/// <response code="404"><see cref="Tag"/> not found</response>
[HttpGet("WithTag/{Tag}")]
[ProducesResponseType<Manga[]>(Status200OK, "application/json")]
public IActionResult GetMangasWithTag(string Tag)
{
if (context.Tags.Find(Tag) is not { } tag)
return NotFound();
return Ok(context.Mangas.Where(m => m.MangaTags.Contains(tag)));
}
} }

View File

@@ -48,30 +48,29 @@ public class NotificationConnectorController(NotificationsContext context) : Con
/// Creates a new <see cref="NotificationConnector"/> /// Creates a new <see cref="NotificationConnector"/>
/// </summary> /// </summary>
/// <remarks>Formatting placeholders: "%title" and "%text" can be placed in url, header-values and body and will be replaced when notifications are sent</remarks> /// <remarks>Formatting placeholders: "%title" and "%text" can be placed in url, header-values and body and will be replaced when notifications are sent</remarks>
/// <response code="201"></response> /// <response code="200">ID of the new <see cref="NotificationConnector"/></response>
/// <response code="500">Error during Database Operation</response> /// <response code="500">Error during Database Operation</response>
[HttpPut] [HttpPut]
[ProducesResponseType(Status201Created)] [ProducesResponseType<string>(Status200OK, "text/plain")]
[ProducesResponseType(Status409Conflict)]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")] [ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public IActionResult CreateConnector([FromBody]NotificationConnector notificationConnector) public IActionResult CreateConnector([FromBody]NotificationConnector notificationConnector)
{ {
context.NotificationConnectors.Add(notificationConnector); context.NotificationConnectors.Add(notificationConnector);
context.Notifications.Add(new ("Added new Notification Connector!", notificationConnector.Name, NotificationUrgency.High));
if(context.Sync() is { success: false } result) if(context.Sync() is { success: false } result)
return StatusCode(Status500InternalServerError, result.exceptionMessage); return StatusCode(Status500InternalServerError, result.exceptionMessage);
return Created(); return Ok(notificationConnector.Name);
} }
/// <summary> /// <summary>
/// Creates a new Gotify-<see cref="NotificationConnector"/> /// Creates a new Gotify-<see cref="NotificationConnector"/>
/// </summary> /// </summary>
/// <remarks>Priority needs to be between 0 and 10</remarks> /// <remarks>Priority needs to be between 0 and 10</remarks>
/// <response code="201"></response> /// <response code="200">ID of the new <see cref="NotificationConnector"/></response>
/// <response code="500">Error during Database Operation</response> /// <response code="500">Error during Database Operation</response>
[HttpPut("Gotify")] [HttpPut("Gotify")]
[ProducesResponseType<string>(Status201Created, "application/json")] [ProducesResponseType<string>(Status200OK, "text/plain")]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")] [ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public IActionResult CreateGotifyConnector([FromBody]GotifyRecord gotifyData) public IActionResult CreateGotifyConnector([FromBody]GotifyRecord gotifyData)
{ {
@@ -79,7 +78,7 @@ public class NotificationConnectorController(NotificationsContext context) : Con
NotificationConnector gotifyConnector = new (gotifyData.Name, NotificationConnector gotifyConnector = new (gotifyData.Name,
gotifyData.Endpoint, gotifyData.Endpoint,
new Dictionary<string, string>() { { "X-Gotify-IDOnConnector", gotifyData.AppToken } }, new Dictionary<string, string>() { { "X-Gotify-Key", gotifyData.AppToken } },
"POST", "POST",
$"{{\"message\": \"%text\", \"title\": \"%title\", \"Priority\": {gotifyData.Priority}}}"); $"{{\"message\": \"%text\", \"title\": \"%title\", \"Priority\": {gotifyData.Priority}}}");
return CreateConnector(gotifyConnector); return CreateConnector(gotifyConnector);
@@ -89,10 +88,10 @@ public class NotificationConnectorController(NotificationsContext context) : Con
/// Creates a new Ntfy-<see cref="NotificationConnector"/> /// Creates a new Ntfy-<see cref="NotificationConnector"/>
/// </summary> /// </summary>
/// <remarks>Priority needs to be between 1 and 5</remarks> /// <remarks>Priority needs to be between 1 and 5</remarks>
/// <response code="201"></response> /// <response code="200">ID of the new <see cref="NotificationConnector"/></response>
/// <response code="500">Error during Database Operation</response> /// <response code="500">Error during Database Operation</response>
[HttpPut("Ntfy")] [HttpPut("Ntfy")]
[ProducesResponseType<string>(Status201Created, "application/json")] [ProducesResponseType<string>(Status200OK, "text/plain")]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")] [ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public IActionResult CreateNtfyConnector([FromBody]NtfyRecord ntfyRecord) public IActionResult CreateNtfyConnector([FromBody]NtfyRecord ntfyRecord)
{ {
@@ -102,14 +101,13 @@ public class NotificationConnectorController(NotificationsContext context) : Con
string auth = Convert.ToBase64String(Encoding.UTF8.GetBytes(authHeader)).Replace("=",""); string auth = Convert.ToBase64String(Encoding.UTF8.GetBytes(authHeader)).Replace("=","");
NotificationConnector ntfyConnector = new (ntfyRecord.Name, NotificationConnector ntfyConnector = new (ntfyRecord.Name,
$"{ntfyRecord.Endpoint}/{ntfyRecord.Topic}?auth={auth}", $"{ntfyRecord.Endpoint}?auth={auth}",
new Dictionary<string, string>() new Dictionary<string, string>()
{ {
{"Title", "%title"}, {"Authorization", auth}
{"Priority", ntfyRecord.Priority.ToString()},
}, },
"POST", "POST",
"%text"); $"{{\"message\": \"%text\", \"title\": \"%title\", \"Priority\": {ntfyRecord.Priority} \"Topic\": \"{ntfyRecord.Topic}\"}}");
return CreateConnector(ntfyConnector); return CreateConnector(ntfyConnector);
} }
@@ -117,10 +115,10 @@ public class NotificationConnectorController(NotificationsContext context) : Con
/// Creates a new Pushover-<see cref="NotificationConnector"/> /// Creates a new Pushover-<see cref="NotificationConnector"/>
/// </summary> /// </summary>
/// <remarks>https://pushover.net/api</remarks> /// <remarks>https://pushover.net/api</remarks>
/// <response code="201">ID of new connector</response> /// <response code="200">ID of the new <see cref="NotificationConnector"/></response>
/// <response code="500">Error during Database Operation</response> /// <response code="500">Error during Database Operation</response>
[HttpPut("Pushover")] [HttpPut("Pushover")]
[ProducesResponseType<string>(Status201Created, "application/json")] [ProducesResponseType<string>(Status200OK, "text/plain")]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")] [ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public IActionResult CreatePushoverConnector([FromBody]PushoverRecord pushoverRecord) public IActionResult CreatePushoverConnector([FromBody]PushoverRecord pushoverRecord)
{ {
@@ -154,6 +152,6 @@ public class NotificationConnectorController(NotificationsContext context) : Con
if(context.Sync() is { success: false } result) if(context.Sync() is { success: false } result)
return StatusCode(Status500InternalServerError, result.exceptionMessage); return StatusCode(Status500InternalServerError, result.exceptionMessage);
return Created(); return Ok();
} }
} }

View File

@@ -1,6 +1,8 @@
using API.Schema.MangaContext; using API.MangaConnectors;
using API.Schema.MangaContext;
using Asp.Versioning; using Asp.Versioning;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Soenneker.Utils.String.NeedlemanWunsch;
using static Microsoft.AspNetCore.Http.StatusCodes; using static Microsoft.AspNetCore.Http.StatusCodes;
// ReSharper disable InconsistentNaming // ReSharper disable InconsistentNaming
@@ -28,38 +30,6 @@ public class QueryController(MangaContext context) : Controller
return Ok(author); return Ok(author);
} }
/// <summary>
/// Returns all <see cref="Manga"/> which where Authored by <see cref="Author"/> with <paramref name="AuthorId"/>
/// </summary>
/// <param name="AuthorId"><see cref="Author"/>.Key</param>
/// <response code="200"></response>
/// <response code="404"><see cref="Author"/> with <paramref name="AuthorId"/></response>
[HttpGet("Mangas/WithAuthorId/{AuthorId}")]
[ProducesResponseType<Manga[]>(Status200OK, "application/json")]
public IActionResult GetMangaWithAuthorIds(string AuthorId)
{
if (context.Authors.Find(AuthorId) is not { } author)
return NotFound();
return Ok(context.Mangas.Where(m => m.Authors.Contains(author)));
}
/// <summary>
/// Returns all <see cref="Manga"/> with <see cref="Tag"/>
/// </summary>
/// <param name="Tag"><see cref="Tag"/>.Tag</param>
/// <response code="200"></response>
/// <response code="404"><see cref="Tag"/> not found</response>
[HttpGet("Mangas/WithTag/{Tag}")]
[ProducesResponseType<Manga[]>(Status200OK, "application/json")]
public IActionResult GetMangasWithTag(string Tag)
{
if (context.Tags.Find(Tag) is not { } tag)
return NotFound();
return Ok(context.Mangas.Where(m => m.MangaTags.Contains(tag)));
}
/// <summary> /// <summary>
/// Returns <see cref="Chapter"/> with <paramref name="ChapterId"/> /// Returns <see cref="Chapter"/> with <paramref name="ChapterId"/>
/// </summary> /// </summary>
@@ -68,6 +38,7 @@ public class QueryController(MangaContext context) : Controller
/// <response code="404"><see cref="Chapter"/> with <paramref name="ChapterId"/> not found</response> /// <response code="404"><see cref="Chapter"/> with <paramref name="ChapterId"/> not found</response>
[HttpGet("Chapter/{ChapterId}")] [HttpGet("Chapter/{ChapterId}")]
[ProducesResponseType<Chapter>(Status200OK, "application/json")] [ProducesResponseType<Chapter>(Status200OK, "application/json")]
[ProducesResponseType(Status404NotFound)]
public IActionResult GetChapter(string ChapterId) public IActionResult GetChapter(string ChapterId)
{ {
if (context.Chapters.Find(ChapterId) is not { } chapter) if (context.Chapters.Find(ChapterId) is not { } chapter)
@@ -75,4 +46,73 @@ public class QueryController(MangaContext context) : Controller
return Ok(chapter); return Ok(chapter);
} }
/// <summary>
/// Returns the <see cref="MangaConnectorId{Manga}"/> with <see cref="MangaConnectorId{Manga}"/>.Key
/// </summary>
/// <param name="MangaConnectorIdId">Key of <see cref="MangaConnectorId{Manga}"/></param>
/// <response code="200"></response>
/// <response code="404"><see cref="MangaConnectorId{Manga}"/> with <paramref name="MangaConnectorIdId"/> not found</response>
[HttpGet("Manga/MangaConnectorId/{MangaConnectorIdId}")]
[ProducesResponseType<MangaConnectorId<Manga>>(Status200OK, "application/json")]
[ProducesResponseType(Status404NotFound)]
public IActionResult GetMangaMangaConnectorId(string MangaConnectorIdId)
{
if(context.MangaConnectorToManga.Find(MangaConnectorIdId) is not { } mcIdManga)
return NotFound();
return Ok(mcIdManga);
}
/// <summary>
/// Returns all <see cref="Manga"/> that are being downloaded from at least one <see cref="MangaConnector"/>
/// </summary>
/// <response code="200"></response>
[HttpGet("Manga/Downloading")]
[ProducesResponseType<Manga[]>(Status200OK, "application/json")]
public IActionResult GetMangaDownloading()
{
Manga[] ret = context.MangaIncludeAll()
.Where(m => m.MangaConnectorIds.Any(id => id.UseForDownload))
.ToArray();
return Ok(ret);
}
/// <summary>
/// Returns <see cref="Manga"/> with names similar to <see cref="Manga"/> (identified by <paramref name="MangaId"/>
/// </summary>
/// <param name="MangaId">Key of <see cref="Manga"/></param>
/// <response code="200"></response>
/// <response code="404"><see cref="Manga"/> with <paramref name="MangaId"/> not found</response>
[HttpGet("Manga/{MangaId}/SimilarName")]
[ProducesResponseType<string[]>(Status200OK, "application/json")]
[ProducesResponseType(Status404NotFound)]
public IActionResult GetSimilarManga(string MangaId)
{
if (context.Mangas.Find(MangaId) is not { } manga)
return NotFound();
string name = manga.Name;
Dictionary<string, string> mangaNames = context.Mangas.Where(m => m.Key != MangaId).ToDictionary(m => m.Key, m => m.Name);
string[] similarIds = mangaNames
.Where(kv => NeedlemanWunschStringUtil.CalculateSimilarityPercentage(name, kv.Value) > 0.8)
.Select(kv => kv.Key).ToArray();
return Ok(similarIds);
}
/// <summary>
/// Returns the <see cref="MangaConnectorId{Chapter}"/> with <see cref="MangaConnectorId{Chapter}"/>.Key
/// </summary>
/// <param name="MangaConnectorIdId">Key of <see cref="MangaConnectorId{Manga}"/></param>
/// <response code="200"></response>
/// <response code="404"><see cref="MangaConnectorId{Chapter}"/> with <paramref name="MangaConnectorIdId"/> not found</response>
[HttpGet("Chapter/MangaConnectorId/{MangaConnectorIdId}")]
[ProducesResponseType<MangaConnectorId<Chapter>>(Status200OK, "application/json")]
[ProducesResponseType(Status404NotFound)]
public IActionResult GetChapterMangaConnectorId(string MangaConnectorIdId)
{
if(context.MangaConnectorToChapter.Find(MangaConnectorIdId) is not { } mcIdChapter)
return NotFound();
return Ok(mcIdChapter);
}
} }

View File

@@ -1,5 +1,5 @@
using API.MangaConnectors;
using API.Schema.MangaContext; using API.Schema.MangaContext;
using API.Schema.MangaContext.MangaConnectors;
using Asp.Versioning; using Asp.Versioning;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using static Microsoft.AspNetCore.Http.StatusCodes; using static Microsoft.AspNetCore.Http.StatusCodes;
@@ -26,7 +26,7 @@ public class SearchController(MangaContext context) : Controller
[ProducesResponseType(Status406NotAcceptable)] [ProducesResponseType(Status406NotAcceptable)]
public IActionResult SearchManga(string MangaConnectorName, string Query) public IActionResult SearchManga(string MangaConnectorName, string Query)
{ {
if(context.MangaConnectors.Find(MangaConnectorName) is not { } connector) if(Tranga.MangaConnectors.FirstOrDefault(c => c.Name.Equals(MangaConnectorName, StringComparison.InvariantCultureIgnoreCase)) is not { } connector)
return NotFound(); return NotFound();
if (connector.Enabled is false) if (connector.Enabled is false)
return StatusCode(Status412PreconditionFailed); return StatusCode(Status412PreconditionFailed);
@@ -56,7 +56,7 @@ public class SearchController(MangaContext context) : Controller
[ProducesResponseType(Status500InternalServerError)] [ProducesResponseType(Status500InternalServerError)]
public IActionResult GetMangaFromUrl([FromBody]string url) public IActionResult GetMangaFromUrl([FromBody]string url)
{ {
if (context.MangaConnectors.Find("Global") is not { } connector) if(Tranga.MangaConnectors.FirstOrDefault(c => c.Name.Equals("Global", StringComparison.InvariantCultureIgnoreCase)) is not { } connector)
return StatusCode(Status500InternalServerError, "Could not find Global Connector."); return StatusCode(Status500InternalServerError, "Could not find Global Connector.");
if(connector.GetMangaFromUrl(url) is not { } manga) if(connector.GetMangaFromUrl(url) is not { } manga)

View File

@@ -1,7 +1,6 @@
using API.APIEndpointRecords; using API.APIEndpointRecords;
using API.Workers; using API.Workers;
using Asp.Versioning; using Asp.Versioning;
using log4net;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using static Microsoft.AspNetCore.Http.StatusCodes; using static Microsoft.AspNetCore.Http.StatusCodes;
// ReSharper disable InconsistentNaming // ReSharper disable InconsistentNaming
@@ -14,14 +13,14 @@ namespace API.Controllers;
public class WorkerController() : Controller public class WorkerController() : Controller
{ {
/// <summary> /// <summary>
/// Returns all <see cref="BaseWorker"/> /// Returns all <see cref="BaseWorker"/>.Keys
/// </summary> /// </summary>
/// <response code="200"></response> /// <response code="200"><see cref="BaseWorker"/> Keys/IDs</response>
[HttpGet] [HttpGet]
[ProducesResponseType<BaseWorker[]>(Status200OK, "application/json")] [ProducesResponseType<string[]>(Status200OK, "application/json")]
public IActionResult GetAllWorkers() public IActionResult GetAllWorkers()
{ {
return Ok(Tranga.AllWorkers.ToArray()); return Ok(Tranga.GetRunningWorkers().Select(w => w.Key).ToArray());
} }
/// <summary> /// <summary>
@@ -31,9 +30,9 @@ public class WorkerController() : Controller
/// <response code="200"></response> /// <response code="200"></response>
[HttpPost("WithIDs")] [HttpPost("WithIDs")]
[ProducesResponseType<BaseWorker[]>(Status200OK, "application/json")] [ProducesResponseType<BaseWorker[]>(Status200OK, "application/json")]
public IActionResult GetJobs([FromBody]string[] WorkerIds) public IActionResult GetWorkers([FromBody]string[] WorkerIds)
{ {
return Ok(Tranga.AllWorkers.Where(worker => WorkerIds.Contains(worker.Key)).ToArray()); return Ok(Tranga.GetRunningWorkers().Where(worker => WorkerIds.Contains(worker.Key)).ToArray());
} }
/// <summary> /// <summary>
@@ -43,9 +42,9 @@ public class WorkerController() : Controller
/// <response code="200"></response> /// <response code="200"></response>
[HttpGet("State/{State}")] [HttpGet("State/{State}")]
[ProducesResponseType<BaseWorker[]>(Status200OK, "application/json")] [ProducesResponseType<BaseWorker[]>(Status200OK, "application/json")]
public IActionResult GetJobsInState(WorkerExecutionState State) public IActionResult GetWorkersInState(WorkerExecutionState State)
{ {
return Ok(Tranga.AllWorkers.Where(worker => worker.State == State).ToArray()); return Ok(Tranga.GetRunningWorkers().Where(worker => worker.State == State).ToArray());
} }
/// <summary> /// <summary>
@@ -57,9 +56,9 @@ public class WorkerController() : Controller
[HttpGet("{WorkerId}")] [HttpGet("{WorkerId}")]
[ProducesResponseType<BaseWorker>(Status200OK, "application/json")] [ProducesResponseType<BaseWorker>(Status200OK, "application/json")]
[ProducesResponseType(Status404NotFound)] [ProducesResponseType(Status404NotFound)]
public IActionResult GetJob(string WorkerId) public IActionResult GetWorker(string WorkerId)
{ {
if(Tranga.AllWorkers.FirstOrDefault(w => w.Key == WorkerId) is not { } worker) if(Tranga.GetRunningWorkers().FirstOrDefault(w => w.Key == WorkerId) is not { } worker)
return NotFound(nameof(WorkerId)); return NotFound(nameof(WorkerId));
return Ok(worker); return Ok(worker);
} }
@@ -73,41 +72,14 @@ public class WorkerController() : Controller
[HttpDelete("{WorkerId}")] [HttpDelete("{WorkerId}")]
[ProducesResponseType(Status200OK)] [ProducesResponseType(Status200OK)]
[ProducesResponseType(Status404NotFound)] [ProducesResponseType(Status404NotFound)]
public IActionResult DeleteJob(string WorkerId) public IActionResult DeleteWorker(string WorkerId)
{ {
if(Tranga.AllWorkers.FirstOrDefault(w => w.Key == WorkerId) is not { } worker) if(Tranga.GetRunningWorkers().FirstOrDefault(w => w.Key == WorkerId) is not { } worker)
return NotFound(nameof(WorkerId)); return NotFound(nameof(WorkerId));
Tranga.RemoveWorker(worker); Tranga.StopWorker(worker);
return Ok(); return Ok();
} }
/// <summary>
/// Modify <see cref="BaseWorker"/> with <paramref name="WorkerId"/>
/// </summary>
/// <param name="WorkerId"><see cref="BaseWorker"/>.Key</param>
/// <param name="modifyWorkerRecord">Fields to modify, set to null to keep previous value</param>
/// <response code="202"></response>
/// <response code="400"></response>
/// <response code="404"><see cref="BaseWorker"/> with <paramref name="WorkerId"/> could not be found</response>
/// <response code="409"><see cref="BaseWorker"/> is not <see cref="IPeriodic"/>, can not modify <paramref name="modifyWorkerRecord.IntervalMs"/></response>
[HttpPatch("{WorkerId}")]
[ProducesResponseType<BaseWorker>(Status202Accepted, "application/json")]
[ProducesResponseType(Status400BadRequest)]
[ProducesResponseType(Status404NotFound)]
[ProducesResponseType<string>(Status409Conflict, "text/plain")]
public IActionResult ModifyJob(string WorkerId, [FromBody]ModifyWorkerRecord modifyWorkerRecord)
{
if(Tranga.AllWorkers.FirstOrDefault(w => w.Key == WorkerId) is not { } worker)
return NotFound(nameof(WorkerId));
if(modifyWorkerRecord.IntervalMs is not null && worker is not IPeriodic)
return Conflict("Can not modify Interval of non-Periodic worker");
else if(modifyWorkerRecord.IntervalMs is not null && worker is IPeriodic periodic)
periodic.Interval = TimeSpan.FromMilliseconds((long)modifyWorkerRecord.IntervalMs);
return Accepted(worker);
}
/// <summary> /// <summary>
/// Starts <see cref="BaseWorker"/> with <paramref name="WorkerId"/> /// Starts <see cref="BaseWorker"/> with <paramref name="WorkerId"/>
/// </summary> /// </summary>
@@ -119,15 +91,15 @@ public class WorkerController() : Controller
[ProducesResponseType(Status202Accepted)] [ProducesResponseType(Status202Accepted)]
[ProducesResponseType(Status404NotFound)] [ProducesResponseType(Status404NotFound)]
[ProducesResponseType<string>(Status412PreconditionFailed, "text/plain")] [ProducesResponseType<string>(Status412PreconditionFailed, "text/plain")]
public IActionResult StartJob(string WorkerId) public IActionResult StartWorker(string WorkerId)
{ {
if(Tranga.AllWorkers.FirstOrDefault(w => w.Key == WorkerId) is not { } worker) if(Tranga.GetRunningWorkers().FirstOrDefault(w => w.Key == WorkerId) is not { } worker)
return NotFound(nameof(WorkerId)); return NotFound(nameof(WorkerId));
if (worker.State >= WorkerExecutionState.Waiting) if (worker.State >= WorkerExecutionState.Waiting)
return StatusCode(Status412PreconditionFailed, "Already running"); return StatusCode(Status412PreconditionFailed, "Already running");
Tranga.MarkWorkerForStart(worker); Tranga.StartWorker(worker);
return Ok(); return Ok();
} }
@@ -140,9 +112,9 @@ public class WorkerController() : Controller
/// <response code="208"><see cref="BaseWorker"/> was not running</response> /// <response code="208"><see cref="BaseWorker"/> was not running</response>
[HttpPost("{WorkerId}/Stop")] [HttpPost("{WorkerId}/Stop")]
[ProducesResponseType(Status501NotImplemented)] [ProducesResponseType(Status501NotImplemented)]
public IActionResult StopJob(string WorkerId) public IActionResult StopWorker(string WorkerId)
{ {
if(Tranga.AllWorkers.FirstOrDefault(w => w.Key == WorkerId) is not { } worker) if(Tranga.GetRunningWorkers().FirstOrDefault(w => w.Key == WorkerId) is not { } worker)
return NotFound(nameof(WorkerId)); return NotFound(nameof(WorkerId));
if(worker.State is < WorkerExecutionState.Running or >= WorkerExecutionState.Completed) if(worker.State is < WorkerExecutionState.Running or >= WorkerExecutionState.Completed)

View File

@@ -1,8 +1,9 @@
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using API.MangaDownloadClients; using API.MangaDownloadClients;
using API.Schema.MangaContext;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
namespace API.Schema.MangaContext.MangaConnectors; namespace API.MangaConnectors;
public class ComickIo : MangaConnector public class ComickIo : MangaConnector
{ {
@@ -176,19 +177,19 @@ public class ComickIo : MangaConnector
byte whatever = 0; byte whatever = 0;
List<AltTitle> altTitles = altTitlesArray? List<AltTitle> altTitles = altTitlesArray?
.Select(token => new AltTitle(token.Value<string>("lang")??whatever++.ToString(), token.Value<string>("title")!)) .Select(token => new AltTitle(token.Value<string>("lang")??whatever++.ToString(), token.Value<string>("title")!))
.ToList()!; .ToList()??[];
JArray? authorsArray = json["authors"] as JArray; JArray? authorsArray = json["authors"] as JArray;
JArray? artistsArray = json["artists"] as JArray; JArray? artistsArray = json["artists"] as JArray;
List<Author> authors = authorsArray?.Concat(artistsArray!) List<Author> authors = authorsArray?.Concat(artistsArray!)
.Select(token => new Author(token.Value<string>("name")!)) .Select(token => new Author(token.Value<string>("name")!))
.DistinctBy(a => a.Key) .DistinctBy(a => a.Key)
.ToList()!; .ToList()??[];
JArray? genreArray = json["comic"]?["md_comic_md_genres"] as JArray; JArray? genreArray = json["comic"]?["md_comic_md_genres"] as JArray;
List<MangaTag> tags = genreArray? List<MangaTag> tags = genreArray?
.Select(token => new MangaTag(token["md_genres"]?.Value<string>("name")!)) .Select(token => new MangaTag(token["md_genres"]?.Value<string>("name")!))
.ToList()!; .ToList()??[];
JArray? linksArray = json["comic"]?["links"] as JArray; JArray? linksArray = json["comic"]?["links"] as JArray;
List<Link> links = linksArray? List<Link> links = linksArray?
@@ -220,7 +221,7 @@ public class ComickIo : MangaConnector
_ => kv.Key _ => kv.Key
}; };
return new Link(key, fullUrl); return new Link(key, fullUrl);
}).ToList()!; }).ToList()??[];
if(hid is null) if(hid is null)
throw new Exception("hid is null"); throw new Exception("hid is null");
@@ -231,7 +232,9 @@ public class ComickIo : MangaConnector
Manga manga = new (name, description??"", coverUrl, status, authors, tags, links, altTitles, Manga manga = new (name, description??"", coverUrl, status, authors, tags, links, altTitles,
year: year, originalLanguage: originalLanguage); year: year, originalLanguage: originalLanguage);
return (manga, new MangaConnectorId<Manga>(manga, this, hid, url)); MangaConnectorId<Manga> mcId = new (manga, this, hid, url);
manga.MangaConnectorIds.Add(mcId);
return (manga, mcId);
} }
private List<(Chapter, MangaConnectorId<Chapter>)> ParseChapters(MangaConnectorId<Manga> mcIdManga, JArray chaptersArray) private List<(Chapter, MangaConnectorId<Chapter>)> ParseChapters(MangaConnectorId<Manga> mcIdManga, JArray chaptersArray)
@@ -250,8 +253,9 @@ public class ComickIo : MangaConnector
continue; continue;
Chapter ch = new (mcIdManga.Obj, chapterNum, volumeNum, title); Chapter ch = new (mcIdManga.Obj, chapterNum, volumeNum, title);
MangaConnectorId<Chapter> mcId = new(ch, this, hid, url);
chapters.Add((ch, new (ch, this, hid, url))); ch.MangaConnectorIds.Add(mcId);
chapters.Add((ch, mcId));
} }
return chapters; return chapters;
} }

View File

@@ -1,17 +1,17 @@
namespace API.Schema.MangaContext.MangaConnectors; using API.Schema.MangaContext;
namespace API.MangaConnectors;
public class Global : MangaConnector public class Global : MangaConnector
{ {
private MangaContext context { get; init; } public Global() : base("Global", ["all"], [""], "")
public Global(MangaContext context) : base("Global", ["all"], [""], "")
{ {
this.context = context;
} }
public override (Manga, MangaConnectorId<Manga>)[] SearchManga(string mangaSearchName) public override (Manga, MangaConnectorId<Manga>)[] SearchManga(string mangaSearchName)
{ {
//Get all enabled Connectors //Get all enabled Connectors
MangaConnector[] enabledConnectors = context.MangaConnectors.Where(c => c.Enabled && c.Name != "Global").ToArray(); MangaConnector[] enabledConnectors = Tranga.MangaConnectors.Where(c => c.Enabled && c.Name != "Global").ToArray();
//Create Task for each MangaConnector to search simultaneously //Create Task for each MangaConnector to search simultaneously
Task<(Manga, MangaConnectorId<Manga>)[]>[] tasks = Task<(Manga, MangaConnectorId<Manga>)[]>[] tasks =
@@ -32,7 +32,7 @@ public class Global : MangaConnector
public override (Manga, MangaConnectorId<Manga>)? GetMangaFromUrl(string url) public override (Manga, MangaConnectorId<Manga>)? GetMangaFromUrl(string url)
{ {
MangaConnector? mc = context.MangaConnectors.ToArray().FirstOrDefault(c => c.UrlMatchesConnector(url)); MangaConnector? mc = Tranga.MangaConnectors.FirstOrDefault(c => c.UrlMatchesConnector(url));
return mc?.GetMangaFromUrl(url) ?? null; return mc?.GetMangaFromUrl(url) ?? null;
} }
@@ -44,11 +44,15 @@ public class Global : MangaConnector
public override (Chapter, MangaConnectorId<Chapter>)[] GetChapters(MangaConnectorId<Manga> manga, public override (Chapter, MangaConnectorId<Chapter>)[] GetChapters(MangaConnectorId<Manga> manga,
string? language = null) string? language = null)
{ {
return manga.MangaConnector.GetChapters(manga, language); if (!Tranga.TryGetMangaConnector(manga.MangaConnectorName, out MangaConnector? mangaConnector))
return [];
return mangaConnector.GetChapters(manga, language);
} }
internal override string[] GetChapterImageUrls(MangaConnectorId<Chapter> chapterId) internal override string[] GetChapterImageUrls(MangaConnectorId<Chapter> chapterId)
{ {
return chapterId.MangaConnector.GetChapterImageUrls(chapterId); if (!Tranga.TryGetMangaConnector(chapterId.MangaConnectorName, out MangaConnector? mangaConnector))
return [];
return mangaConnector.GetChapterImageUrls(chapterId);
} }
} }

View File

@@ -2,11 +2,12 @@
using System.ComponentModel.DataAnnotations.Schema; using System.ComponentModel.DataAnnotations.Schema;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using API.MangaDownloadClients; using API.MangaDownloadClients;
using API.Schema.MangaContext;
using log4net; using log4net;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Newtonsoft.Json; using Newtonsoft.Json;
namespace API.Schema.MangaContext.MangaConnectors; namespace API.MangaConnectors;
[PrimaryKey("Name")] [PrimaryKey("Name")]
public abstract class MangaConnector(string name, string[] supportedLanguages, string[] baseUris, string iconUrl) public abstract class MangaConnector(string name, string[] supportedLanguages, string[] baseUris, string iconUrl)

View File

@@ -1,8 +1,9 @@
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using API.MangaDownloadClients; using API.MangaDownloadClients;
using API.Schema.MangaContext;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
namespace API.Schema.MangaContext.MangaConnectors; namespace API.MangaConnectors;
public class MangaDex : MangaConnector public class MangaDex : MangaConnector
{ {
@@ -276,9 +277,9 @@ public class MangaDex : MangaConnector
_ => kv.Key _ => kv.Key
}; };
return new Link(key, url); return new Link(key, url);
}).ToList()!; }).ToList()??[];
List<AltTitle> altTitles = (altTitlesJArray??[]) List<AltTitle> altTitles = altTitlesJArray?
.Select(t => .Select(t =>
{ {
JObject? j = t as JObject; JObject? j = t as JObject;
@@ -286,19 +287,19 @@ public class MangaDex : MangaConnector
if (p is null) if (p is null)
return null; return null;
return new AltTitle(p.Name, p.Value.ToString()); return new AltTitle(p.Name, p.Value.ToString());
}).Where(x => x is not null).ToList()!; }).Where(x => x is not null).Cast<AltTitle>().ToList()??[];
List<MangaTag> tags = (tagsJArray??[]) List<MangaTag> tags = tagsJArray?
.Where(t => t.Value<string>("type") == "tag") .Where(t => t.Value<string>("type") == "tag")
.Select(t => t["attributes"]?["name"]?.Value<string>("en")??t["attributes"]?["name"]?.First?.First?.Value<string>()) .Select(t => t["attributes"]?["name"]?.Value<string>("en")??t["attributes"]?["name"]?.First?.First?.Value<string>())
.Select(str => str is not null ? new MangaTag(str) : null) .Select(str => str is not null ? new MangaTag(str) : null)
.Where(x => x is not null).ToList()!; .Where(x => x is not null).Cast<MangaTag>().ToList()??[];
List<Author> authors = relationships List<Author> authors = relationships
.Where(r => r["type"]?.Value<string>() == "author") .Where(r => r["type"]?.Value<string>() == "author")
.Select(t => t["attributes"]?.Value<string>("name")) .Select(t => t["attributes"]?.Value<string>("name"))
.Select(str => str is not null ? new Author(str) : null) .Select(str => str is not null ? new Author(str) : null)
.Where(x => x is not null).ToList()!; .Where(x => x is not null).Cast<Author>().ToList();
MangaReleaseStatus releaseStatus = status switch MangaReleaseStatus releaseStatus = status switch
@@ -312,9 +313,11 @@ public class MangaDex : MangaConnector
string websiteUrl = $"https://mangadex.org/title/{id}"; string websiteUrl = $"https://mangadex.org/title/{id}";
string coverUrl = $"https://uploads.mangadex.org/covers/{id}/{coverFileName}"; string coverUrl = $"https://uploads.mangadex.org/covers/{id}/{coverFileName}";
Manga manga = new Manga(name, description, coverUrl, releaseStatus, authors, tags, links,altTitles, Manga manga = new (name, description, coverUrl, releaseStatus, authors, tags, links,altTitles,
null, 0f, year, originalLanguage); null, 0f, year, originalLanguage);
return (manga, new MangaConnectorId<Manga>(manga, this, id, websiteUrl)); MangaConnectorId<Manga> mcId = new (manga, this, id, websiteUrl);
manga.MangaConnectorIds.Add(mcId);
return (manga, mcId);
} }
private (Chapter chapter, MangaConnectorId<Chapter> id) ParseChapterFromJToken(MangaConnectorId<Manga> mcIdManga, JToken jToken) private (Chapter chapter, MangaConnectorId<Chapter> id) ParseChapterFromJToken(MangaConnectorId<Manga> mcIdManga, JToken jToken)
@@ -333,6 +336,8 @@ public class MangaDex : MangaConnector
string websiteUrl = $"https://mangadex.org/chapter/{id}"; string websiteUrl = $"https://mangadex.org/chapter/{id}";
Chapter chapter = new (mcIdManga.Obj, chapterStr, volumeNumber, title); Chapter chapter = new (mcIdManga.Obj, chapterStr, volumeNumber, title);
return (chapter, new MangaConnectorId<Chapter>(chapter, this, id, websiteUrl)); MangaConnectorId<Chapter> mcId = new(chapter, this, id, websiteUrl);
chapter.MangaConnectorIds.Add(mcId);
return (chapter, mcId);
} }
} }

View File

@@ -1,11 +1,12 @@
using System.Net; using System.Collections.Concurrent;
using System.Net;
using log4net; using log4net;
namespace API.MangaDownloadClients; namespace API.MangaDownloadClients;
public abstract class DownloadClient public abstract class DownloadClient
{ {
private static readonly Dictionary<RequestType, DateTime> LastExecutedRateLimit = new(); private static readonly ConcurrentDictionary<RequestType, DateTime> LastExecutedRateLimit = new();
protected ILog Log { get; init; } protected ILog Log { get; init; }
protected DownloadClient() protected DownloadClient()

View File

@@ -10,8 +10,6 @@ namespace API.MangaDownloadClients;
public class FlareSolverrDownloadClient : DownloadClient public class FlareSolverrDownloadClient : DownloadClient
{ {
internal override RequestResult MakeRequestInternal(string url, string? referrer = null, string? clickButton = null) internal override RequestResult MakeRequestInternal(string url, string? referrer = null, string? clickButton = null)
{ {
if (clickButton is not null) if (clickButton is not null)

View File

@@ -5,6 +5,7 @@ namespace API.MangaDownloadClients;
internal class HttpDownloadClient : DownloadClient internal class HttpDownloadClient : DownloadClient
{ {
private static readonly FlareSolverrDownloadClient FlareSolverrDownloadClient = new();
internal override RequestResult MakeRequestInternal(string url, string? referrer = null, string? clickButton = null) internal override RequestResult MakeRequestInternal(string url, string? referrer = null, string? clickButton = null)
{ {
if (clickButton is not null) if (clickButton is not null)
@@ -36,7 +37,7 @@ internal class HttpDownloadClient : DownloadClient
(s.Product?.Name ?? "").Contains("cloudflare", StringComparison.InvariantCultureIgnoreCase))) (s.Product?.Name ?? "").Contains("cloudflare", StringComparison.InvariantCultureIgnoreCase)))
{ {
Log.Debug("Retrying with FlareSolverr!"); Log.Debug("Retrying with FlareSolverr!");
return new FlareSolverrDownloadClient().MakeRequestInternal(url, referrer, clickButton); return FlareSolverrDownloadClient.MakeRequestInternal(url, referrer, clickButton);
} }
else else
{ {

View File

@@ -11,7 +11,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
namespace API.Migrations.Manga namespace API.Migrations.Manga
{ {
[DbContext(typeof(MangaContext))] [DbContext(typeof(MangaContext))]
[Migration("20250703192023_Initial")] [Migration("20250722203315_Initial")]
partial class Initial partial class Initial
{ {
/// <inheritdoc /> /// <inheritdoc />
@@ -24,6 +24,39 @@ namespace API.Migrations.Manga
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("API.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("MangaConnector");
b.HasDiscriminator<string>("Name").HasValue("MangaConnector");
b.UseTphMappingStrategy();
});
modelBuilder.Entity("API.Schema.MangaContext.Author", b => modelBuilder.Entity("API.Schema.MangaContext.Author", b =>
{ {
b.Property<string>("Key") b.Property<string>("Key")
@@ -177,8 +210,6 @@ namespace API.Migrations.Manga
b.HasKey("Key"); b.HasKey("Key");
b.HasIndex("MangaConnectorName");
b.HasIndex("ObjId"); b.HasIndex("ObjId");
b.ToTable("MangaConnectorToChapter"); b.ToTable("MangaConnectorToChapter");
@@ -213,46 +244,11 @@ namespace API.Migrations.Manga
b.HasKey("Key"); b.HasKey("Key");
b.HasIndex("MangaConnectorName");
b.HasIndex("ObjId"); b.HasIndex("ObjId");
b.ToTable("MangaConnectorToManga"); b.ToTable("MangaConnectorToManga");
}); });
modelBuilder.Entity("API.Schema.MangaContext.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.MangaContext.MangaTag", b => modelBuilder.Entity("API.Schema.MangaContext.MangaTag", b =>
{ {
b.Property<string>("Tag") b.Property<string>("Tag")
@@ -332,23 +328,23 @@ namespace API.Migrations.Manga
b.ToTable("MangaTagToManga"); b.ToTable("MangaTagToManga");
}); });
modelBuilder.Entity("API.Schema.MangaContext.MangaConnectors.ComickIo", b => modelBuilder.Entity("API.MangaConnectors.ComickIo", b =>
{ {
b.HasBaseType("API.Schema.MangaContext.MangaConnectors.MangaConnector"); b.HasBaseType("API.MangaConnectors.MangaConnector");
b.HasDiscriminator().HasValue("ComickIo"); b.HasDiscriminator().HasValue("ComickIo");
}); });
modelBuilder.Entity("API.Schema.MangaContext.MangaConnectors.Global", b => modelBuilder.Entity("API.MangaConnectors.Global", b =>
{ {
b.HasBaseType("API.Schema.MangaContext.MangaConnectors.MangaConnector"); b.HasBaseType("API.MangaConnectors.MangaConnector");
b.HasDiscriminator().HasValue("Global"); b.HasDiscriminator().HasValue("Global");
}); });
modelBuilder.Entity("API.Schema.MangaContext.MangaConnectors.MangaDex", b => modelBuilder.Entity("API.MangaConnectors.MangaDex", b =>
{ {
b.HasBaseType("API.Schema.MangaContext.MangaConnectors.MangaConnector"); b.HasBaseType("API.MangaConnectors.MangaConnector");
b.HasDiscriminator().HasValue("MangaDex"); b.HasDiscriminator().HasValue("MangaDex");
}); });
@@ -445,39 +441,23 @@ namespace API.Migrations.Manga
modelBuilder.Entity("API.Schema.MangaContext.MangaConnectorId<API.Schema.MangaContext.Chapter>", b => modelBuilder.Entity("API.Schema.MangaContext.MangaConnectorId<API.Schema.MangaContext.Chapter>", b =>
{ {
b.HasOne("API.Schema.MangaContext.MangaConnectors.MangaConnector", "MangaConnector")
.WithMany()
.HasForeignKey("MangaConnectorName")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("API.Schema.MangaContext.Chapter", "Obj") b.HasOne("API.Schema.MangaContext.Chapter", "Obj")
.WithMany("MangaConnectorIds") .WithMany("MangaConnectorIds")
.HasForeignKey("ObjId") .HasForeignKey("ObjId")
.OnDelete(DeleteBehavior.NoAction) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
b.Navigation("MangaConnector");
b.Navigation("Obj"); b.Navigation("Obj");
}); });
modelBuilder.Entity("API.Schema.MangaContext.MangaConnectorId<API.Schema.MangaContext.Manga>", b => modelBuilder.Entity("API.Schema.MangaContext.MangaConnectorId<API.Schema.MangaContext.Manga>", b =>
{ {
b.HasOne("API.Schema.MangaContext.MangaConnectors.MangaConnector", "MangaConnector")
.WithMany()
.HasForeignKey("MangaConnectorName")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("API.Schema.MangaContext.Manga", "Obj") b.HasOne("API.Schema.MangaContext.Manga", "Obj")
.WithMany("MangaConnectorIds") .WithMany("MangaConnectorIds")
.HasForeignKey("ObjId") .HasForeignKey("ObjId")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
b.Navigation("MangaConnector");
b.Navigation("Obj"); b.Navigation("Obj");
}); });

View File

@@ -36,7 +36,7 @@ namespace API.Migrations.Manga
}); });
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
name: "MangaConnectors", name: "MangaConnector",
columns: table => new columns: table => new
{ {
Name = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: false), Name = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: false),
@@ -47,7 +47,7 @@ namespace API.Migrations.Manga
}, },
constraints: table => constraints: table =>
{ {
table.PrimaryKey("PK_MangaConnectors", x => x.Name); table.PrimaryKey("PK_MangaConnector", x => x.Name);
}); });
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
@@ -201,12 +201,6 @@ namespace API.Migrations.Manga
constraints: table => constraints: table =>
{ {
table.PrimaryKey("PK_MangaConnectorToManga", x => x.Key); table.PrimaryKey("PK_MangaConnectorToManga", x => x.Key);
table.ForeignKey(
name: "FK_MangaConnectorToManga_MangaConnectors_MangaConnectorName",
column: x => x.MangaConnectorName,
principalTable: "MangaConnectors",
principalColumn: "Name",
onDelete: ReferentialAction.Cascade);
table.ForeignKey( table.ForeignKey(
name: "FK_MangaConnectorToManga_Mangas_ObjId", name: "FK_MangaConnectorToManga_Mangas_ObjId",
column: x => x.ObjId, column: x => x.ObjId,
@@ -282,12 +276,7 @@ namespace API.Migrations.Manga
name: "FK_MangaConnectorToChapter_Chapters_ObjId", name: "FK_MangaConnectorToChapter_Chapters_ObjId",
column: x => x.ObjId, column: x => x.ObjId,
principalTable: "Chapters", principalTable: "Chapters",
principalColumn: "Key"); principalColumn: "Key",
table.ForeignKey(
name: "FK_MangaConnectorToChapter_MangaConnectors_MangaConnectorName",
column: x => x.MangaConnectorName,
principalTable: "MangaConnectors",
principalColumn: "Name",
onDelete: ReferentialAction.Cascade); onDelete: ReferentialAction.Cascade);
}); });
@@ -311,21 +300,11 @@ namespace API.Migrations.Manga
table: "Link", table: "Link",
column: "MangaKey"); column: "MangaKey");
migrationBuilder.CreateIndex(
name: "IX_MangaConnectorToChapter_MangaConnectorName",
table: "MangaConnectorToChapter",
column: "MangaConnectorName");
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_MangaConnectorToChapter_ObjId", name: "IX_MangaConnectorToChapter_ObjId",
table: "MangaConnectorToChapter", table: "MangaConnectorToChapter",
column: "ObjId"); column: "ObjId");
migrationBuilder.CreateIndex(
name: "IX_MangaConnectorToManga_MangaConnectorName",
table: "MangaConnectorToManga",
column: "MangaConnectorName");
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_MangaConnectorToManga_ObjId", name: "IX_MangaConnectorToManga_ObjId",
table: "MangaConnectorToManga", table: "MangaConnectorToManga",
@@ -359,6 +338,9 @@ namespace API.Migrations.Manga
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "Link"); name: "Link");
migrationBuilder.DropTable(
name: "MangaConnector");
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "MangaConnectorToChapter"); name: "MangaConnectorToChapter");
@@ -377,9 +359,6 @@ namespace API.Migrations.Manga
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "Chapters"); name: "Chapters");
migrationBuilder.DropTable(
name: "MangaConnectors");
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "Tags"); name: "Tags");

View File

@@ -21,6 +21,39 @@ namespace API.Migrations.Manga
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("API.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("MangaConnector");
b.HasDiscriminator<string>("Name").HasValue("MangaConnector");
b.UseTphMappingStrategy();
});
modelBuilder.Entity("API.Schema.MangaContext.Author", b => modelBuilder.Entity("API.Schema.MangaContext.Author", b =>
{ {
b.Property<string>("Key") b.Property<string>("Key")
@@ -174,8 +207,6 @@ namespace API.Migrations.Manga
b.HasKey("Key"); b.HasKey("Key");
b.HasIndex("MangaConnectorName");
b.HasIndex("ObjId"); b.HasIndex("ObjId");
b.ToTable("MangaConnectorToChapter"); b.ToTable("MangaConnectorToChapter");
@@ -210,46 +241,11 @@ namespace API.Migrations.Manga
b.HasKey("Key"); b.HasKey("Key");
b.HasIndex("MangaConnectorName");
b.HasIndex("ObjId"); b.HasIndex("ObjId");
b.ToTable("MangaConnectorToManga"); b.ToTable("MangaConnectorToManga");
}); });
modelBuilder.Entity("API.Schema.MangaContext.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.MangaContext.MangaTag", b => modelBuilder.Entity("API.Schema.MangaContext.MangaTag", b =>
{ {
b.Property<string>("Tag") b.Property<string>("Tag")
@@ -329,23 +325,23 @@ namespace API.Migrations.Manga
b.ToTable("MangaTagToManga"); b.ToTable("MangaTagToManga");
}); });
modelBuilder.Entity("API.Schema.MangaContext.MangaConnectors.ComickIo", b => modelBuilder.Entity("API.MangaConnectors.ComickIo", b =>
{ {
b.HasBaseType("API.Schema.MangaContext.MangaConnectors.MangaConnector"); b.HasBaseType("API.MangaConnectors.MangaConnector");
b.HasDiscriminator().HasValue("ComickIo"); b.HasDiscriminator().HasValue("ComickIo");
}); });
modelBuilder.Entity("API.Schema.MangaContext.MangaConnectors.Global", b => modelBuilder.Entity("API.MangaConnectors.Global", b =>
{ {
b.HasBaseType("API.Schema.MangaContext.MangaConnectors.MangaConnector"); b.HasBaseType("API.MangaConnectors.MangaConnector");
b.HasDiscriminator().HasValue("Global"); b.HasDiscriminator().HasValue("Global");
}); });
modelBuilder.Entity("API.Schema.MangaContext.MangaConnectors.MangaDex", b => modelBuilder.Entity("API.MangaConnectors.MangaDex", b =>
{ {
b.HasBaseType("API.Schema.MangaContext.MangaConnectors.MangaConnector"); b.HasBaseType("API.MangaConnectors.MangaConnector");
b.HasDiscriminator().HasValue("MangaDex"); b.HasDiscriminator().HasValue("MangaDex");
}); });
@@ -442,39 +438,23 @@ namespace API.Migrations.Manga
modelBuilder.Entity("API.Schema.MangaContext.MangaConnectorId<API.Schema.MangaContext.Chapter>", b => modelBuilder.Entity("API.Schema.MangaContext.MangaConnectorId<API.Schema.MangaContext.Chapter>", b =>
{ {
b.HasOne("API.Schema.MangaContext.MangaConnectors.MangaConnector", "MangaConnector")
.WithMany()
.HasForeignKey("MangaConnectorName")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("API.Schema.MangaContext.Chapter", "Obj") b.HasOne("API.Schema.MangaContext.Chapter", "Obj")
.WithMany("MangaConnectorIds") .WithMany("MangaConnectorIds")
.HasForeignKey("ObjId") .HasForeignKey("ObjId")
.OnDelete(DeleteBehavior.NoAction) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
b.Navigation("MangaConnector");
b.Navigation("Obj"); b.Navigation("Obj");
}); });
modelBuilder.Entity("API.Schema.MangaContext.MangaConnectorId<API.Schema.MangaContext.Manga>", b => modelBuilder.Entity("API.Schema.MangaContext.MangaConnectorId<API.Schema.MangaContext.Manga>", b =>
{ {
b.HasOne("API.Schema.MangaContext.MangaConnectors.MangaConnector", "MangaConnector")
.WithMany()
.HasForeignKey("MangaConnectorName")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("API.Schema.MangaContext.Manga", "Obj") b.HasOne("API.Schema.MangaContext.Manga", "Obj")
.WithMany("MangaConnectorIds") .WithMany("MangaConnectorIds")
.HasForeignKey("ObjId") .HasForeignKey("ObjId")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
b.Navigation("MangaConnector");
b.Navigation("Obj"); b.Navigation("Obj");
}); });

View File

@@ -34,7 +34,7 @@ public class NamedSwaggerGenOptions : IConfigureNamedOptions<SwaggerGenOptions>
{ {
var info = new OpenApiInfo() var info = new OpenApiInfo()
{ {
Title = "Test API " + description.GroupName, Title = "Tranga " + description.GroupName,
Version = description.ApiVersion.ToString() Version = description.ApiVersion.ToString()
}; };
if (description.IsDeprecated) if (description.IsDeprecated)

View File

@@ -1,8 +1,8 @@
using System.Reflection; using System.Reflection;
using API; using API;
using API.MangaConnectors;
using API.Schema.LibraryContext; using API.Schema.LibraryContext;
using API.Schema.MangaContext; using API.Schema.MangaContext;
using API.Schema.MangaContext.MangaConnectors;
using API.Schema.NotificationsContext; using API.Schema.NotificationsContext;
using Asp.Versioning; using Asp.Versioning;
using Asp.Versioning.Builder; using Asp.Versioning.Builder;
@@ -109,14 +109,6 @@ using (IServiceScope scope = app.Services.CreateScope())
MangaContext context = scope.ServiceProvider.GetRequiredService<MangaContext>(); MangaContext context = scope.ServiceProvider.GetRequiredService<MangaContext>();
context.Database.Migrate(); context.Database.Migrate();
MangaConnector[] connectors =
[
new MangaDex(),
new ComickIo(),
new Global(scope.ServiceProvider.GetService<MangaContext>()!)
];
MangaConnector[] newConnectors = connectors.Where(c => !context.MangaConnectors.Contains(c)).ToArray();
context.MangaConnectors.AddRange(newConnectors);
if (!context.FileLibraries.Any()) if (!context.FileLibraries.Any())
context.FileLibraries.Add(new FileLibrary(Tranga.Settings.DownloadLocation, "Default FileLibrary")); context.FileLibraries.Add(new FileLibrary(Tranga.Settings.DownloadLocation, "Default FileLibrary"));
@@ -143,9 +135,9 @@ using (IServiceScope scope = app.Services.CreateScope())
context.Sync(); context.Sync();
} }
Tranga.SetServiceProvider(app.Services);
Tranga.StartLogger(); Tranga.StartLogger();
Tranga.AddDefaultWorkers();
Tranga.PeriodicWorkerStarterThread.Start(app.Services);
app.UseCors("AllowAll"); app.UseCors("AllowAll");

View File

@@ -4,7 +4,6 @@ using System.Text;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Xml.Linq; using System.Xml.Linq;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Newtonsoft.Json; using Newtonsoft.Json;
namespace API.Schema.MangaContext; namespace API.Schema.MangaContext;
@@ -13,30 +12,11 @@ namespace API.Schema.MangaContext;
public class Chapter : Identifiable, IComparable<Chapter> public class Chapter : Identifiable, IComparable<Chapter>
{ {
[StringLength(64)] [Required] public string ParentMangaId { get; init; } = null!; [StringLength(64)] [Required] public string ParentMangaId { get; init; } = null!;
private Manga? _parentManga; [JsonIgnore] public Manga ParentManga = null!;
[JsonIgnore] [NotMapped] public Dictionary<string, string> IdsOnMangaConnectors =>
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); MangaConnectorIds.ToDictionary(id => id.MangaConnectorName, id => id.IdOnConnectorSite);
[JsonIgnore] public ICollection<MangaConnectorId<Chapter>> MangaConnectorIds = null!;
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; } public int? VolumeNumber { get; private set; }
[StringLength(10)] [Required] public string ChapterNumber { get; private set; } [StringLength(10)] [Required] public string ChapterNumber { get; private set; }
@@ -48,8 +28,6 @@ public class Chapter : Identifiable, IComparable<Chapter>
[Required] public bool Downloaded { get; internal set; } [Required] public bool Downloaded { get; internal set; }
[NotMapped] public string FullArchiveFilePath => Path.Join(ParentManga.FullDirectoryPath, FileName); [NotMapped] public string FullArchiveFilePath => Path.Join(ParentManga.FullDirectoryPath, FileName);
private readonly ILazyLoader _lazyLoader = null!;
public Chapter(Manga parentManga, string chapterNumber, public Chapter(Manga parentManga, string chapterNumber,
int? volumeNumber, string? title = null) int? volumeNumber, string? title = null)
: base(TokenGen.CreateToken(typeof(Chapter), parentManga.Key, chapterNumber)) : base(TokenGen.CreateToken(typeof(Chapter), parentManga.Key, chapterNumber))
@@ -61,15 +39,15 @@ public class Chapter : Identifiable, IComparable<Chapter>
this.Title = title; this.Title = title;
this.FileName = GetArchiveFilePath(); this.FileName = GetArchiveFilePath();
this.Downloaded = false; this.Downloaded = false;
this.MangaConnectorIds = [];
} }
/// <summary> /// <summary>
/// EF ONLY!!! /// EF ONLY!!!
/// </summary> /// </summary>
internal Chapter(ILazyLoader lazyLoader, string key, int? volumeNumber, string chapterNumber, string? title, string fileName, bool downloaded) internal Chapter(string key, int? volumeNumber, string chapterNumber, string? title, string fileName, bool downloaded)
: base(key) : base(key)
{ {
this._lazyLoader = lazyLoader;
this.VolumeNumber = volumeNumber; this.VolumeNumber = volumeNumber;
this.ChapterNumber = chapterNumber; this.ChapterNumber = chapterNumber;
this.Title = title; this.Title = title;

View File

@@ -18,17 +18,7 @@ public class Manga : Identifiable
[JsonIgnore] [Url] [StringLength(512)] public string CoverUrl { get; internal set; } [JsonIgnore] [Url] [StringLength(512)] public string CoverUrl { get; internal set; }
[Required] public MangaReleaseStatus ReleaseStatus { get; internal set; } [Required] public MangaReleaseStatus ReleaseStatus { get; internal set; }
[StringLength(64)] public string? LibraryId { get; private set; } [StringLength(64)] public string? LibraryId { get; private set; }
private FileLibrary? _library; [JsonIgnore] public FileLibrary? Library = null!;
[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<Author> Authors { get; internal set; } = null!;
public ICollection<MangaTag> MangaTags { get; internal set; } = null!; public ICollection<MangaTag> MangaTags { get; internal set; } = null!;
@@ -45,25 +35,12 @@ public class Manga : Identifiable
public string? FullDirectoryPath => Library is not null ? Path.Join(Library.BasePath, DirectoryName) : null; public string? FullDirectoryPath => Library is not null ? Path.Join(Library.BasePath, DirectoryName) : null;
[NotMapped] public ICollection<string> ChapterIds => Chapters.Select(c => c.Key).ToList(); [NotMapped] public ICollection<string> ChapterIds => Chapters.Select(c => c.Key).ToList();
private ICollection<Chapter>? _chapters; [JsonIgnore] public ICollection<Chapter> Chapters = null!;
[JsonIgnore]
public ICollection<Chapter> Chapters
{
get => _lazyLoader.Load(this, ref _chapters) ?? throw new InvalidOperationException();
init => _chapters = value;
}
[NotMapped] public Dictionary<string, string> IdsOnMangaConnectors => [NotMapped] public Dictionary<string, string> IdsOnMangaConnectors =>
MangaConnectorIds.ToDictionary(id => id.MangaConnectorName, id => id.IdOnConnectorSite); MangaConnectorIds.ToDictionary(id => id.MangaConnectorName, id => id.IdOnConnectorSite);
private ICollection<MangaConnectorId<Manga>>? _mangaConnectorIds; [NotMapped] public ICollection<string> MangaConnectorIdsIds => MangaConnectorIds.Select(id => id.Key).ToList();
[JsonIgnore] [JsonIgnore] public ICollection<MangaConnectorId<Manga>> MangaConnectorIds = null!;
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, public Manga(string name, string description, string coverUrl, MangaReleaseStatus releaseStatus,
ICollection<Author> authors, ICollection<MangaTag> mangaTags, ICollection<Link> links, ICollection<AltTitle> altTitles, ICollection<Author> authors, ICollection<MangaTag> mangaTags, ICollection<Link> links, ICollection<AltTitle> altTitles,
@@ -84,17 +61,17 @@ public class Manga : Identifiable
this.Year = year; this.Year = year;
this.OriginalLanguage = originalLanguage; this.OriginalLanguage = originalLanguage;
this.Chapters = []; this.Chapters = [];
this.MangaConnectorIds = [];
} }
/// <summary> /// <summary>
/// EF ONLY!!! /// EF ONLY!!!
/// </summary> /// </summary>
public Manga(ILazyLoader lazyLoader, string key, string name, string description, string coverUrl, public Manga(string key, string name, string description, string coverUrl,
MangaReleaseStatus releaseStatus, MangaReleaseStatus releaseStatus,
string directoryName, float ignoreChaptersBefore, string? libraryId, uint? year, string? originalLanguage) string directoryName, float ignoreChaptersBefore, string? libraryId, uint? year, string? originalLanguage)
: base(key) : base(key)
{ {
this._lazyLoader = lazyLoader;
this.Name = name; this.Name = name;
this.Description = description; this.Description = description;
this.CoverUrl = coverUrl; this.CoverUrl = coverUrl;

View File

@@ -1,7 +1,6 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using API.Schema.MangaContext.MangaConnectors; using API.MangaConnectors;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Newtonsoft.Json; using Newtonsoft.Json;
namespace API.Schema.MangaContext; namespace API.Schema.MangaContext;
@@ -9,44 +8,21 @@ namespace API.Schema.MangaContext;
[PrimaryKey("Key")] [PrimaryKey("Key")]
public class MangaConnectorId<T> : Identifiable where T : Identifiable public class MangaConnectorId<T> : Identifiable where T : Identifiable
{ {
[StringLength(64)] [Required] public string ObjId { get; private set; } = null!; [StringLength(64)] [Required] public string ObjId { get; internal set; }
[JsonIgnore] private T? _obj; [JsonIgnore] public T Obj = null!;
[JsonIgnore] [StringLength(32)] [Required] public string MangaConnectorName { get; private set; }
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; } [StringLength(256)] [Required] public string IdOnConnectorSite { get; init; }
[Url] [StringLength(512)] public string? WebsiteUrl { get; internal init; } [Url] [StringLength(512)] public string? WebsiteUrl { get; internal init; }
public bool UseForDownload { get; internal set; } public bool UseForDownload { get; internal set; }
private readonly ILazyLoader _lazyLoader = null!;
public MangaConnectorId(T obj, MangaConnector mangaConnector, string idOnConnectorSite, string? websiteUrl, bool useForDownload = false) public MangaConnectorId(T obj, MangaConnector mangaConnector, string idOnConnectorSite, string? websiteUrl, bool useForDownload = false)
: base(TokenGen.CreateToken(typeof(MangaConnectorId<T>), mangaConnector.Name, idOnConnectorSite)) : base(TokenGen.CreateToken(typeof(MangaConnectorId<T>), mangaConnector.Name, idOnConnectorSite))
{ {
this.Obj = obj; this.Obj = obj;
this.MangaConnector = mangaConnector; this.ObjId = obj.Key;
this.MangaConnectorName = mangaConnector.Name;
this.IdOnConnectorSite = idOnConnectorSite; this.IdOnConnectorSite = idOnConnectorSite;
this.WebsiteUrl = websiteUrl; this.WebsiteUrl = websiteUrl;
this.UseForDownload = useForDownload; this.UseForDownload = useForDownload;
@@ -55,10 +31,9 @@ public class MangaConnectorId<T> : Identifiable where T : Identifiable
/// <summary> /// <summary>
/// EF CORE ONLY!!! /// EF CORE ONLY!!!
/// </summary> /// </summary>
public MangaConnectorId(ILazyLoader lazyLoader, string key, string objId, string mangaConnectorName, string idOnConnectorSite, bool useForDownload, string? websiteUrl) public MangaConnectorId(string key, string objId, string mangaConnectorName, string idOnConnectorSite, bool useForDownload, string? websiteUrl)
: base(key) : base(key)
{ {
this._lazyLoader = lazyLoader;
this.ObjId = objId; this.ObjId = objId;
this.MangaConnectorName = mangaConnectorName; this.MangaConnectorName = mangaConnectorName;
this.IdOnConnectorSite = idOnConnectorSite; this.IdOnConnectorSite = idOnConnectorSite;
@@ -66,5 +41,5 @@ public class MangaConnectorId<T> : Identifiable where T : Identifiable
this.UseForDownload = useForDownload; this.UseForDownload = useForDownload;
} }
public override string ToString() => $"{base.ToString()} {_obj}"; public override string ToString() => $"{base.ToString()} {Obj}";
} }

View File

@@ -1,12 +1,12 @@
using API.Schema.MangaContext.MangaConnectors; using API.MangaConnectors;
using API.Schema.MangaContext.MetadataFetchers; using API.Schema.MangaContext.MetadataFetchers;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Query;
namespace API.Schema.MangaContext; namespace API.Schema.MangaContext;
public class MangaContext(DbContextOptions<MangaContext> options) : TrangaBaseContext<MangaContext>(options) public class MangaContext(DbContextOptions<MangaContext> options) : TrangaBaseContext<MangaContext>(options)
{ {
public DbSet<MangaConnector> MangaConnectors { get; set; }
public DbSet<Manga> Mangas { get; set; } public DbSet<Manga> Mangas { get; set; }
public DbSet<FileLibrary> FileLibraries { get; set; } public DbSet<FileLibrary> FileLibraries { get; set; }
public DbSet<Chapter> Chapters { get; set; } public DbSet<Chapter> Chapters { get; set; }
@@ -31,26 +31,12 @@ public class MangaContext(DbContextOptions<MangaContext> options) : TrangaBaseCo
.WithOne(c => c.ParentManga) .WithOne(c => c.ParentManga)
.HasForeignKey(c => c.ParentMangaId) .HasForeignKey(c => c.ParentMangaId)
.OnDelete(DeleteBehavior.Cascade); .OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<Manga>()
.Navigation(m => m.Chapters)
.EnableLazyLoading();
modelBuilder.Entity<Chapter>()
.Navigation(c => c.ParentManga)
.EnableLazyLoading();
//Chapter has MangaConnectorIds //Chapter has MangaConnectorIds
modelBuilder.Entity<Chapter>() modelBuilder.Entity<Chapter>()
.HasMany<MangaConnectorId<Chapter>>(c => c.MangaConnectorIds) .HasMany<MangaConnectorId<Chapter>>(c => c.MangaConnectorIds)
.WithOne(id => id.Obj) .WithOne(id => id.Obj)
.HasForeignKey(id => id.ObjId) .HasForeignKey(id => id.ObjId)
.OnDelete(DeleteBehavior.NoAction);
modelBuilder.Entity<MangaConnectorId<Chapter>>()
.HasOne<MangaConnector>(id => id.MangaConnector)
.WithMany()
.HasForeignKey(id => id.MangaConnectorName)
.OnDelete(DeleteBehavior.Cascade); .OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<MangaConnectorId<Chapter>>()
.Navigation(entry => entry.MangaConnector)
.EnableLazyLoading();
//Manga owns MangaAltTitles //Manga owns MangaAltTitles
modelBuilder.Entity<Manga>() modelBuilder.Entity<Manga>()
.OwnsMany<AltTitle>(m => m.AltTitles) .OwnsMany<AltTitle>(m => m.AltTitles)
@@ -95,17 +81,6 @@ public class MangaContext(DbContextOptions<MangaContext> options) : TrangaBaseCo
.WithOne(id => id.Obj) .WithOne(id => id.Obj)
.HasForeignKey(id => id.ObjId) .HasForeignKey(id => id.ObjId)
.OnDelete(DeleteBehavior.Cascade); .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();
//FileLibrary has many Mangas //FileLibrary has many Mangas
@@ -114,9 +89,6 @@ public class MangaContext(DbContextOptions<MangaContext> options) : TrangaBaseCo
.WithOne(m => m.Library) .WithOne(m => m.Library)
.HasForeignKey(m => m.LibraryId) .HasForeignKey(m => m.LibraryId)
.OnDelete(DeleteBehavior.SetNull); .OnDelete(DeleteBehavior.SetNull);
modelBuilder.Entity<Manga>()
.Navigation(m => m.Library)
.EnableLazyLoading();
modelBuilder.Entity<MetadataFetcher>() modelBuilder.Entity<MetadataFetcher>()
.HasDiscriminator<string>(nameof(MetadataEntry)) .HasDiscriminator<string>(nameof(MetadataEntry))
@@ -131,4 +103,23 @@ public class MangaContext(DbContextOptions<MangaContext> options) : TrangaBaseCo
.WithMany() .WithMany()
.OnDelete(DeleteBehavior.Cascade); .OnDelete(DeleteBehavior.Cascade);
} }
public Manga? FindMangaLike(Manga other)
{
if (MangaIncludeAll().FirstOrDefault(m => m.Key == other.Key) is { } f)
return f;
return MangaIncludeAll()
.FirstOrDefault(m => m.Links.Any(l => l.Key == other.Key) ||
m.AltTitles.Any(t => other.AltTitles.Select(ot => ot.Title)
.Any(s => s.Equals(t.Title))));
}
public IIncludableQueryable<Manga, ICollection<MangaConnectorId<Manga>>> MangaIncludeAll() => Mangas.Include(m => m.Library)
.Include(m => m.Authors)
.Include(m => m.MangaTags)
.Include(m => m.Links)
.Include(m => m.AltTitles)
.Include(m => m.Chapters)
.Include(m => m.MangaConnectorIds);
} }

View File

@@ -1,6 +1,7 @@
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using JikanDotNet; using JikanDotNet;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.ChangeTracking;
namespace API.Schema.MangaContext.MetadataFetchers; namespace API.Schema.MangaContext.MetadataFetchers;
@@ -47,6 +48,11 @@ public class MyAnimeList : MetadataFetcher
public override void UpdateMetadata(MetadataEntry metadataEntry, MangaContext dbContext) public override void UpdateMetadata(MetadataEntry metadataEntry, MangaContext dbContext)
{ {
Manga dbManga = dbContext.Mangas.Find(metadataEntry.MangaId)!; Manga dbManga = dbContext.Mangas.Find(metadataEntry.MangaId)!;
foreach (CollectionEntry collectionEntry in dbContext.Entry(dbManga).Collections)
collectionEntry.Load();
dbContext.Entry(dbManga).Navigation(nameof(Manga.Library)).Load();
MangaFull resultData; MangaFull resultData;
try try
{ {

View File

@@ -44,41 +44,30 @@ public class NotificationConnector(string name, string url, Dictionary<string, s
public void SendNotification(string title, string notificationText) public void SendNotification(string title, string notificationText)
{ {
Log.Info($"Sending notification: {title} - {notificationText}"); Log.Info($"Sending notification: {title} - {notificationText}");
CustomWebhookFormatProvider formatProvider = new (title, notificationText); string formattedUrl = FormatStr(Url, title, notificationText);
string formattedUrl = string.Format(formatProvider, Url); string formattedBody = FormatStr(Body, title, notificationText);
string formattedBody = string.Format(formatProvider, Body, title, notificationText);
Dictionary<string, string> formattedHeaders = Headers.ToDictionary(h => h.Key, Dictionary<string, string> formattedHeaders = Headers.ToDictionary(h => h.Key,
h => string.Format(formatProvider, h.Value, title, notificationText)); h => FormatStr(h.Value, title, notificationText));
HttpRequestMessage request = new(System.Net.Http.HttpMethod.Parse(HttpMethod), formattedUrl); HttpRequestMessage request = new(System.Net.Http.HttpMethod.Parse(HttpMethod), formattedUrl);
foreach (var (key, value) in formattedHeaders) foreach (var (key, value) in formattedHeaders)
request.Headers.Add(key, value); request.Headers.Add(key, value);
request.Content = new StringContent(formattedBody); request.Content = new StringContent(formattedBody);
request.Content.Headers.ContentType = new ("application/json");
Log.Debug($"Request: {request}"); Log.Debug($"Request: {request}");
HttpResponseMessage response = Client.Send(request); HttpResponseMessage response = Client.Send(request);
Log.Debug($"Response status code: {response.StatusCode}"); Log.Debug($"Response status code: {response.StatusCode} {response.Content.ReadAsStringAsync().Result}");
} }
private class CustomWebhookFormatProvider(string title, string text) : IFormatProvider private string FormatStr(string str, string title, string text)
{ {
public object? GetFormat(Type? formatType) StringBuilder sb = new (str);
{
return this;
}
public string Format(string fmt, object arg, IFormatProvider provider)
{
if(arg.GetType() != typeof(string))
return arg.ToString() ?? string.Empty;
StringBuilder sb = new StringBuilder(fmt);
sb.Replace("%title", title); sb.Replace("%title", title);
sb.Replace("%text", text); sb.Replace("%text", text);
return sb.ToString(); return sb.ToString();
} }
}
public override string ToString() => $"{GetType().Name} {Name}"; public override string ToString() => $"{GetType().Name} {Name}";
} }

View File

@@ -1,4 +1,6 @@
using System.Diagnostics.CodeAnalysis; using System.Collections.Concurrent;
using System.Diagnostics.CodeAnalysis;
using API.MangaConnectors;
using API.Schema.LibraryContext; using API.Schema.LibraryContext;
using API.Schema.MangaContext; using API.Schema.MangaContext;
using API.Schema.MangaContext.MetadataFetchers; using API.Schema.MangaContext.MetadataFetchers;
@@ -7,6 +9,7 @@ using API.Workers;
using API.Workers.MaintenanceWorkers; using API.Workers.MaintenanceWorkers;
using log4net; using log4net;
using log4net.Config; using log4net.Config;
using Microsoft.EntityFrameworkCore;
namespace API; namespace API;
@@ -22,9 +25,11 @@ public static class Tranga
" |___| |__| |___._||__|__||___ ||___._|\n" + " |___| |__| |___._||__|__||___ ||___._|\n" +
" |_____| \n\n"; " |_____| \n\n";
public static Thread PeriodicWorkerStarterThread { get; } = new (WorkerStarter); private static IServiceProvider? ServiceProvider;
private static readonly ILog Log = LogManager.GetLogger(typeof(Tranga)); private static readonly ILog Log = LogManager.GetLogger(typeof(Tranga));
internal static readonly MetadataFetcher[] MetadataFetchers = [new MyAnimeList()]; internal static readonly MetadataFetcher[] MetadataFetchers = [new MyAnimeList()];
internal static readonly MangaConnector[] MangaConnectors = [new Global(), new MangaDex(), new ComickIo()];
internal static TrangaSettings Settings = TrangaSettings.Load(); internal static TrangaSettings Settings = TrangaSettings.Load();
internal static readonly UpdateMetadataWorker UpdateMetadataWorker = new (); internal static readonly UpdateMetadataWorker UpdateMetadataWorker = new ();
@@ -34,13 +39,17 @@ public static class Tranga
internal static readonly CleanupMangaCoversWorker CleanupMangaCoversWorker = new(); internal static readonly CleanupMangaCoversWorker CleanupMangaCoversWorker = new();
internal static readonly StartNewChapterDownloadsWorker StartNewChapterDownloadsWorker = new(); internal static readonly StartNewChapterDownloadsWorker StartNewChapterDownloadsWorker = new();
internal static readonly RemoveOldNotificationsWorker RemoveOldNotificationsWorker = new(); internal static readonly RemoveOldNotificationsWorker RemoveOldNotificationsWorker = new();
internal static readonly UpdateCoversWorker UpdateCoversWorker = new();
internal static void StartLogger() internal static void StartLogger()
{ {
BasicConfigurator.Configure(); BasicConfigurator.Configure();
Log.Info("Logger Configured."); Log.Info("Logger Configured.");
Log.Info(TRANGA); Log.Info(TRANGA);
}
internal static void AddDefaultWorkers()
{
AddWorker(UpdateMetadataWorker); AddWorker(UpdateMetadataWorker);
AddWorker(SendNotificationsWorker); AddWorker(SendNotificationsWorker);
AddWorker(UpdateChaptersDownloadedWorker); AddWorker(UpdateChaptersDownloadedWorker);
@@ -48,119 +57,130 @@ public static class Tranga
AddWorker(CleanupMangaCoversWorker); AddWorker(CleanupMangaCoversWorker);
AddWorker(StartNewChapterDownloadsWorker); AddWorker(StartNewChapterDownloadsWorker);
AddWorker(RemoveOldNotificationsWorker); AddWorker(RemoveOldNotificationsWorker);
AddWorker(UpdateCoversWorker);
} }
internal static HashSet<BaseWorker> AllWorkers { get; private set; } = new (); internal static void SetServiceProvider(IServiceProvider serviceProvider)
public static void AddWorker(BaseWorker worker) => AllWorkers.Add(worker); {
ServiceProvider = serviceProvider;
}
internal static bool TryGetMangaConnector(string name, [NotNullWhen(true)]out MangaConnector? mangaConnector)
{
mangaConnector =
MangaConnectors.FirstOrDefault(c => c.Name.Equals(name, StringComparison.InvariantCultureIgnoreCase));
return mangaConnector != null;
}
internal static readonly ConcurrentDictionary<IPeriodic, Task> PeriodicWorkers = new ();
public static void AddWorker(BaseWorker worker)
{
Log.Debug($"Adding Worker {worker}");
StartWorker(worker);
if(worker is IPeriodic periodic)
AddPeriodicWorker(worker, periodic);
}
private static void AddPeriodicWorker(BaseWorker worker, IPeriodic periodic)
{
Log.Debug($"Adding Periodic {worker}");
Task periodicTask = PeriodicTask(worker, periodic);
PeriodicWorkers.TryAdd((worker as IPeriodic)!, periodicTask);
periodicTask.Start();
}
private static Task PeriodicTask(BaseWorker worker, IPeriodic periodic) => new (() =>
{
Log.Debug($"Waiting {periodic.Interval} for next run of {worker}");
Thread.Sleep(periodic.Interval);
StartWorker(worker, RefreshTask(worker, periodic));
});
private static Action RefreshTask(BaseWorker worker, IPeriodic periodic) => () =>
{
if (worker.State < WorkerExecutionState.Created) //Failed
return;
Log.Debug($"Refreshing {worker}");
Task periodicTask = PeriodicTask(worker, periodic);
PeriodicWorkers.AddOrUpdate((worker as IPeriodic)!, periodicTask, (_, _) => periodicTask);
periodicTask.Start();
};
public static void AddWorkers(IEnumerable<BaseWorker> workers) public static void AddWorkers(IEnumerable<BaseWorker> workers)
{ {
foreach (BaseWorker baseWorker in workers) foreach (BaseWorker baseWorker in workers)
{
AddWorker(baseWorker); AddWorker(baseWorker);
} }
}
public static void RemoveWorker(BaseWorker removeWorker) private static readonly ConcurrentDictionary<BaseWorker, Task<BaseWorker[]>> RunningWorkers = new();
{
IEnumerable<BaseWorker> baseWorkers = AllWorkers.Where(w => w.DependenciesAndSelf.Any(worker => worker == removeWorker));
foreach (BaseWorker worker in baseWorkers)
{
StopWorker(worker);
AllWorkers.Remove(worker);
}
}
private static readonly Dictionary<BaseWorker, Task<BaseWorker[]>> RunningWorkers = new();
public static BaseWorker[] GetRunningWorkers() => RunningWorkers.Keys.ToArray(); public static BaseWorker[] GetRunningWorkers() => RunningWorkers.Keys.ToArray();
private static readonly HashSet<BaseWorker> StartWorkers = new();
private static void WorkerStarter(object? serviceProviderObj) internal static void StartWorker(BaseWorker worker, Action? callback = null)
{ {
Log.Info("WorkerStarter Thread running."); Log.Debug($"Starting {worker}");
if (serviceProviderObj is null) if (ServiceProvider is null)
{ {
Log.Error("serviceProviderObj is null"); Log.Fatal("ServiceProvider is null");
return; return;
} }
IServiceProvider serviceProvider = (IServiceProvider)serviceProviderObj; Action afterWorkCallback = AfterWork(worker, callback);
while (true)
{
CheckRunningWorkers();
foreach (BaseWorker baseWorker in AllWorkers.DueWorkers())
StartWorkers.Add(baseWorker);
foreach (BaseWorker worker in StartWorkers.ToArray())
{
if(RunningWorkers.ContainsKey(worker))
continue;
if (worker is BaseWorkerWithContext<MangaContext> mangaContextWorker) if (worker is BaseWorkerWithContext<MangaContext> mangaContextWorker)
{ {
mangaContextWorker.SetScope(serviceProvider.CreateScope()); mangaContextWorker.SetScope(ServiceProvider.CreateScope());
RunningWorkers.Add(mangaContextWorker, mangaContextWorker.DoWork()); RunningWorkers.TryAdd(mangaContextWorker, mangaContextWorker.DoWork(afterWorkCallback));
}else if (worker is BaseWorkerWithContext<NotificationsContext> notificationContextWorker) }else if (worker is BaseWorkerWithContext<NotificationsContext> notificationContextWorker)
{ {
notificationContextWorker.SetScope(serviceProvider.CreateScope()); notificationContextWorker.SetScope(ServiceProvider.CreateScope());
RunningWorkers.Add(notificationContextWorker, notificationContextWorker.DoWork()); RunningWorkers.TryAdd(notificationContextWorker, notificationContextWorker.DoWork(afterWorkCallback));
}else if (worker is BaseWorkerWithContext<LibraryContext> libraryContextWorker) }else if (worker is BaseWorkerWithContext<LibraryContext> libraryContextWorker)
{ {
libraryContextWorker.SetScope(serviceProvider.CreateScope()); libraryContextWorker.SetScope(ServiceProvider.CreateScope());
RunningWorkers.Add(libraryContextWorker, libraryContextWorker.DoWork()); RunningWorkers.TryAdd(libraryContextWorker, libraryContextWorker.DoWork(afterWorkCallback));
}else }else
RunningWorkers.Add(worker, worker.DoWork()); RunningWorkers.TryAdd(worker, worker.DoWork(afterWorkCallback));
StartWorkers.Remove(worker);
}
Thread.Sleep(Settings.WorkCycleTimeoutMs);
}
} }
private static void CheckRunningWorkers() private static Action AfterWork(BaseWorker worker, Action? callback = null) => () =>
{ {
KeyValuePair<BaseWorker, Task<BaseWorker[]>>[] done = RunningWorkers.Where(kv => kv.Value.IsCompleted).ToArray(); Log.Debug($"AfterWork {worker}");
if (done.Length < 1) RunningWorkers.Remove(worker, out _);
return; callback?.Invoke();
Log.Info($"Done: {done.Length}"); };
Log.Debug(string.Join("\n", done.Select(d => d.Key.ToString())));
foreach ((BaseWorker worker, Task<BaseWorker[]> task) in done)
{
RunningWorkers.Remove(worker);
foreach (BaseWorker newWorker in task.Result)
AllWorkers.Add(newWorker);
task.Dispose();
}
}
private static IEnumerable<BaseWorker> DueWorkers(this IEnumerable<BaseWorker> workers)
{
return workers.Where(w =>
{
if (w.State is >= WorkerExecutionState.Running and < WorkerExecutionState.Completed)
return false;
if (w is IPeriodic periodicWorker)
return periodicWorker.IsDue;
return true;
});
}
internal static void MarkWorkerForStart(BaseWorker worker) => StartWorkers.Add(worker);
internal static void StopWorker(BaseWorker worker) internal static void StopWorker(BaseWorker worker)
{ {
StartWorkers.Remove(worker); Log.Debug($"Stopping {worker}");
if(worker is IPeriodic periodicWorker)
PeriodicWorkers.Remove(periodicWorker, out _);
worker.Cancel(); worker.Cancel();
RunningWorkers.Remove(worker); RunningWorkers.Remove(worker, out _);
} }
internal static bool AddMangaToContext((Manga, MangaConnectorId<Manga>) addManga, MangaContext context, [NotNullWhen(true)]out Manga? manga) => AddMangaToContext(addManga.Item1, addManga.Item2, context, out manga); internal static bool AddMangaToContext((Manga, MangaConnectorId<Manga>) addManga, MangaContext context, [NotNullWhen(true)]out Manga? manga) => AddMangaToContext(addManga.Item1, addManga.Item2, context, out manga);
internal static bool AddMangaToContext(Manga addManga, MangaConnectorId<Manga> addMcId, MangaContext context, [NotNullWhen(true)]out Manga? manga) internal static bool AddMangaToContext(Manga addManga, MangaConnectorId<Manga> addMcId, MangaContext context, [NotNullWhen(true)]out Manga? manga)
{ {
manga = context.Mangas.Find(addManga.Key) ?? addManga; context.ChangeTracker.Clear();
MangaConnectorId<Manga> mcId = context.MangaConnectorToManga.Find(addMcId.Key) ?? addMcId; manga = context.FindMangaLike(addManga);
if (manga is not null)
{
foreach (MangaConnectorId<Manga> mcId in addManga.MangaConnectorIds)
{
mcId.Obj = manga; mcId.Obj = manga;
mcId.ObjId = manga.Key;
}
manga.MangaTags = manga.MangaTags.UnionBy(addManga.MangaTags, tag => tag.Tag).ToList();
manga.Authors = manga.Authors.UnionBy(addManga.Authors, author => author.Key).ToList();
manga.Links = manga.Links.UnionBy(addManga.Links, link => link.Key).ToList();
manga.AltTitles = manga.AltTitles.UnionBy(addManga.AltTitles, altTitle => altTitle.Key).ToList();
manga.Chapters = manga.Chapters.UnionBy(addManga.Chapters, chapter => chapter.Key).ToList();
manga.MangaConnectorIds = manga.MangaConnectorIds.UnionBy(addManga.MangaConnectorIds, id => id.MangaConnectorName).ToList();
}
else
{
manga = addManga;
IEnumerable<MangaTag> mergedTags = manga.MangaTags.Select(mt => IEnumerable<MangaTag> mergedTags = manga.MangaTags.Select(mt =>
{ {
MangaTag? inDb = context.Tags.Find(mt.Tag); MangaTag? inDb = context.Tags.Find(mt.Tag);
@@ -175,11 +195,15 @@ public static class Tranga
}); });
manga.Authors = mergedAuthors.ToList(); manga.Authors = mergedAuthors.ToList();
if(context.MangaConnectorToManga.Find(addMcId.Key) is null) context.Mangas.Add(manga);
context.MangaConnectorToManga.Add(mcId); }
if (context.Sync() is { success: false }) if (context.Sync() is { success: false })
return false; return false;
DownloadCoverFromMangaconnectorWorker downloadCoverWorker = new (addMcId);
AddWorker(downloadCoverWorker);
return true; return true;
} }
@@ -188,12 +212,19 @@ public static class Tranga
internal static bool AddChapterToContext(Chapter addChapter, MangaConnectorId<Chapter> addChId, MangaContext context, [NotNullWhen(true)] out Chapter? chapter) internal static bool AddChapterToContext(Chapter addChapter, MangaConnectorId<Chapter> addChId, MangaContext context, [NotNullWhen(true)] out Chapter? chapter)
{ {
chapter = context.Chapters.Find(addChapter.Key) ?? addChapter; chapter = context.Chapters.Where(ch => ch.Key == addChapter.Key)
MangaConnectorId<Chapter> chId = context.MangaConnectorToChapter.Find(addChId.Key) ?? addChId; .Include(ch => ch.ParentManga)
chId.Obj = chapter; .Include(ch => ch.MangaConnectorIds)
.FirstOrDefault();
if(context.MangaConnectorToChapter.Find(chId.Key) is null) if (chapter is not null)
context.MangaConnectorToChapter.Add(chId); {
chapter.MangaConnectorIds = chapter.MangaConnectorIds.UnionBy(addChapter.MangaConnectorIds, id => id.Key).ToList();
}
else
{
context.Chapters.Add(addChapter);
chapter = addChapter;
}
if (context.Sync() is { success: false }) if (context.Sync() is { success: false })
return false; return false;

View File

@@ -59,7 +59,7 @@ public struct TrangaSettings()
public void Save() public void Save()
{ {
File.WriteAllText(settingsFilePath, JsonConvert.SerializeObject(this)); File.WriteAllText(settingsFilePath, JsonConvert.SerializeObject(this, Formatting.Indented));
} }
public void SetUserAgent(string value) public void SetUserAgent(string value)

View File

@@ -23,7 +23,7 @@ public abstract class BaseWorker : Identifiable
public IEnumerable<BaseWorker> MissingDependencies => DependsOn.Where(d => d.State < WorkerExecutionState.Completed); public IEnumerable<BaseWorker> MissingDependencies => DependsOn.Where(d => d.State < WorkerExecutionState.Completed);
public bool AllDependenciesFulfilled => DependsOn.All(d => d.State >= WorkerExecutionState.Completed); public bool AllDependenciesFulfilled => DependsOn.All(d => d.State >= WorkerExecutionState.Completed);
internal WorkerExecutionState State { get; private set; } internal WorkerExecutionState State { get; private set; }
private static readonly CancellationTokenSource CancellationTokenSource = new(TimeSpan.FromMinutes(10)); private CancellationTokenSource? CancellationTokenSource = null;
protected ILog Log { get; init; } protected ILog Log { get; init; }
/// <summary> /// <summary>
@@ -33,7 +33,7 @@ public abstract class BaseWorker : Identifiable
{ {
Log.Debug($"Cancelled {this}"); Log.Debug($"Cancelled {this}");
this.State = WorkerExecutionState.Cancelled; this.State = WorkerExecutionState.Cancelled;
CancellationTokenSource.Cancel(); CancellationTokenSource?.Cancel();
} }
/// <summary> /// <summary>
@@ -43,7 +43,7 @@ public abstract class BaseWorker : Identifiable
{ {
Log.Debug($"Failed {this}"); Log.Debug($"Failed {this}");
this.State = WorkerExecutionState.Failed; this.State = WorkerExecutionState.Failed;
CancellationTokenSource.Cancel(); CancellationTokenSource?.Cancel();
} }
public BaseWorker(IEnumerable<BaseWorker>? dependsOn = null) public BaseWorker(IEnumerable<BaseWorker>? dependsOn = null)
@@ -68,11 +68,14 @@ public abstract class BaseWorker : Identifiable
/// <item>If <see cref="BaseWorker"/> has run, additional <see cref="BaseWorker"/>.</item> /// <item>If <see cref="BaseWorker"/> has run, additional <see cref="BaseWorker"/>.</item>
/// </list> /// </list>
/// </returns> /// </returns>
public Task<BaseWorker[]> DoWork() public Task<BaseWorker[]> DoWork(Action? callback = null)
{ {
// Start the worker
Log.Debug($"Checking {this}"); Log.Debug($"Checking {this}");
this.CancellationTokenSource = new(TimeSpan.FromMinutes(10));
this.State = WorkerExecutionState.Waiting; this.State = WorkerExecutionState.Waiting;
// Wait for dependencies, start them if necessary
BaseWorker[] missingDependenciesThatNeedStarting = MissingDependencies.Where(d => d.State < WorkerExecutionState.Waiting).ToArray(); BaseWorker[] missingDependenciesThatNeedStarting = MissingDependencies.Where(d => d.State < WorkerExecutionState.Waiting).ToArray();
if(missingDependenciesThatNeedStarting.Any()) if(missingDependenciesThatNeedStarting.Any())
return new Task<BaseWorker[]>(() => missingDependenciesThatNeedStarting); return new Task<BaseWorker[]>(() => missingDependenciesThatNeedStarting);
@@ -80,28 +83,32 @@ public abstract class BaseWorker : Identifiable
if (MissingDependencies.Any()) if (MissingDependencies.Any())
return new Task<BaseWorker[]>(WaitForDependencies); return new Task<BaseWorker[]>(WaitForDependencies);
// Run the actual work
Log.Info($"Running {this}"); Log.Info($"Running {this}");
DateTime startTime = DateTime.UtcNow; DateTime startTime = DateTime.UtcNow;
Task<BaseWorker[]> task = new (DoWorkInternal, CancellationTokenSource.Token); Task<BaseWorker[]> task = new (DoWorkInternal, CancellationTokenSource.Token);
task.GetAwaiter().OnCompleted(() => task.GetAwaiter().OnCompleted(Finish(startTime, callback));
this.State = WorkerExecutionState.Running;
task.Start();
return task;
}
private Action Finish(DateTime startTime, Action? callback = null) => () =>
{ {
DateTime endTime = DateTime.UtcNow; DateTime endTime = DateTime.UtcNow;
Log.Info($"Completed {this}\n\t{endTime.Subtract(startTime).TotalMilliseconds} ms"); Log.Info($"Completed {this}\n\t{endTime.Subtract(startTime).TotalMilliseconds} ms");
this.State = WorkerExecutionState.Completed; this.State = WorkerExecutionState.Completed;
if(this is IPeriodic periodic) if(this is IPeriodic periodic)
periodic.LastExecution = DateTime.UtcNow; periodic.LastExecution = DateTime.UtcNow;
}); callback?.Invoke();
task.Start(); };
this.State = WorkerExecutionState.Running;
return task;
}
protected abstract BaseWorker[] DoWorkInternal(); protected abstract BaseWorker[] DoWorkInternal();
private BaseWorker[] WaitForDependencies() private BaseWorker[] WaitForDependencies()
{ {
Log.Info($"Waiting for {MissingDependencies.Count()} Dependencies {this}:\n\t{string.Join("\n\t", MissingDependencies.Select(d => d.ToString()))}"); Log.Info($"Waiting for {MissingDependencies.Count()} Dependencies {this}:\n\t{string.Join("\n\t", MissingDependencies.Select(d => d.ToString()))}");
while (CancellationTokenSource.IsCancellationRequested == false && MissingDependencies.Any()) while (CancellationTokenSource?.IsCancellationRequested == false && MissingDependencies.Any())
{ {
Thread.Sleep(Tranga.Settings.WorkCycleTimeoutMs); Thread.Sleep(Tranga.Settings.WorkCycleTimeoutMs);
} }

View File

@@ -1,8 +1,9 @@
using System.IO.Compression; using System.IO.Compression;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using API.MangaConnectors;
using API.MangaDownloadClients; using API.MangaDownloadClients;
using API.Schema.MangaContext; using API.Schema.MangaContext;
using API.Schema.MangaContext.MangaConnectors; using Microsoft.EntityFrameworkCore.ChangeTracking;
using SixLabors.ImageSharp; using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Formats.Jpeg; using SixLabors.ImageSharp.Formats.Jpeg;
using SixLabors.ImageSharp.Processing; using SixLabors.ImageSharp.Processing;
@@ -17,17 +18,29 @@ public class DownloadChapterFromMangaconnectorWorker(MangaConnectorId<Chapter> c
internal readonly string MangaConnectorIdId = chId.Key; internal readonly string MangaConnectorIdId = chId.Key;
protected override BaseWorker[] DoWorkInternal() protected override BaseWorker[] DoWorkInternal()
{ {
if (DbContext.MangaConnectorToChapter.Find(MangaConnectorIdId) is not { } MangaConnectorId) if (DbContext.MangaConnectorToChapter.Find(MangaConnectorIdId) is not { } mangaConnectorId)
return []; //TODO Exception? return []; //TODO Exception?
MangaConnector mangaConnector = MangaConnectorId.MangaConnector; if (!Tranga.TryGetMangaConnector(mangaConnectorId.MangaConnectorName, out MangaConnector? mangaConnector))
Chapter chapter = MangaConnectorId.Obj; return []; //TODO Exception?
DbContext.Entry(mangaConnectorId).Navigation(nameof(MangaConnectorId<Chapter>.Obj)).Load();
Chapter chapter = mangaConnectorId.Obj;
if (chapter.Downloaded) if (chapter.Downloaded)
{ {
Log.Info("Chapter was already downloaded."); Log.Info("Chapter was already downloaded.");
return []; return [];
} }
string[] imageUrls = mangaConnector.GetChapterImageUrls(MangaConnectorId); DbContext.Entry(chapter).Navigation(nameof(Chapter.ParentManga)).Load();
DbContext.Entry(chapter.ParentManga).Navigation(nameof(Manga.Library)).Load();
if (chapter.ParentManga.LibraryId is null)
{
Log.Info($"Library is not set for {chapter.ParentManga} {chapter}");
return [];
}
string[] imageUrls = mangaConnector.GetChapterImageUrls(mangaConnectorId);
if (imageUrls.Length < 1) if (imageUrls.Length < 1)
{ {
Log.Info($"No imageUrls for chapter {chapter}"); Log.Info($"No imageUrls for chapter {chapter}");
@@ -82,6 +95,9 @@ public class DownloadChapterFromMangaconnectorWorker(MangaConnectorId<Chapter> c
CopyCoverFromCacheToDownloadLocation(chapter.ParentManga); CopyCoverFromCacheToDownloadLocation(chapter.ParentManga);
Log.Debug($"Creating ComicInfo.xml {chapter}"); Log.Debug($"Creating ComicInfo.xml {chapter}");
foreach (CollectionEntry collectionEntry in DbContext.Entry(chapter.ParentManga).Collections)
collectionEntry.Load();
DbContext.Entry(chapter.ParentManga).Navigation(nameof(Manga.Library)).Load();
File.WriteAllText(Path.Join(tempFolder, "ComicInfo.xml"), chapter.GetComicInfoXmlString()); File.WriteAllText(Path.Join(tempFolder, "ComicInfo.xml"), chapter.GetComicInfoXmlString());
Log.Debug($"Packaging images to archive {chapter}"); Log.Debug($"Packaging images to archive {chapter}");
@@ -147,10 +163,17 @@ public class DownloadChapterFromMangaconnectorWorker(MangaConnectorId<Chapter> c
} }
//TODO MangaConnector Selection //TODO MangaConnector Selection
MangaConnectorId<Manga> mcId = manga.MangaConnectorIds.First(); DbContext.Entry(manga).Collection(m => m.MangaConnectorIds).Load();
MangaConnectorId<Manga> mangaConnectorId = manga.MangaConnectorIds.First();
if (!Tranga.TryGetMangaConnector(mangaConnectorId.MangaConnectorName, out MangaConnector? mangaConnector))
{
Log.Error($"MangaConnector with name {mangaConnectorId.MangaConnectorName} could not be found");
return;
}
Log.Info($"Copying cover to {publicationFolder}"); Log.Info($"Copying cover to {publicationFolder}");
string? fileInCache = manga.CoverFileNameInCache ?? mcId.MangaConnector.SaveCoverImageToCache(mcId); DbContext.Entry(mangaConnectorId).Navigation(nameof(MangaConnectorId<Manga>.Obj)).Load();
string? fileInCache = manga.CoverFileNameInCache ?? mangaConnector.SaveCoverImageToCache(mangaConnectorId);
if (fileInCache is null) if (fileInCache is null)
{ {
Log.Error($"File {fileInCache} does not exist"); Log.Error($"File {fileInCache} does not exist");

View File

@@ -1,5 +1,5 @@
using API.MangaConnectors;
using API.Schema.MangaContext; using API.Schema.MangaContext;
using API.Schema.MangaContext.MangaConnectors;
namespace API.Workers; namespace API.Workers;
@@ -9,12 +9,15 @@ public class DownloadCoverFromMangaconnectorWorker(MangaConnectorId<Manga> mcId,
internal readonly string MangaConnectorIdId = mcId.Key; internal readonly string MangaConnectorIdId = mcId.Key;
protected override BaseWorker[] DoWorkInternal() protected override BaseWorker[] DoWorkInternal()
{ {
if (DbContext.MangaConnectorToManga.Find(MangaConnectorIdId) is not { } MangaConnectorId) if (DbContext.MangaConnectorToManga.Find(MangaConnectorIdId) is not { } mangaConnectorId)
return []; //TODO Exception?
if (!Tranga.TryGetMangaConnector(mangaConnectorId.MangaConnectorName, out MangaConnector? mangaConnector))
return []; //TODO Exception? return []; //TODO Exception?
MangaConnector mangaConnector = MangaConnectorId.MangaConnector;
Manga manga = MangaConnectorId.Obj;
manga.CoverFileNameInCache = mangaConnector.SaveCoverImageToCache(MangaConnectorId); DbContext.Entry(mangaConnectorId).Navigation(nameof(MangaConnectorId<Manga>.Obj)).Load();
Manga manga = mangaConnectorId.Obj;
manga.CoverFileNameInCache = mangaConnector.SaveCoverImageToCache(mangaConnectorId);
DbContext.Sync(); DbContext.Sync();
return []; return [];

View File

@@ -1,5 +1,5 @@
using API.MangaConnectors;
using API.Schema.MangaContext; using API.Schema.MangaContext;
using API.Schema.MangaContext.MangaConnectors;
namespace API.Workers; namespace API.Workers;
@@ -9,13 +9,18 @@ public class RetrieveMangaChaptersFromMangaconnectorWorker(MangaConnectorId<Mang
internal readonly string MangaConnectorIdId = mcId.Key; internal readonly string MangaConnectorIdId = mcId.Key;
protected override BaseWorker[] DoWorkInternal() protected override BaseWorker[] DoWorkInternal()
{ {
if (DbContext.MangaConnectorToManga.Find(MangaConnectorIdId) is not { } MangaConnectorId) if (DbContext.MangaConnectorToManga.Find(MangaConnectorIdId) is not { } mangaConnectorId)
return []; //TODO Exception? return []; //TODO Exception?
MangaConnector mangaConnector = MangaConnectorId.MangaConnector; if (!Tranga.TryGetMangaConnector(mangaConnectorId.MangaConnectorName, out MangaConnector? mangaConnector))
Manga manga = MangaConnectorId.Obj; return []; //TODO Exception?
DbContext.Entry(mangaConnectorId).Navigation(nameof(MangaConnectorId<Manga>.Obj)).Load();
Manga manga = mangaConnectorId.Obj;
DbContext.Entry(manga).Collection(m => m.Chapters).Load();
// This gets all chapters that are not downloaded // This gets all chapters that are not downloaded
(Chapter, MangaConnectorId<Chapter>)[] allChapters = (Chapter, MangaConnectorId<Chapter>)[] allChapters =
mangaConnector.GetChapters(MangaConnectorId, language).DistinctBy(c => c.Item1.Key).ToArray(); mangaConnector.GetChapters(mangaConnectorId, language).DistinctBy(c => c.Item1.Key).ToArray();
int addedChapters = 0; int addedChapters = 0;
foreach ((Chapter chapter, MangaConnectorId<Chapter> mcId) newChapter in allChapters) foreach ((Chapter chapter, MangaConnectorId<Chapter> mcId) newChapter in allChapters)

View File

@@ -13,6 +13,10 @@ public class MoveMangaLibraryWorker(Manga manga, FileLibrary toLibrary, IEnumera
return []; //TODO Exception? return []; //TODO Exception?
if (DbContext.FileLibraries.Find(LibraryId) is not { } toLibrary) if (DbContext.FileLibraries.Find(LibraryId) is not { } toLibrary)
return []; //TODO Exception? return []; //TODO Exception?
DbContext.Entry(manga).Collection(m => m.Chapters).Load();
DbContext.Entry(manga).Navigation(nameof(Manga.Library)).Load();
Dictionary<Chapter, string> oldPath = manga.Chapters.ToDictionary(c => c, c => c.FullArchiveFilePath); Dictionary<Chapter, string> oldPath = manga.Chapters.ToDictionary(c => c, c => c.FullArchiveFilePath);
manga.Library = toLibrary; manga.Library = toLibrary;

View File

@@ -1,4 +1,5 @@
using API.Schema.MangaContext; using API.Schema.MangaContext;
using Microsoft.EntityFrameworkCore;
namespace API.Workers; namespace API.Workers;
@@ -10,7 +11,9 @@ public class CheckForNewChaptersWorker(TimeSpan? interval = null, IEnumerable<Ba
protected override BaseWorker[] DoWorkInternal() protected override BaseWorker[] DoWorkInternal()
{ {
IQueryable<MangaConnectorId<Manga>> connectorIdsManga = DbContext.MangaConnectorToManga.Where(id => id.UseForDownload); IQueryable<MangaConnectorId<Manga>> connectorIdsManga = DbContext.MangaConnectorToManga
.Include(id => id.Obj)
.Where(id => id.UseForDownload);
List<BaseWorker> newWorkers = new(); List<BaseWorker> newWorkers = new();
foreach (MangaConnectorId<Manga> mangaConnectorId in connectorIdsManga) foreach (MangaConnectorId<Manga> mangaConnectorId in connectorIdsManga)

View File

@@ -1,4 +1,5 @@
using API.Schema.MangaContext; using API.Schema.MangaContext;
using Microsoft.EntityFrameworkCore;
namespace API.Workers; namespace API.Workers;
@@ -10,7 +11,9 @@ public class StartNewChapterDownloadsWorker(TimeSpan? interval = null, IEnumerab
public TimeSpan Interval { get; set; } = interval ?? TimeSpan.FromMinutes(1); public TimeSpan Interval { get; set; } = interval ?? TimeSpan.FromMinutes(1);
protected override BaseWorker[] DoWorkInternal() protected override BaseWorker[] DoWorkInternal()
{ {
IQueryable<MangaConnectorId<Chapter>> mangaConnectorIds = DbContext.MangaConnectorToChapter.Where(id => id.Obj.Downloaded == false && id.UseForDownload); IQueryable<MangaConnectorId<Chapter>> mangaConnectorIds = DbContext.MangaConnectorToChapter
.Include(id => id.Obj)
.Where(id => id.Obj.Downloaded == false && id.UseForDownload);
List<BaseWorker> newWorkers = new(); List<BaseWorker> newWorkers = new();
foreach (MangaConnectorId<Chapter> mangaConnectorId in mangaConnectorIds) foreach (MangaConnectorId<Chapter> mangaConnectorId in mangaConnectorIds)

View File

@@ -1,4 +1,6 @@
using API.Schema.MangaContext; using API.Schema.MangaContext;
using Microsoft.EntityFrameworkCore;
namespace API.Workers; namespace API.Workers;
public class UpdateChaptersDownloadedWorker(TimeSpan? interval = null, IEnumerable<BaseWorker>? dependsOn = null) public class UpdateChaptersDownloadedWorker(TimeSpan? interval = null, IEnumerable<BaseWorker>? dependsOn = null)
@@ -8,7 +10,7 @@ public class UpdateChaptersDownloadedWorker(TimeSpan? interval = null, IEnumerab
public TimeSpan Interval { get; set; } = interval??TimeSpan.FromMinutes(60); public TimeSpan Interval { get; set; } = interval??TimeSpan.FromMinutes(60);
protected override BaseWorker[] DoWorkInternal() protected override BaseWorker[] DoWorkInternal()
{ {
foreach (Chapter dbContextChapter in DbContext.Chapters) foreach (Chapter dbContextChapter in DbContext.Chapters.Include(c => c.ParentManga))
dbContextChapter.Downloaded = dbContextChapter.CheckDownloaded(); dbContextChapter.Downloaded = dbContextChapter.CheckDownloaded();
DbContext.Sync(); DbContext.Sync();

View File

@@ -0,0 +1,19 @@
using API.Schema.MangaContext;
namespace API.Workers;
public class UpdateCoversWorker(TimeSpan? interval = null, IEnumerable<BaseWorker>? dependsOn = null)
: BaseWorkerWithContext<MangaContext>(dependsOn), IPeriodic
{
public DateTime LastExecution { get; set; } = DateTime.UnixEpoch;
public TimeSpan Interval { get; set; } = interval ?? TimeSpan.FromHours(6);
protected override BaseWorker[] DoWorkInternal()
{
List<BaseWorker> workers = new();
foreach (MangaConnectorId<Manga> mangaConnectorId in DbContext.MangaConnectorToManga)
workers.Add(new DownloadCoverFromMangaconnectorWorker(mangaConnectorId));
return workers.ToArray();
}
}

View File

@@ -8,7 +8,8 @@ ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium
ENV XDG_CONFIG_HOME=/tmp/.chromium ENV XDG_CONFIG_HOME=/tmp/.chromium
ENV XDG_CACHE_HOME=/tmp/.chromium ENV XDG_CACHE_HOME=/tmp/.chromium
RUN apt-get update \ RUN apt-get update \
&& apt-get install -y libx11-6 libx11-xcb1 libatk1.0-0 libgtk-3-0 libcups2 libdrm2 libxkbcommon0 libxcomposite1 libxdamage1 libxrandr2 libgbm1 libpango-1.0-0 libcairo2 libasound2 libxshmfence1 libnss3 chromium \ && apt-get upgrade -y \
&& apt-get install -y chromium \
&& apt-get autopurge -y \ && apt-get autopurge -y \
&& apt-get autoclean -y && apt-get autoclean -y