mirror of
https://github.com/C9Glax/tranga.git
synced 2025-09-10 11:58:19 +02:00
Compare commits
27 Commits
6b4317834d
...
c5689557b3
Author | SHA1 | Date | |
---|---|---|---|
c5689557b3 | |||
c044532564 | |||
383258ac87 | |||
de36ce9c16 | |||
eba79abf51 | |||
ae20ad47a8 | |||
691506b2d8 | |||
356a22d72e | |||
5d8f203a35 | |||
17995d1603 | |||
d2f9ab64aa | |||
d79dd8c3d5 | |||
2b527e15b0 | |||
305b9d900c | |||
413fb0e69e | |||
64b89d4537 | |||
fab70501ce | |||
0a1021b488 | |||
18c6f64405 | |||
5202bfe782 | |||
8408407a8e | |||
a091250195 | |||
a352495866 | |||
3a46d0fd24 | |||
6034937c23 | |||
454f468fd4 | |||
1ee442ea2e |
@@ -33,4 +33,8 @@
|
||||
<PackageReference Include="System.Drawing.Common" Version="9.0.3" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Migrations\Manga\" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
36
API/Controllers/MaintenanceController.cs
Normal file
36
API/Controllers/MaintenanceController.cs
Normal 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();
|
||||
}
|
||||
}
|
@@ -1,5 +1,5 @@
|
||||
using API.Schema.MangaContext;
|
||||
using API.Schema.MangaContext.MangaConnectors;
|
||||
using API.MangaConnectors;
|
||||
using API.Schema.MangaContext;
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using static Microsoft.AspNetCore.Http.StatusCodes;
|
||||
@@ -20,7 +20,7 @@ public class MangaConnectorController(MangaContext context) : Controller
|
||||
[ProducesResponseType<MangaConnector[]>(Status200OK, "application/json")]
|
||||
public IActionResult GetConnectors()
|
||||
{
|
||||
return Ok(context.MangaConnectors.Select(c => c.Name).ToArray());
|
||||
return Ok(Tranga.MangaConnectors.ToArray());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -34,7 +34,7 @@ public class MangaConnectorController(MangaContext context) : Controller
|
||||
[ProducesResponseType(Status404NotFound)]
|
||||
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 Ok(connector);
|
||||
@@ -48,8 +48,7 @@ public class MangaConnectorController(MangaContext context) : Controller
|
||||
[ProducesResponseType<MangaConnector[]>(Status200OK, "application/json")]
|
||||
public IActionResult GetEnabledConnectors()
|
||||
{
|
||||
|
||||
return Ok(context.MangaConnectors.Where(c => c.Enabled).ToArray());
|
||||
return Ok(Tranga.MangaConnectors.Where(c => c.Enabled).ToArray());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -61,7 +60,7 @@ public class MangaConnectorController(MangaContext context) : Controller
|
||||
public IActionResult GetDisabledConnectors()
|
||||
{
|
||||
|
||||
return Ok(context.MangaConnectors.Where(c => c.Enabled == false).ToArray());
|
||||
return Ok(Tranga.MangaConnectors.Where(c => c.Enabled == false).ToArray());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -78,7 +77,7 @@ public class MangaConnectorController(MangaContext context) : Controller
|
||||
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
|
||||
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();
|
||||
|
||||
connector.Enabled = Enabled;
|
||||
|
@@ -1,8 +1,10 @@
|
||||
using API.Schema.MangaContext;
|
||||
using API.Schema.MangaContext.MangaConnectors;
|
||||
using API.MangaConnectors;
|
||||
using API.Schema.MangaContext;
|
||||
using API.Workers;
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.ChangeTracking;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.Formats.Jpeg;
|
||||
@@ -21,12 +23,12 @@ public class MangaController(MangaContext context) : Controller
|
||||
/// <summary>
|
||||
/// Returns all cached <see cref="Manga"/>
|
||||
/// </summary>
|
||||
/// <response code="200"></response>
|
||||
/// <response code="200"><see cref="Manga"/> Keys/IDs</response>
|
||||
[HttpGet]
|
||||
[ProducesResponseType<Manga[]>(Status200OK, "application/json")]
|
||||
[ProducesResponseType<string[]>(Status200OK, "application/json")]
|
||||
public IActionResult GetAllManga()
|
||||
{
|
||||
Manga[] ret = context.Mangas.ToArray();
|
||||
string[] ret = context.Mangas.Select(m => m.Key).ToArray();
|
||||
return Ok(ret);
|
||||
}
|
||||
|
||||
@@ -39,7 +41,7 @@ public class MangaController(MangaContext context) : Controller
|
||||
[ProducesResponseType<Manga[]>(Status200OK, "application/json")]
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -54,7 +56,7 @@ public class MangaController(MangaContext context) : Controller
|
||||
[ProducesResponseType(Status404NotFound)]
|
||||
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 Ok(manga);
|
||||
}
|
||||
@@ -64,7 +66,7 @@ public class MangaController(MangaContext context) : Controller
|
||||
/// </summary>
|
||||
/// <param name="MangaId"><see cref="Manga"/>.Key</param>
|
||||
/// <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>
|
||||
[HttpDelete("{MangaId}")]
|
||||
[ProducesResponseType(Status200OK)]
|
||||
@@ -100,6 +102,15 @@ public class MangaController(MangaContext context) : Controller
|
||||
if (context.Mangas.Find(MangaIdInto) is not { } into)
|
||||
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);
|
||||
Tranga.AddWorkers(newJobs);
|
||||
|
||||
@@ -173,6 +184,8 @@ public class MangaController(MangaContext context) : Controller
|
||||
if (context.Mangas.Find(MangaId) is not { } manga)
|
||||
return NotFound(nameof(MangaId));
|
||||
|
||||
context.Entry(manga).Collection(m => m.Chapters).Load();
|
||||
|
||||
Chapter[] chapters = manga.Chapters.ToArray();
|
||||
return Ok(chapters);
|
||||
}
|
||||
@@ -193,6 +206,8 @@ public class MangaController(MangaContext context) : Controller
|
||||
if (context.Mangas.Find(MangaId) is not { } manga)
|
||||
return NotFound(nameof(MangaId));
|
||||
|
||||
context.Entry(manga).Collection(m => m.Chapters).Load();
|
||||
|
||||
List<Chapter> chapters = manga.Chapters.Where(c => c.Downloaded).ToList();
|
||||
if (chapters.Count == 0)
|
||||
return NoContent();
|
||||
@@ -216,6 +231,8 @@ public class MangaController(MangaContext context) : Controller
|
||||
if (context.Mangas.Find(MangaId) is not { } manga)
|
||||
return NotFound(nameof(MangaId));
|
||||
|
||||
context.Entry(manga).Collection(m => m.Chapters).Load();
|
||||
|
||||
List<Chapter> chapters = manga.Chapters.Where(c => c.Downloaded == false).ToList();
|
||||
if (chapters.Count == 0)
|
||||
return NoContent();
|
||||
@@ -243,6 +260,8 @@ public class MangaController(MangaContext context) : Controller
|
||||
if (context.Mangas.Find(MangaId) is not { } manga)
|
||||
return NotFound(nameof(MangaId));
|
||||
|
||||
context.Entry(manga).Collection(m => m.Chapters).Load();
|
||||
|
||||
List<Chapter> chapters = manga.Chapters.ToList();
|
||||
if (chapters.Count == 0)
|
||||
{
|
||||
@@ -258,6 +277,10 @@ public class MangaController(MangaContext context) : Controller
|
||||
if (max is null)
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -281,6 +304,8 @@ public class MangaController(MangaContext context) : Controller
|
||||
if (context.Mangas.Find(MangaId) is not { } manga)
|
||||
return NotFound(nameof(MangaId));
|
||||
|
||||
context.Entry(manga).Collection(m => m.Chapters).Load();
|
||||
|
||||
List<Chapter> chapters = manga.Chapters.ToList();
|
||||
if (chapters.Count == 0)
|
||||
{
|
||||
@@ -296,6 +321,10 @@ public class MangaController(MangaContext context) : Controller
|
||||
if (max is null)
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -333,13 +362,17 @@ public class MangaController(MangaContext context) : Controller
|
||||
[HttpPost("{MangaId}/ChangeLibrary/{LibraryId}")]
|
||||
[ProducesResponseType(Status202Accepted)]
|
||||
[ProducesResponseType(Status404NotFound)]
|
||||
public IActionResult MoveFolder(string MangaId, string LibraryId)
|
||||
public IActionResult ChangeLibrary(string MangaId, string LibraryId)
|
||||
{
|
||||
if (context.Mangas.Find(MangaId) is not { } manga)
|
||||
return NotFound(nameof(MangaId));
|
||||
if(context.FileLibraries.Find(LibraryId) is not { } library)
|
||||
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);
|
||||
|
||||
Tranga.AddWorkers([moveLibrary]);
|
||||
@@ -368,23 +401,80 @@ public class MangaController(MangaContext context) : Controller
|
||||
{
|
||||
if (context.Mangas.Find(MangaId) is null)
|
||||
return NotFound(nameof(MangaId));
|
||||
if(context.MangaConnectors.Find(MangaConnectorName) is null)
|
||||
if(!Tranga.TryGetMangaConnector(MangaConnectorName, out MangaConnector? mangaConnector))
|
||||
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)
|
||||
return StatusCode(Status428PreconditionRequired, "Don't know how to download this Manga from MangaConnector");
|
||||
else
|
||||
return StatusCode(Status412PreconditionFailed, "Not linked anyways.");
|
||||
}
|
||||
|
||||
mcId.UseForDownload = IsRequested;
|
||||
if(context.Sync() is { success: false } result)
|
||||
return StatusCode(Status500InternalServerError, result.exceptionMessage);
|
||||
|
||||
|
||||
DownloadCoverFromMangaconnectorWorker downloadCover = new(mcId);
|
||||
RetrieveMangaChaptersFromMangaconnectorWorker retrieveChapters = new(mcId, Tranga.Settings.DownloadLanguage);
|
||||
Tranga.AddWorkers([downloadCover, retrieveChapters]);
|
||||
|
||||
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)));
|
||||
}
|
||||
}
|
@@ -48,30 +48,29 @@ public class NotificationConnectorController(NotificationsContext context) : Con
|
||||
/// Creates a new <see cref="NotificationConnector"/>
|
||||
/// </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>
|
||||
/// <response code="201"></response>
|
||||
/// <response code="200">ID of the new <see cref="NotificationConnector"/></response>
|
||||
/// <response code="500">Error during Database Operation</response>
|
||||
[HttpPut]
|
||||
[ProducesResponseType(Status201Created)]
|
||||
[ProducesResponseType(Status409Conflict)]
|
||||
[ProducesResponseType<string>(Status200OK, "text/plain")]
|
||||
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
|
||||
public IActionResult CreateConnector([FromBody]NotificationConnector notificationConnector)
|
||||
{
|
||||
|
||||
context.NotificationConnectors.Add(notificationConnector);
|
||||
context.Notifications.Add(new ("Added new Notification Connector!", notificationConnector.Name, NotificationUrgency.High));
|
||||
|
||||
if(context.Sync() is { success: false } result)
|
||||
return StatusCode(Status500InternalServerError, result.exceptionMessage);
|
||||
return Created();
|
||||
return Ok(notificationConnector.Name);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new Gotify-<see cref="NotificationConnector"/>
|
||||
/// </summary>
|
||||
/// <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>
|
||||
[HttpPut("Gotify")]
|
||||
[ProducesResponseType<string>(Status201Created, "application/json")]
|
||||
[ProducesResponseType<string>(Status200OK, "text/plain")]
|
||||
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
|
||||
public IActionResult CreateGotifyConnector([FromBody]GotifyRecord gotifyData)
|
||||
{
|
||||
@@ -79,7 +78,7 @@ public class NotificationConnectorController(NotificationsContext context) : Con
|
||||
|
||||
NotificationConnector gotifyConnector = new (gotifyData.Name,
|
||||
gotifyData.Endpoint,
|
||||
new Dictionary<string, string>() { { "X-Gotify-IDOnConnector", gotifyData.AppToken } },
|
||||
new Dictionary<string, string>() { { "X-Gotify-Key", gotifyData.AppToken } },
|
||||
"POST",
|
||||
$"{{\"message\": \"%text\", \"title\": \"%title\", \"Priority\": {gotifyData.Priority}}}");
|
||||
return CreateConnector(gotifyConnector);
|
||||
@@ -89,10 +88,10 @@ public class NotificationConnectorController(NotificationsContext context) : Con
|
||||
/// Creates a new Ntfy-<see cref="NotificationConnector"/>
|
||||
/// </summary>
|
||||
/// <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>
|
||||
[HttpPut("Ntfy")]
|
||||
[ProducesResponseType<string>(Status201Created, "application/json")]
|
||||
[ProducesResponseType<string>(Status200OK, "text/plain")]
|
||||
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
|
||||
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("=","");
|
||||
|
||||
NotificationConnector ntfyConnector = new (ntfyRecord.Name,
|
||||
$"{ntfyRecord.Endpoint}/{ntfyRecord.Topic}?auth={auth}",
|
||||
$"{ntfyRecord.Endpoint}?auth={auth}",
|
||||
new Dictionary<string, string>()
|
||||
{
|
||||
{"Title", "%title"},
|
||||
{"Priority", ntfyRecord.Priority.ToString()},
|
||||
{"Authorization", auth}
|
||||
},
|
||||
"POST",
|
||||
"%text");
|
||||
$"{{\"message\": \"%text\", \"title\": \"%title\", \"Priority\": {ntfyRecord.Priority} \"Topic\": \"{ntfyRecord.Topic}\"}}");
|
||||
return CreateConnector(ntfyConnector);
|
||||
}
|
||||
|
||||
@@ -117,10 +115,10 @@ public class NotificationConnectorController(NotificationsContext context) : Con
|
||||
/// Creates a new Pushover-<see cref="NotificationConnector"/>
|
||||
/// </summary>
|
||||
/// <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>
|
||||
[HttpPut("Pushover")]
|
||||
[ProducesResponseType<string>(Status201Created, "application/json")]
|
||||
[ProducesResponseType<string>(Status200OK, "text/plain")]
|
||||
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
|
||||
public IActionResult CreatePushoverConnector([FromBody]PushoverRecord pushoverRecord)
|
||||
{
|
||||
@@ -154,6 +152,6 @@ public class NotificationConnectorController(NotificationsContext context) : Con
|
||||
|
||||
if(context.Sync() is { success: false } result)
|
||||
return StatusCode(Status500InternalServerError, result.exceptionMessage);
|
||||
return Created();
|
||||
return Ok();
|
||||
}
|
||||
}
|
@@ -1,6 +1,8 @@
|
||||
using API.Schema.MangaContext;
|
||||
using API.MangaConnectors;
|
||||
using API.Schema.MangaContext;
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Soenneker.Utils.String.NeedlemanWunsch;
|
||||
using static Microsoft.AspNetCore.Http.StatusCodes;
|
||||
// ReSharper disable InconsistentNaming
|
||||
|
||||
@@ -28,38 +30,6 @@ public class QueryController(MangaContext context) : Controller
|
||||
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>
|
||||
/// Returns <see cref="Chapter"/> with <paramref name="ChapterId"/>
|
||||
/// </summary>
|
||||
@@ -68,6 +38,7 @@ public class QueryController(MangaContext context) : Controller
|
||||
/// <response code="404"><see cref="Chapter"/> with <paramref name="ChapterId"/> not found</response>
|
||||
[HttpGet("Chapter/{ChapterId}")]
|
||||
[ProducesResponseType<Chapter>(Status200OK, "application/json")]
|
||||
[ProducesResponseType(Status404NotFound)]
|
||||
public IActionResult GetChapter(string ChapterId)
|
||||
{
|
||||
if (context.Chapters.Find(ChapterId) is not { } chapter)
|
||||
@@ -75,4 +46,73 @@ public class QueryController(MangaContext context) : Controller
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
@@ -1,5 +1,5 @@
|
||||
using API.MangaConnectors;
|
||||
using API.Schema.MangaContext;
|
||||
using API.Schema.MangaContext.MangaConnectors;
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using static Microsoft.AspNetCore.Http.StatusCodes;
|
||||
@@ -26,7 +26,7 @@ public class SearchController(MangaContext context) : Controller
|
||||
[ProducesResponseType(Status406NotAcceptable)]
|
||||
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();
|
||||
if (connector.Enabled is false)
|
||||
return StatusCode(Status412PreconditionFailed);
|
||||
@@ -56,7 +56,7 @@ public class SearchController(MangaContext context) : Controller
|
||||
[ProducesResponseType(Status500InternalServerError)]
|
||||
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.");
|
||||
|
||||
if(connector.GetMangaFromUrl(url) is not { } manga)
|
||||
|
@@ -1,7 +1,6 @@
|
||||
using API.APIEndpointRecords;
|
||||
using API.Workers;
|
||||
using Asp.Versioning;
|
||||
using log4net;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using static Microsoft.AspNetCore.Http.StatusCodes;
|
||||
// ReSharper disable InconsistentNaming
|
||||
@@ -14,14 +13,14 @@ namespace API.Controllers;
|
||||
public class WorkerController() : Controller
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns all <see cref="BaseWorker"/>
|
||||
/// Returns all <see cref="BaseWorker"/>.Keys
|
||||
/// </summary>
|
||||
/// <response code="200"></response>
|
||||
/// <response code="200"><see cref="BaseWorker"/> Keys/IDs</response>
|
||||
[HttpGet]
|
||||
[ProducesResponseType<BaseWorker[]>(Status200OK, "application/json")]
|
||||
[ProducesResponseType<string[]>(Status200OK, "application/json")]
|
||||
public IActionResult GetAllWorkers()
|
||||
{
|
||||
return Ok(Tranga.AllWorkers.ToArray());
|
||||
return Ok(Tranga.GetRunningWorkers().Select(w => w.Key).ToArray());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -31,9 +30,9 @@ public class WorkerController() : Controller
|
||||
/// <response code="200"></response>
|
||||
[HttpPost("WithIDs")]
|
||||
[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>
|
||||
@@ -43,9 +42,9 @@ public class WorkerController() : Controller
|
||||
/// <response code="200"></response>
|
||||
[HttpGet("State/{State}")]
|
||||
[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>
|
||||
@@ -57,9 +56,9 @@ public class WorkerController() : Controller
|
||||
[HttpGet("{WorkerId}")]
|
||||
[ProducesResponseType<BaseWorker>(Status200OK, "application/json")]
|
||||
[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 Ok(worker);
|
||||
}
|
||||
@@ -73,41 +72,14 @@ public class WorkerController() : Controller
|
||||
[HttpDelete("{WorkerId}")]
|
||||
[ProducesResponseType(Status200OK)]
|
||||
[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));
|
||||
Tranga.RemoveWorker(worker);
|
||||
Tranga.StopWorker(worker);
|
||||
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>
|
||||
/// Starts <see cref="BaseWorker"/> with <paramref name="WorkerId"/>
|
||||
/// </summary>
|
||||
@@ -119,15 +91,15 @@ public class WorkerController() : Controller
|
||||
[ProducesResponseType(Status202Accepted)]
|
||||
[ProducesResponseType(Status404NotFound)]
|
||||
[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));
|
||||
|
||||
if (worker.State >= WorkerExecutionState.Waiting)
|
||||
return StatusCode(Status412PreconditionFailed, "Already running");
|
||||
|
||||
Tranga.MarkWorkerForStart(worker);
|
||||
Tranga.StartWorker(worker);
|
||||
return Ok();
|
||||
}
|
||||
|
||||
@@ -140,9 +112,9 @@ public class WorkerController() : Controller
|
||||
/// <response code="208"><see cref="BaseWorker"/> was not running</response>
|
||||
[HttpPost("{WorkerId}/Stop")]
|
||||
[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));
|
||||
|
||||
if(worker.State is < WorkerExecutionState.Running or >= WorkerExecutionState.Completed)
|
||||
|
@@ -1,8 +1,9 @@
|
||||
using System.Text.RegularExpressions;
|
||||
using API.MangaDownloadClients;
|
||||
using API.Schema.MangaContext;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace API.Schema.MangaContext.MangaConnectors;
|
||||
namespace API.MangaConnectors;
|
||||
|
||||
public class ComickIo : MangaConnector
|
||||
{
|
||||
@@ -176,19 +177,19 @@ public class ComickIo : MangaConnector
|
||||
byte whatever = 0;
|
||||
List<AltTitle> altTitles = altTitlesArray?
|
||||
.Select(token => new AltTitle(token.Value<string>("lang")??whatever++.ToString(), token.Value<string>("title")!))
|
||||
.ToList()!;
|
||||
.ToList()??[];
|
||||
|
||||
JArray? authorsArray = json["authors"] as JArray;
|
||||
JArray? artistsArray = json["artists"] as JArray;
|
||||
List<Author> authors = authorsArray?.Concat(artistsArray!)
|
||||
.Select(token => new Author(token.Value<string>("name")!))
|
||||
.DistinctBy(a => a.Key)
|
||||
.ToList()!;
|
||||
.ToList()??[];
|
||||
|
||||
JArray? genreArray = json["comic"]?["md_comic_md_genres"] as JArray;
|
||||
List<MangaTag> tags = genreArray?
|
||||
.Select(token => new MangaTag(token["md_genres"]?.Value<string>("name")!))
|
||||
.ToList()!;
|
||||
.ToList()??[];
|
||||
|
||||
JArray? linksArray = json["comic"]?["links"] as JArray;
|
||||
List<Link> links = linksArray?
|
||||
@@ -220,7 +221,7 @@ public class ComickIo : MangaConnector
|
||||
_ => kv.Key
|
||||
};
|
||||
return new Link(key, fullUrl);
|
||||
}).ToList()!;
|
||||
}).ToList()??[];
|
||||
|
||||
if(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,
|
||||
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)
|
||||
@@ -250,8 +253,9 @@ public class ComickIo : MangaConnector
|
||||
continue;
|
||||
|
||||
Chapter ch = new (mcIdManga.Obj, chapterNum, volumeNum, title);
|
||||
|
||||
chapters.Add((ch, new (ch, this, hid, url)));
|
||||
MangaConnectorId<Chapter> mcId = new(ch, this, hid, url);
|
||||
ch.MangaConnectorIds.Add(mcId);
|
||||
chapters.Add((ch, mcId));
|
||||
}
|
||||
return chapters;
|
||||
}
|
@@ -1,17 +1,17 @@
|
||||
namespace API.Schema.MangaContext.MangaConnectors;
|
||||
using API.Schema.MangaContext;
|
||||
|
||||
namespace API.MangaConnectors;
|
||||
|
||||
public class Global : MangaConnector
|
||||
{
|
||||
private MangaContext context { get; init; }
|
||||
public Global(MangaContext context) : base("Global", ["all"], [""], "")
|
||||
public Global() : base("Global", ["all"], [""], "")
|
||||
{
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
public override (Manga, MangaConnectorId<Manga>)[] SearchManga(string mangaSearchName)
|
||||
{
|
||||
//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
|
||||
Task<(Manga, MangaConnectorId<Manga>)[]>[] tasks =
|
||||
@@ -32,7 +32,7 @@ public class Global : MangaConnector
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -44,11 +44,15 @@ public class Global : MangaConnector
|
||||
public override (Chapter, MangaConnectorId<Chapter>)[] GetChapters(MangaConnectorId<Manga> manga,
|
||||
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)
|
||||
{
|
||||
return chapterId.MangaConnector.GetChapterImageUrls(chapterId);
|
||||
if (!Tranga.TryGetMangaConnector(chapterId.MangaConnectorName, out MangaConnector? mangaConnector))
|
||||
return [];
|
||||
return mangaConnector.GetChapterImageUrls(chapterId);
|
||||
}
|
||||
}
|
@@ -2,11 +2,12 @@
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using System.Text.RegularExpressions;
|
||||
using API.MangaDownloadClients;
|
||||
using API.Schema.MangaContext;
|
||||
using log4net;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace API.Schema.MangaContext.MangaConnectors;
|
||||
namespace API.MangaConnectors;
|
||||
|
||||
[PrimaryKey("Name")]
|
||||
public abstract class MangaConnector(string name, string[] supportedLanguages, string[] baseUris, string iconUrl)
|
@@ -1,8 +1,9 @@
|
||||
using System.Text.RegularExpressions;
|
||||
using API.MangaDownloadClients;
|
||||
using API.Schema.MangaContext;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace API.Schema.MangaContext.MangaConnectors;
|
||||
namespace API.MangaConnectors;
|
||||
|
||||
public class MangaDex : MangaConnector
|
||||
{
|
||||
@@ -276,9 +277,9 @@ public class MangaDex : MangaConnector
|
||||
_ => kv.Key
|
||||
};
|
||||
return new Link(key, url);
|
||||
}).ToList()!;
|
||||
}).ToList()??[];
|
||||
|
||||
List<AltTitle> altTitles = (altTitlesJArray??[])
|
||||
List<AltTitle> altTitles = altTitlesJArray?
|
||||
.Select(t =>
|
||||
{
|
||||
JObject? j = t as JObject;
|
||||
@@ -286,19 +287,19 @@ public class MangaDex : MangaConnector
|
||||
if (p is null)
|
||||
return null;
|
||||
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")
|
||||
.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)
|
||||
.Where(x => x is not null).ToList()!;
|
||||
.Where(x => x is not null).Cast<MangaTag>().ToList()??[];
|
||||
|
||||
List<Author> authors = relationships
|
||||
.Where(r => r["type"]?.Value<string>() == "author")
|
||||
.Select(t => t["attributes"]?.Value<string>("name"))
|
||||
.Select(str => str is not null ? new Author(str) : null)
|
||||
.Where(x => x is not null).ToList()!;
|
||||
.Where(x => x is not null).Cast<Author>().ToList();
|
||||
|
||||
|
||||
MangaReleaseStatus releaseStatus = status switch
|
||||
@@ -312,9 +313,11 @@ public class MangaDex : MangaConnector
|
||||
string websiteUrl = $"https://mangadex.org/title/{id}";
|
||||
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);
|
||||
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)
|
||||
@@ -333,6 +336,8 @@ public class MangaDex : MangaConnector
|
||||
|
||||
string websiteUrl = $"https://mangadex.org/chapter/{id}";
|
||||
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);
|
||||
}
|
||||
}
|
@@ -1,11 +1,12 @@
|
||||
using System.Net;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Net;
|
||||
using log4net;
|
||||
|
||||
namespace API.MangaDownloadClients;
|
||||
|
||||
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 DownloadClient()
|
||||
|
@@ -10,8 +10,6 @@ namespace API.MangaDownloadClients;
|
||||
|
||||
public class FlareSolverrDownloadClient : DownloadClient
|
||||
{
|
||||
|
||||
|
||||
internal override RequestResult MakeRequestInternal(string url, string? referrer = null, string? clickButton = null)
|
||||
{
|
||||
if (clickButton is not null)
|
||||
|
@@ -5,6 +5,7 @@ namespace API.MangaDownloadClients;
|
||||
|
||||
internal class HttpDownloadClient : DownloadClient
|
||||
{
|
||||
private static readonly FlareSolverrDownloadClient FlareSolverrDownloadClient = new();
|
||||
internal override RequestResult MakeRequestInternal(string url, string? referrer = null, string? clickButton = null)
|
||||
{
|
||||
if (clickButton is not null)
|
||||
@@ -36,7 +37,7 @@ internal class HttpDownloadClient : DownloadClient
|
||||
(s.Product?.Name ?? "").Contains("cloudflare", StringComparison.InvariantCultureIgnoreCase)))
|
||||
{
|
||||
Log.Debug("Retrying with FlareSolverr!");
|
||||
return new FlareSolverrDownloadClient().MakeRequestInternal(url, referrer, clickButton);
|
||||
return FlareSolverrDownloadClient.MakeRequestInternal(url, referrer, clickButton);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@@ -11,7 +11,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
namespace API.Migrations.Manga
|
||||
{
|
||||
[DbContext(typeof(MangaContext))]
|
||||
[Migration("20250703192023_Initial")]
|
||||
[Migration("20250722203315_Initial")]
|
||||
partial class Initial
|
||||
{
|
||||
/// <inheritdoc />
|
||||
@@ -24,6 +24,39 @@ namespace API.Migrations.Manga
|
||||
|
||||
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 =>
|
||||
{
|
||||
b.Property<string>("Key")
|
||||
@@ -177,8 +210,6 @@ namespace API.Migrations.Manga
|
||||
|
||||
b.HasKey("Key");
|
||||
|
||||
b.HasIndex("MangaConnectorName");
|
||||
|
||||
b.HasIndex("ObjId");
|
||||
|
||||
b.ToTable("MangaConnectorToChapter");
|
||||
@@ -213,46 +244,11 @@ namespace API.Migrations.Manga
|
||||
|
||||
b.HasKey("Key");
|
||||
|
||||
b.HasIndex("MangaConnectorName");
|
||||
|
||||
b.HasIndex("ObjId");
|
||||
|
||||
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 =>
|
||||
{
|
||||
b.Property<string>("Tag")
|
||||
@@ -332,23 +328,23 @@ namespace API.Migrations.Manga
|
||||
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");
|
||||
});
|
||||
|
||||
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");
|
||||
});
|
||||
|
||||
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");
|
||||
});
|
||||
@@ -445,39 +441,23 @@ namespace API.Migrations.Manga
|
||||
|
||||
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")
|
||||
.WithMany("MangaConnectorIds")
|
||||
.HasForeignKey("ObjId")
|
||||
.OnDelete(DeleteBehavior.NoAction)
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("MangaConnector");
|
||||
|
||||
b.Navigation("Obj");
|
||||
});
|
||||
|
||||
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")
|
||||
.WithMany("MangaConnectorIds")
|
||||
.HasForeignKey("ObjId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("MangaConnector");
|
||||
|
||||
b.Navigation("Obj");
|
||||
});
|
||||
|
@@ -36,7 +36,7 @@ namespace API.Migrations.Manga
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "MangaConnectors",
|
||||
name: "MangaConnector",
|
||||
columns: table => new
|
||||
{
|
||||
Name = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: false),
|
||||
@@ -47,7 +47,7 @@ namespace API.Migrations.Manga
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_MangaConnectors", x => x.Name);
|
||||
table.PrimaryKey("PK_MangaConnector", x => x.Name);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
@@ -201,12 +201,6 @@ namespace API.Migrations.Manga
|
||||
constraints: table =>
|
||||
{
|
||||
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(
|
||||
name: "FK_MangaConnectorToManga_Mangas_ObjId",
|
||||
column: x => x.ObjId,
|
||||
@@ -282,12 +276,7 @@ namespace API.Migrations.Manga
|
||||
name: "FK_MangaConnectorToChapter_Chapters_ObjId",
|
||||
column: x => x.ObjId,
|
||||
principalTable: "Chapters",
|
||||
principalColumn: "Key");
|
||||
table.ForeignKey(
|
||||
name: "FK_MangaConnectorToChapter_MangaConnectors_MangaConnectorName",
|
||||
column: x => x.MangaConnectorName,
|
||||
principalTable: "MangaConnectors",
|
||||
principalColumn: "Name",
|
||||
principalColumn: "Key",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
@@ -311,21 +300,11 @@ namespace API.Migrations.Manga
|
||||
table: "Link",
|
||||
column: "MangaKey");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_MangaConnectorToChapter_MangaConnectorName",
|
||||
table: "MangaConnectorToChapter",
|
||||
column: "MangaConnectorName");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_MangaConnectorToChapter_ObjId",
|
||||
table: "MangaConnectorToChapter",
|
||||
column: "ObjId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_MangaConnectorToManga_MangaConnectorName",
|
||||
table: "MangaConnectorToManga",
|
||||
column: "MangaConnectorName");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_MangaConnectorToManga_ObjId",
|
||||
table: "MangaConnectorToManga",
|
||||
@@ -359,6 +338,9 @@ namespace API.Migrations.Manga
|
||||
migrationBuilder.DropTable(
|
||||
name: "Link");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "MangaConnector");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "MangaConnectorToChapter");
|
||||
|
||||
@@ -377,9 +359,6 @@ namespace API.Migrations.Manga
|
||||
migrationBuilder.DropTable(
|
||||
name: "Chapters");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "MangaConnectors");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Tags");
|
||||
|
@@ -21,6 +21,39 @@ namespace API.Migrations.Manga
|
||||
|
||||
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 =>
|
||||
{
|
||||
b.Property<string>("Key")
|
||||
@@ -174,8 +207,6 @@ namespace API.Migrations.Manga
|
||||
|
||||
b.HasKey("Key");
|
||||
|
||||
b.HasIndex("MangaConnectorName");
|
||||
|
||||
b.HasIndex("ObjId");
|
||||
|
||||
b.ToTable("MangaConnectorToChapter");
|
||||
@@ -210,46 +241,11 @@ namespace API.Migrations.Manga
|
||||
|
||||
b.HasKey("Key");
|
||||
|
||||
b.HasIndex("MangaConnectorName");
|
||||
|
||||
b.HasIndex("ObjId");
|
||||
|
||||
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 =>
|
||||
{
|
||||
b.Property<string>("Tag")
|
||||
@@ -329,23 +325,23 @@ namespace API.Migrations.Manga
|
||||
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");
|
||||
});
|
||||
|
||||
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");
|
||||
});
|
||||
|
||||
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");
|
||||
});
|
||||
@@ -442,39 +438,23 @@ namespace API.Migrations.Manga
|
||||
|
||||
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")
|
||||
.WithMany("MangaConnectorIds")
|
||||
.HasForeignKey("ObjId")
|
||||
.OnDelete(DeleteBehavior.NoAction)
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("MangaConnector");
|
||||
|
||||
b.Navigation("Obj");
|
||||
});
|
||||
|
||||
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")
|
||||
.WithMany("MangaConnectorIds")
|
||||
.HasForeignKey("ObjId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("MangaConnector");
|
||||
|
||||
b.Navigation("Obj");
|
||||
});
|
||||
|
||||
|
@@ -34,7 +34,7 @@ public class NamedSwaggerGenOptions : IConfigureNamedOptions<SwaggerGenOptions>
|
||||
{
|
||||
var info = new OpenApiInfo()
|
||||
{
|
||||
Title = "Test API " + description.GroupName,
|
||||
Title = "Tranga " + description.GroupName,
|
||||
Version = description.ApiVersion.ToString()
|
||||
};
|
||||
if (description.IsDeprecated)
|
||||
|
@@ -1,8 +1,8 @@
|
||||
using System.Reflection;
|
||||
using API;
|
||||
using API.MangaConnectors;
|
||||
using API.Schema.LibraryContext;
|
||||
using API.Schema.MangaContext;
|
||||
using API.Schema.MangaContext.MangaConnectors;
|
||||
using API.Schema.NotificationsContext;
|
||||
using Asp.Versioning;
|
||||
using Asp.Versioning.Builder;
|
||||
@@ -109,14 +109,6 @@ using (IServiceScope scope = app.Services.CreateScope())
|
||||
MangaContext context = scope.ServiceProvider.GetRequiredService<MangaContext>();
|
||||
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())
|
||||
context.FileLibraries.Add(new FileLibrary(Tranga.Settings.DownloadLocation, "Default FileLibrary"));
|
||||
|
||||
@@ -143,9 +135,9 @@ using (IServiceScope scope = app.Services.CreateScope())
|
||||
context.Sync();
|
||||
}
|
||||
|
||||
Tranga.SetServiceProvider(app.Services);
|
||||
Tranga.StartLogger();
|
||||
|
||||
Tranga.PeriodicWorkerStarterThread.Start(app.Services);
|
||||
Tranga.AddDefaultWorkers();
|
||||
|
||||
app.UseCors("AllowAll");
|
||||
|
||||
|
@@ -4,7 +4,6 @@ using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Xml.Linq;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace API.Schema.MangaContext;
|
||||
@@ -13,30 +12,11 @@ namespace API.Schema.MangaContext;
|
||||
public class Chapter : Identifiable, IComparable<Chapter>
|
||||
{
|
||||
[StringLength(64)] [Required] public string ParentMangaId { get; init; } = null!;
|
||||
private Manga? _parentManga;
|
||||
[JsonIgnore] public Manga ParentManga = null!;
|
||||
|
||||
[JsonIgnore]
|
||||
public Manga ParentManga
|
||||
{
|
||||
get => _lazyLoader.Load(this, ref _parentManga) ?? throw new InvalidOperationException();
|
||||
init
|
||||
{
|
||||
ParentMangaId = value.Key;
|
||||
_parentManga = value;
|
||||
}
|
||||
}
|
||||
|
||||
[NotMapped]
|
||||
public Dictionary<string, string> IdsOnMangaConnectors =>
|
||||
[NotMapped] public Dictionary<string, string> IdsOnMangaConnectors =>
|
||||
MangaConnectorIds.ToDictionary(id => id.MangaConnectorName, id => id.IdOnConnectorSite);
|
||||
|
||||
private ICollection<MangaConnectorId<Chapter>>? _mangaConnectorIds;
|
||||
[JsonIgnore]
|
||||
public ICollection<MangaConnectorId<Chapter>> MangaConnectorIds
|
||||
{
|
||||
get => _lazyLoader.Load(this, ref _mangaConnectorIds) ?? throw new InvalidOperationException();
|
||||
init => _mangaConnectorIds = value;
|
||||
}
|
||||
[JsonIgnore] public ICollection<MangaConnectorId<Chapter>> MangaConnectorIds = null!;
|
||||
|
||||
public int? VolumeNumber { 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; }
|
||||
[NotMapped] public string FullArchiveFilePath => Path.Join(ParentManga.FullDirectoryPath, FileName);
|
||||
|
||||
private readonly ILazyLoader _lazyLoader = null!;
|
||||
|
||||
public Chapter(Manga parentManga, string chapterNumber,
|
||||
int? volumeNumber, string? title = null)
|
||||
: base(TokenGen.CreateToken(typeof(Chapter), parentManga.Key, chapterNumber))
|
||||
@@ -61,15 +39,15 @@ public class Chapter : Identifiable, IComparable<Chapter>
|
||||
this.Title = title;
|
||||
this.FileName = GetArchiveFilePath();
|
||||
this.Downloaded = false;
|
||||
this.MangaConnectorIds = [];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EF ONLY!!!
|
||||
/// </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)
|
||||
{
|
||||
this._lazyLoader = lazyLoader;
|
||||
this.VolumeNumber = volumeNumber;
|
||||
this.ChapterNumber = chapterNumber;
|
||||
this.Title = title;
|
||||
|
@@ -18,21 +18,11 @@ public class Manga : Identifiable
|
||||
[JsonIgnore] [Url] [StringLength(512)] public string CoverUrl { get; internal set; }
|
||||
[Required] public MangaReleaseStatus ReleaseStatus { get; internal set; }
|
||||
[StringLength(64)] public string? LibraryId { get; private set; }
|
||||
private FileLibrary? _library;
|
||||
[JsonIgnore]
|
||||
public FileLibrary? Library
|
||||
{
|
||||
get => _lazyLoader.Load(this, ref _library);
|
||||
set
|
||||
{
|
||||
LibraryId = value?.Key;
|
||||
_library = value;
|
||||
}
|
||||
}
|
||||
[JsonIgnore] public FileLibrary? Library = null!;
|
||||
|
||||
public ICollection<Author> Authors { get; internal set; }= null!;
|
||||
public ICollection<MangaTag> MangaTags { get; internal set; }= null!;
|
||||
public ICollection<Link> Links { get; internal set; }= null!;
|
||||
public ICollection<Author> Authors { get; internal set; } = null!;
|
||||
public ICollection<MangaTag> MangaTags { get; internal set; } = null!;
|
||||
public ICollection<Link> Links { get; internal set; } = null!;
|
||||
public ICollection<AltTitle> AltTitles { get; internal set; } = null!;
|
||||
[Required] public float IgnoreChaptersBefore { get; internal set; }
|
||||
[StringLength(1024)] [Required] public string DirectoryName { get; private set; }
|
||||
@@ -45,25 +35,12 @@ public class Manga : Identifiable
|
||||
public string? FullDirectoryPath => Library is not null ? Path.Join(Library.BasePath, DirectoryName) : null;
|
||||
|
||||
[NotMapped] public ICollection<string> ChapterIds => Chapters.Select(c => c.Key).ToList();
|
||||
private ICollection<Chapter>? _chapters;
|
||||
[JsonIgnore]
|
||||
public ICollection<Chapter> Chapters
|
||||
{
|
||||
get => _lazyLoader.Load(this, ref _chapters) ?? throw new InvalidOperationException();
|
||||
init => _chapters = value;
|
||||
}
|
||||
[JsonIgnore] public ICollection<Chapter> Chapters = null!;
|
||||
|
||||
[NotMapped] public Dictionary<string, string> IdsOnMangaConnectors =>
|
||||
MangaConnectorIds.ToDictionary(id => id.MangaConnectorName, id => id.IdOnConnectorSite);
|
||||
private ICollection<MangaConnectorId<Manga>>? _mangaConnectorIds;
|
||||
[JsonIgnore]
|
||||
public ICollection<MangaConnectorId<Manga>> MangaConnectorIds
|
||||
{
|
||||
get => _lazyLoader.Load(this, ref _mangaConnectorIds) ?? throw new InvalidOperationException();
|
||||
private set => _mangaConnectorIds = value;
|
||||
}
|
||||
|
||||
private readonly ILazyLoader _lazyLoader = null!;
|
||||
[NotMapped] public ICollection<string> MangaConnectorIdsIds => MangaConnectorIds.Select(id => id.Key).ToList();
|
||||
[JsonIgnore] public ICollection<MangaConnectorId<Manga>> MangaConnectorIds = null!;
|
||||
|
||||
public Manga(string name, string description, string coverUrl, MangaReleaseStatus releaseStatus,
|
||||
ICollection<Author> authors, ICollection<MangaTag> mangaTags, ICollection<Link> links, ICollection<AltTitle> altTitles,
|
||||
@@ -84,17 +61,17 @@ public class Manga : Identifiable
|
||||
this.Year = year;
|
||||
this.OriginalLanguage = originalLanguage;
|
||||
this.Chapters = [];
|
||||
this.MangaConnectorIds = [];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EF ONLY!!!
|
||||
/// </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,
|
||||
string directoryName, float ignoreChaptersBefore, string? libraryId, uint? year, string? originalLanguage)
|
||||
: base(key)
|
||||
{
|
||||
this._lazyLoader = lazyLoader;
|
||||
this.Name = name;
|
||||
this.Description = description;
|
||||
this.CoverUrl = coverUrl;
|
||||
|
@@ -1,7 +1,6 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using API.Schema.MangaContext.MangaConnectors;
|
||||
using API.MangaConnectors;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace API.Schema.MangaContext;
|
||||
@@ -9,44 +8,21 @@ namespace API.Schema.MangaContext;
|
||||
[PrimaryKey("Key")]
|
||||
public class MangaConnectorId<T> : Identifiable where T : Identifiable
|
||||
{
|
||||
[StringLength(64)] [Required] public string ObjId { get; private set; } = null!;
|
||||
[JsonIgnore] private T? _obj;
|
||||
[StringLength(64)] [Required] public string ObjId { get; internal set; }
|
||||
[JsonIgnore] public T Obj = null!;
|
||||
|
||||
[JsonIgnore]
|
||||
public T Obj
|
||||
{
|
||||
get => _lazyLoader.Load(this, ref _obj) ?? throw new InvalidOperationException();
|
||||
internal set
|
||||
{
|
||||
ObjId = value.Key;
|
||||
_obj = value;
|
||||
}
|
||||
}
|
||||
|
||||
[StringLength(32)] [Required] public string MangaConnectorName { get; private set; } = null!;
|
||||
[JsonIgnore] private MangaConnector? _mangaConnector;
|
||||
[JsonIgnore]
|
||||
public MangaConnector MangaConnector
|
||||
{
|
||||
get => _lazyLoader.Load(this, ref _mangaConnector) ?? throw new InvalidOperationException();
|
||||
init
|
||||
{
|
||||
MangaConnectorName = value.Name;
|
||||
_mangaConnector = value;
|
||||
}
|
||||
}
|
||||
[StringLength(32)] [Required] public string MangaConnectorName { get; private set; }
|
||||
|
||||
[StringLength(256)] [Required] public string IdOnConnectorSite { get; init; }
|
||||
[Url] [StringLength(512)] public string? WebsiteUrl { get; internal init; }
|
||||
public bool UseForDownload { get; internal set; }
|
||||
|
||||
private readonly ILazyLoader _lazyLoader = null!;
|
||||
|
||||
public MangaConnectorId(T obj, MangaConnector mangaConnector, string idOnConnectorSite, string? websiteUrl, bool useForDownload = false)
|
||||
: base(TokenGen.CreateToken(typeof(MangaConnectorId<T>), mangaConnector.Name, idOnConnectorSite))
|
||||
{
|
||||
this.Obj = obj;
|
||||
this.MangaConnector = mangaConnector;
|
||||
this.ObjId = obj.Key;
|
||||
this.MangaConnectorName = mangaConnector.Name;
|
||||
this.IdOnConnectorSite = idOnConnectorSite;
|
||||
this.WebsiteUrl = websiteUrl;
|
||||
this.UseForDownload = useForDownload;
|
||||
@@ -55,10 +31,9 @@ public class MangaConnectorId<T> : Identifiable where T : Identifiable
|
||||
/// <summary>
|
||||
/// EF CORE ONLY!!!
|
||||
/// </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)
|
||||
{
|
||||
this._lazyLoader = lazyLoader;
|
||||
this.ObjId = objId;
|
||||
this.MangaConnectorName = mangaConnectorName;
|
||||
this.IdOnConnectorSite = idOnConnectorSite;
|
||||
@@ -66,5 +41,5 @@ public class MangaConnectorId<T> : Identifiable where T : Identifiable
|
||||
this.UseForDownload = useForDownload;
|
||||
}
|
||||
|
||||
public override string ToString() => $"{base.ToString()} {_obj}";
|
||||
public override string ToString() => $"{base.ToString()} {Obj}";
|
||||
}
|
@@ -1,12 +1,12 @@
|
||||
using API.Schema.MangaContext.MangaConnectors;
|
||||
using API.MangaConnectors;
|
||||
using API.Schema.MangaContext.MetadataFetchers;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Query;
|
||||
|
||||
namespace API.Schema.MangaContext;
|
||||
|
||||
public class MangaContext(DbContextOptions<MangaContext> options) : TrangaBaseContext<MangaContext>(options)
|
||||
{
|
||||
public DbSet<MangaConnector> MangaConnectors { get; set; }
|
||||
public DbSet<Manga> Mangas { get; set; }
|
||||
public DbSet<FileLibrary> FileLibraries { get; set; }
|
||||
public DbSet<Chapter> Chapters { get; set; }
|
||||
@@ -31,26 +31,12 @@ public class MangaContext(DbContextOptions<MangaContext> options) : TrangaBaseCo
|
||||
.WithOne(c => c.ParentManga)
|
||||
.HasForeignKey(c => c.ParentMangaId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
modelBuilder.Entity<Manga>()
|
||||
.Navigation(m => m.Chapters)
|
||||
.EnableLazyLoading();
|
||||
modelBuilder.Entity<Chapter>()
|
||||
.Navigation(c => c.ParentManga)
|
||||
.EnableLazyLoading();
|
||||
//Chapter has MangaConnectorIds
|
||||
modelBuilder.Entity<Chapter>()
|
||||
.HasMany<MangaConnectorId<Chapter>>(c => c.MangaConnectorIds)
|
||||
.WithOne(id => id.Obj)
|
||||
.HasForeignKey(id => id.ObjId)
|
||||
.OnDelete(DeleteBehavior.NoAction);
|
||||
modelBuilder.Entity<MangaConnectorId<Chapter>>()
|
||||
.HasOne<MangaConnector>(id => id.MangaConnector)
|
||||
.WithMany()
|
||||
.HasForeignKey(id => id.MangaConnectorName)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
modelBuilder.Entity<MangaConnectorId<Chapter>>()
|
||||
.Navigation(entry => entry.MangaConnector)
|
||||
.EnableLazyLoading();
|
||||
//Manga owns MangaAltTitles
|
||||
modelBuilder.Entity<Manga>()
|
||||
.OwnsMany<AltTitle>(m => m.AltTitles)
|
||||
@@ -95,17 +81,6 @@ public class MangaContext(DbContextOptions<MangaContext> options) : TrangaBaseCo
|
||||
.WithOne(id => id.Obj)
|
||||
.HasForeignKey(id => id.ObjId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
modelBuilder.Entity<Manga>()
|
||||
.Navigation(m => m.MangaConnectorIds)
|
||||
.EnableLazyLoading();
|
||||
modelBuilder.Entity<MangaConnectorId<Manga>>()
|
||||
.HasOne<MangaConnector>(id => id.MangaConnector)
|
||||
.WithMany()
|
||||
.HasForeignKey(id => id.MangaConnectorName)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
modelBuilder.Entity<MangaConnectorId<Manga>>()
|
||||
.Navigation(entry => entry.MangaConnector)
|
||||
.EnableLazyLoading();
|
||||
|
||||
|
||||
//FileLibrary has many Mangas
|
||||
@@ -114,9 +89,6 @@ public class MangaContext(DbContextOptions<MangaContext> options) : TrangaBaseCo
|
||||
.WithOne(m => m.Library)
|
||||
.HasForeignKey(m => m.LibraryId)
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
modelBuilder.Entity<Manga>()
|
||||
.Navigation(m => m.Library)
|
||||
.EnableLazyLoading();
|
||||
|
||||
modelBuilder.Entity<MetadataFetcher>()
|
||||
.HasDiscriminator<string>(nameof(MetadataEntry))
|
||||
@@ -131,4 +103,23 @@ public class MangaContext(DbContextOptions<MangaContext> options) : TrangaBaseCo
|
||||
.WithMany()
|
||||
.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);
|
||||
}
|
@@ -1,6 +1,7 @@
|
||||
using System.Text.RegularExpressions;
|
||||
using JikanDotNet;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.ChangeTracking;
|
||||
|
||||
namespace API.Schema.MangaContext.MetadataFetchers;
|
||||
|
||||
@@ -47,6 +48,11 @@ public class MyAnimeList : MetadataFetcher
|
||||
public override void UpdateMetadata(MetadataEntry metadataEntry, MangaContext dbContext)
|
||||
{
|
||||
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;
|
||||
try
|
||||
{
|
||||
|
@@ -44,40 +44,29 @@ public class NotificationConnector(string name, string url, Dictionary<string, s
|
||||
public void SendNotification(string title, string notificationText)
|
||||
{
|
||||
Log.Info($"Sending notification: {title} - {notificationText}");
|
||||
CustomWebhookFormatProvider formatProvider = new (title, notificationText);
|
||||
string formattedUrl = string.Format(formatProvider, Url);
|
||||
string formattedBody = string.Format(formatProvider, Body, title, notificationText);
|
||||
string formattedUrl = FormatStr(Url, title, notificationText);
|
||||
string formattedBody = FormatStr(Body, title, notificationText);
|
||||
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);
|
||||
foreach (var (key, value) in formattedHeaders)
|
||||
request.Headers.Add(key, value);
|
||||
request.Content = new StringContent(formattedBody);
|
||||
request.Content.Headers.ContentType = new ("application/json");
|
||||
Log.Debug($"Request: {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)
|
||||
{
|
||||
return this;
|
||||
}
|
||||
StringBuilder sb = new (str);
|
||||
sb.Replace("%title", title);
|
||||
sb.Replace("%text", text);
|
||||
|
||||
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("%text", text);
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
public override string ToString() => $"{GetType().Name} {Name}";
|
||||
|
243
API/Tranga.cs
243
API/Tranga.cs
@@ -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.MangaContext;
|
||||
using API.Schema.MangaContext.MetadataFetchers;
|
||||
@@ -7,6 +9,7 @@ using API.Workers;
|
||||
using API.Workers.MaintenanceWorkers;
|
||||
using log4net;
|
||||
using log4net.Config;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace API;
|
||||
|
||||
@@ -22,9 +25,11 @@ public static class Tranga
|
||||
" |___| |__| |___._||__|__||___ ||___._|\n" +
|
||||
" |_____| \n\n";
|
||||
|
||||
public static Thread PeriodicWorkerStarterThread { get; } = new (WorkerStarter);
|
||||
private static IServiceProvider? ServiceProvider;
|
||||
|
||||
private static readonly ILog Log = LogManager.GetLogger(typeof(Tranga));
|
||||
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 readonly UpdateMetadataWorker UpdateMetadataWorker = new ();
|
||||
@@ -34,13 +39,17 @@ public static class Tranga
|
||||
internal static readonly CleanupMangaCoversWorker CleanupMangaCoversWorker = new();
|
||||
internal static readonly StartNewChapterDownloadsWorker StartNewChapterDownloadsWorker = new();
|
||||
internal static readonly RemoveOldNotificationsWorker RemoveOldNotificationsWorker = new();
|
||||
internal static readonly UpdateCoversWorker UpdateCoversWorker = new();
|
||||
|
||||
internal static void StartLogger()
|
||||
{
|
||||
BasicConfigurator.Configure();
|
||||
Log.Info("Logger Configured.");
|
||||
Log.Info(TRANGA);
|
||||
}
|
||||
|
||||
internal static void AddDefaultWorkers()
|
||||
{
|
||||
AddWorker(UpdateMetadataWorker);
|
||||
AddWorker(SendNotificationsWorker);
|
||||
AddWorker(UpdateChaptersDownloadedWorker);
|
||||
@@ -48,138 +57,153 @@ public static class Tranga
|
||||
AddWorker(CleanupMangaCoversWorker);
|
||||
AddWorker(StartNewChapterDownloadsWorker);
|
||||
AddWorker(RemoveOldNotificationsWorker);
|
||||
AddWorker(UpdateCoversWorker);
|
||||
}
|
||||
|
||||
internal static HashSet<BaseWorker> AllWorkers { get; private set; } = new ();
|
||||
public static void AddWorker(BaseWorker worker) => AllWorkers.Add(worker);
|
||||
internal static void SetServiceProvider(IServiceProvider serviceProvider)
|
||||
{
|
||||
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)
|
||||
{
|
||||
foreach (BaseWorker baseWorker in workers)
|
||||
{
|
||||
AddWorker(baseWorker);
|
||||
}
|
||||
}
|
||||
|
||||
public static void RemoveWorker(BaseWorker removeWorker)
|
||||
{
|
||||
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();
|
||||
private static readonly ConcurrentDictionary<BaseWorker, Task<BaseWorker[]>> RunningWorkers = new();
|
||||
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.");
|
||||
if (serviceProviderObj is null)
|
||||
Log.Debug($"Starting {worker}");
|
||||
if (ServiceProvider is null)
|
||||
{
|
||||
Log.Error("serviceProviderObj is null");
|
||||
Log.Fatal("ServiceProvider is null");
|
||||
return;
|
||||
}
|
||||
IServiceProvider serviceProvider = (IServiceProvider)serviceProviderObj;
|
||||
Action afterWorkCallback = AfterWork(worker, callback);
|
||||
|
||||
while (true)
|
||||
if (worker is BaseWorkerWithContext<MangaContext> mangaContextWorker)
|
||||
{
|
||||
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)
|
||||
{
|
||||
mangaContextWorker.SetScope(serviceProvider.CreateScope());
|
||||
RunningWorkers.Add(mangaContextWorker, mangaContextWorker.DoWork());
|
||||
}else if (worker is BaseWorkerWithContext<NotificationsContext> notificationContextWorker)
|
||||
{
|
||||
notificationContextWorker.SetScope(serviceProvider.CreateScope());
|
||||
RunningWorkers.Add(notificationContextWorker, notificationContextWorker.DoWork());
|
||||
}else if (worker is BaseWorkerWithContext<LibraryContext> libraryContextWorker)
|
||||
{
|
||||
libraryContextWorker.SetScope(serviceProvider.CreateScope());
|
||||
RunningWorkers.Add(libraryContextWorker, libraryContextWorker.DoWork());
|
||||
}else
|
||||
RunningWorkers.Add(worker, worker.DoWork());
|
||||
|
||||
StartWorkers.Remove(worker);
|
||||
}
|
||||
Thread.Sleep(Settings.WorkCycleTimeoutMs);
|
||||
}
|
||||
mangaContextWorker.SetScope(ServiceProvider.CreateScope());
|
||||
RunningWorkers.TryAdd(mangaContextWorker, mangaContextWorker.DoWork(afterWorkCallback));
|
||||
}else if (worker is BaseWorkerWithContext<NotificationsContext> notificationContextWorker)
|
||||
{
|
||||
notificationContextWorker.SetScope(ServiceProvider.CreateScope());
|
||||
RunningWorkers.TryAdd(notificationContextWorker, notificationContextWorker.DoWork(afterWorkCallback));
|
||||
}else if (worker is BaseWorkerWithContext<LibraryContext> libraryContextWorker)
|
||||
{
|
||||
libraryContextWorker.SetScope(ServiceProvider.CreateScope());
|
||||
RunningWorkers.TryAdd(libraryContextWorker, libraryContextWorker.DoWork(afterWorkCallback));
|
||||
}else
|
||||
RunningWorkers.TryAdd(worker, worker.DoWork(afterWorkCallback));
|
||||
}
|
||||
|
||||
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();
|
||||
if (done.Length < 1)
|
||||
return;
|
||||
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);
|
||||
Log.Debug($"AfterWork {worker}");
|
||||
RunningWorkers.Remove(worker, out _);
|
||||
callback?.Invoke();
|
||||
};
|
||||
|
||||
internal static void StopWorker(BaseWorker worker)
|
||||
{
|
||||
StartWorkers.Remove(worker);
|
||||
Log.Debug($"Stopping {worker}");
|
||||
if(worker is IPeriodic periodicWorker)
|
||||
PeriodicWorkers.Remove(periodicWorker, out _);
|
||||
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 addManga, MangaConnectorId<Manga> addMcId, MangaContext context, [NotNullWhen(true)]out Manga? manga)
|
||||
{
|
||||
manga = context.Mangas.Find(addManga.Key) ?? addManga;
|
||||
MangaConnectorId<Manga> mcId = context.MangaConnectorToManga.Find(addMcId.Key) ?? addMcId;
|
||||
mcId.Obj = manga;
|
||||
|
||||
IEnumerable<MangaTag> mergedTags = manga.MangaTags.Select(mt =>
|
||||
context.ChangeTracker.Clear();
|
||||
manga = context.FindMangaLike(addManga);
|
||||
if (manga is not null)
|
||||
{
|
||||
MangaTag? inDb = context.Tags.Find(mt.Tag);
|
||||
return inDb ?? mt;
|
||||
});
|
||||
manga.MangaTags = mergedTags.ToList();
|
||||
|
||||
IEnumerable<Author> mergedAuthors = manga.Authors.Select(ma =>
|
||||
foreach (MangaConnectorId<Manga> mcId in addManga.MangaConnectorIds)
|
||||
{
|
||||
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
|
||||
{
|
||||
Author? inDb = context.Authors.Find(ma.Key);
|
||||
return inDb ?? ma;
|
||||
});
|
||||
manga.Authors = mergedAuthors.ToList();
|
||||
manga = addManga;
|
||||
IEnumerable<MangaTag> mergedTags = manga.MangaTags.Select(mt =>
|
||||
{
|
||||
MangaTag? inDb = context.Tags.Find(mt.Tag);
|
||||
return inDb ?? mt;
|
||||
});
|
||||
manga.MangaTags = mergedTags.ToList();
|
||||
|
||||
if(context.MangaConnectorToManga.Find(addMcId.Key) is null)
|
||||
context.MangaConnectorToManga.Add(mcId);
|
||||
IEnumerable<Author> mergedAuthors = manga.Authors.Select(ma =>
|
||||
{
|
||||
Author? inDb = context.Authors.Find(ma.Key);
|
||||
return inDb ?? ma;
|
||||
});
|
||||
manga.Authors = mergedAuthors.ToList();
|
||||
|
||||
context.Mangas.Add(manga);
|
||||
}
|
||||
|
||||
if (context.Sync() is { success: false })
|
||||
return false;
|
||||
|
||||
DownloadCoverFromMangaconnectorWorker downloadCoverWorker = new (addMcId);
|
||||
AddWorker(downloadCoverWorker);
|
||||
|
||||
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)
|
||||
{
|
||||
chapter = context.Chapters.Find(addChapter.Key) ?? addChapter;
|
||||
MangaConnectorId<Chapter> chId = context.MangaConnectorToChapter.Find(addChId.Key) ?? addChId;
|
||||
chId.Obj = chapter;
|
||||
|
||||
if(context.MangaConnectorToChapter.Find(chId.Key) is null)
|
||||
context.MangaConnectorToChapter.Add(chId);
|
||||
chapter = context.Chapters.Where(ch => ch.Key == addChapter.Key)
|
||||
.Include(ch => ch.ParentManga)
|
||||
.Include(ch => ch.MangaConnectorIds)
|
||||
.FirstOrDefault();
|
||||
if (chapter is not null)
|
||||
{
|
||||
chapter.MangaConnectorIds = chapter.MangaConnectorIds.UnionBy(addChapter.MangaConnectorIds, id => id.Key).ToList();
|
||||
}
|
||||
else
|
||||
{
|
||||
context.Chapters.Add(addChapter);
|
||||
chapter = addChapter;
|
||||
}
|
||||
|
||||
if (context.Sync() is { success: false })
|
||||
return false;
|
||||
|
@@ -59,7 +59,7 @@ public struct TrangaSettings()
|
||||
|
||||
public void Save()
|
||||
{
|
||||
File.WriteAllText(settingsFilePath, JsonConvert.SerializeObject(this));
|
||||
File.WriteAllText(settingsFilePath, JsonConvert.SerializeObject(this, Formatting.Indented));
|
||||
}
|
||||
|
||||
public void SetUserAgent(string value)
|
||||
|
@@ -23,7 +23,7 @@ public abstract class BaseWorker : Identifiable
|
||||
public IEnumerable<BaseWorker> MissingDependencies => DependsOn.Where(d => d.State < WorkerExecutionState.Completed);
|
||||
public bool AllDependenciesFulfilled => DependsOn.All(d => d.State >= WorkerExecutionState.Completed);
|
||||
internal WorkerExecutionState State { get; private set; }
|
||||
private static readonly CancellationTokenSource CancellationTokenSource = new(TimeSpan.FromMinutes(10));
|
||||
private CancellationTokenSource? CancellationTokenSource = null;
|
||||
protected ILog Log { get; init; }
|
||||
|
||||
/// <summary>
|
||||
@@ -33,7 +33,7 @@ public abstract class BaseWorker : Identifiable
|
||||
{
|
||||
Log.Debug($"Cancelled {this}");
|
||||
this.State = WorkerExecutionState.Cancelled;
|
||||
CancellationTokenSource.Cancel();
|
||||
CancellationTokenSource?.Cancel();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -43,7 +43,7 @@ public abstract class BaseWorker : Identifiable
|
||||
{
|
||||
Log.Debug($"Failed {this}");
|
||||
this.State = WorkerExecutionState.Failed;
|
||||
CancellationTokenSource.Cancel();
|
||||
CancellationTokenSource?.Cancel();
|
||||
}
|
||||
|
||||
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>
|
||||
/// </list>
|
||||
/// </returns>
|
||||
public Task<BaseWorker[]> DoWork()
|
||||
public Task<BaseWorker[]> DoWork(Action? callback = null)
|
||||
{
|
||||
// Start the worker
|
||||
Log.Debug($"Checking {this}");
|
||||
this.CancellationTokenSource = new(TimeSpan.FromMinutes(10));
|
||||
this.State = WorkerExecutionState.Waiting;
|
||||
|
||||
// Wait for dependencies, start them if necessary
|
||||
BaseWorker[] missingDependenciesThatNeedStarting = MissingDependencies.Where(d => d.State < WorkerExecutionState.Waiting).ToArray();
|
||||
if(missingDependenciesThatNeedStarting.Any())
|
||||
return new Task<BaseWorker[]>(() => missingDependenciesThatNeedStarting);
|
||||
@@ -80,28 +83,32 @@ public abstract class BaseWorker : Identifiable
|
||||
if (MissingDependencies.Any())
|
||||
return new Task<BaseWorker[]>(WaitForDependencies);
|
||||
|
||||
// Run the actual work
|
||||
Log.Info($"Running {this}");
|
||||
DateTime startTime = DateTime.UtcNow;
|
||||
Task<BaseWorker[]> task = new (DoWorkInternal, CancellationTokenSource.Token);
|
||||
task.GetAwaiter().OnCompleted(() =>
|
||||
{
|
||||
DateTime endTime = DateTime.UtcNow;
|
||||
Log.Info($"Completed {this}\n\t{endTime.Subtract(startTime).TotalMilliseconds} ms");
|
||||
this.State = WorkerExecutionState.Completed;
|
||||
if(this is IPeriodic periodic)
|
||||
periodic.LastExecution = DateTime.UtcNow;
|
||||
});
|
||||
task.Start();
|
||||
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;
|
||||
Log.Info($"Completed {this}\n\t{endTime.Subtract(startTime).TotalMilliseconds} ms");
|
||||
this.State = WorkerExecutionState.Completed;
|
||||
if(this is IPeriodic periodic)
|
||||
periodic.LastExecution = DateTime.UtcNow;
|
||||
callback?.Invoke();
|
||||
};
|
||||
|
||||
protected abstract BaseWorker[] DoWorkInternal();
|
||||
|
||||
private BaseWorker[] WaitForDependencies()
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
@@ -1,8 +1,9 @@
|
||||
using System.IO.Compression;
|
||||
using System.Runtime.InteropServices;
|
||||
using API.MangaConnectors;
|
||||
using API.MangaDownloadClients;
|
||||
using API.Schema.MangaContext;
|
||||
using API.Schema.MangaContext.MangaConnectors;
|
||||
using Microsoft.EntityFrameworkCore.ChangeTracking;
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.Formats.Jpeg;
|
||||
using SixLabors.ImageSharp.Processing;
|
||||
@@ -17,17 +18,29 @@ public class DownloadChapterFromMangaconnectorWorker(MangaConnectorId<Chapter> c
|
||||
internal readonly string MangaConnectorIdId = chId.Key;
|
||||
protected override BaseWorker[] DoWorkInternal()
|
||||
{
|
||||
if (DbContext.MangaConnectorToChapter.Find(MangaConnectorIdId) is not { } MangaConnectorId)
|
||||
if (DbContext.MangaConnectorToChapter.Find(MangaConnectorIdId) is not { } mangaConnectorId)
|
||||
return []; //TODO Exception?
|
||||
MangaConnector mangaConnector = MangaConnectorId.MangaConnector;
|
||||
Chapter chapter = MangaConnectorId.Obj;
|
||||
if (!Tranga.TryGetMangaConnector(mangaConnectorId.MangaConnectorName, out MangaConnector? mangaConnector))
|
||||
return []; //TODO Exception?
|
||||
|
||||
DbContext.Entry(mangaConnectorId).Navigation(nameof(MangaConnectorId<Chapter>.Obj)).Load();
|
||||
Chapter chapter = mangaConnectorId.Obj;
|
||||
if (chapter.Downloaded)
|
||||
{
|
||||
Log.Info("Chapter was already downloaded.");
|
||||
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)
|
||||
{
|
||||
Log.Info($"No imageUrls for chapter {chapter}");
|
||||
@@ -82,6 +95,9 @@ public class DownloadChapterFromMangaconnectorWorker(MangaConnectorId<Chapter> c
|
||||
CopyCoverFromCacheToDownloadLocation(chapter.ParentManga);
|
||||
|
||||
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());
|
||||
|
||||
Log.Debug($"Packaging images to archive {chapter}");
|
||||
@@ -147,10 +163,17 @@ public class DownloadChapterFromMangaconnectorWorker(MangaConnectorId<Chapter> c
|
||||
}
|
||||
|
||||
//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}");
|
||||
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)
|
||||
{
|
||||
Log.Error($"File {fileInCache} does not exist");
|
||||
|
@@ -1,5 +1,5 @@
|
||||
using API.MangaConnectors;
|
||||
using API.Schema.MangaContext;
|
||||
using API.Schema.MangaContext.MangaConnectors;
|
||||
|
||||
namespace API.Workers;
|
||||
|
||||
@@ -9,12 +9,15 @@ public class DownloadCoverFromMangaconnectorWorker(MangaConnectorId<Manga> mcId,
|
||||
internal readonly string MangaConnectorIdId = mcId.Key;
|
||||
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?
|
||||
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();
|
||||
return [];
|
||||
|
@@ -1,5 +1,5 @@
|
||||
using API.MangaConnectors;
|
||||
using API.Schema.MangaContext;
|
||||
using API.Schema.MangaContext.MangaConnectors;
|
||||
|
||||
namespace API.Workers;
|
||||
|
||||
@@ -9,13 +9,18 @@ public class RetrieveMangaChaptersFromMangaconnectorWorker(MangaConnectorId<Mang
|
||||
internal readonly string MangaConnectorIdId = mcId.Key;
|
||||
protected override BaseWorker[] DoWorkInternal()
|
||||
{
|
||||
if (DbContext.MangaConnectorToManga.Find(MangaConnectorIdId) is not { } MangaConnectorId)
|
||||
if (DbContext.MangaConnectorToManga.Find(MangaConnectorIdId) is not { } mangaConnectorId)
|
||||
return []; //TODO Exception?
|
||||
MangaConnector mangaConnector = MangaConnectorId.MangaConnector;
|
||||
Manga manga = MangaConnectorId.Obj;
|
||||
if (!Tranga.TryGetMangaConnector(mangaConnectorId.MangaConnectorName, out MangaConnector? mangaConnector))
|
||||
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
|
||||
(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;
|
||||
foreach ((Chapter chapter, MangaConnectorId<Chapter> mcId) newChapter in allChapters)
|
||||
|
@@ -13,6 +13,10 @@ public class MoveMangaLibraryWorker(Manga manga, FileLibrary toLibrary, IEnumera
|
||||
return []; //TODO Exception?
|
||||
if (DbContext.FileLibraries.Find(LibraryId) is not { } toLibrary)
|
||||
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);
|
||||
manga.Library = toLibrary;
|
||||
|
||||
|
@@ -1,4 +1,5 @@
|
||||
using API.Schema.MangaContext;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace API.Workers;
|
||||
|
||||
@@ -10,7 +11,9 @@ public class CheckForNewChaptersWorker(TimeSpan? interval = null, IEnumerable<Ba
|
||||
|
||||
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();
|
||||
foreach (MangaConnectorId<Manga> mangaConnectorId in connectorIdsManga)
|
||||
|
@@ -1,4 +1,5 @@
|
||||
using API.Schema.MangaContext;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace API.Workers;
|
||||
|
||||
@@ -10,7 +11,9 @@ public class StartNewChapterDownloadsWorker(TimeSpan? interval = null, IEnumerab
|
||||
public TimeSpan Interval { get; set; } = interval ?? TimeSpan.FromMinutes(1);
|
||||
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();
|
||||
foreach (MangaConnectorId<Chapter> mangaConnectorId in mangaConnectorIds)
|
||||
|
@@ -1,4 +1,6 @@
|
||||
using API.Schema.MangaContext;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace API.Workers;
|
||||
|
||||
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);
|
||||
protected override BaseWorker[] DoWorkInternal()
|
||||
{
|
||||
foreach (Chapter dbContextChapter in DbContext.Chapters)
|
||||
foreach (Chapter dbContextChapter in DbContext.Chapters.Include(c => c.ParentManga))
|
||||
dbContextChapter.Downloaded = dbContextChapter.CheckDownloaded();
|
||||
|
||||
DbContext.Sync();
|
||||
|
19
API/Workers/PeriodicWorkers/UpdateCoversWorker.cs
Normal file
19
API/Workers/PeriodicWorkers/UpdateCoversWorker.cs
Normal 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();
|
||||
}
|
||||
}
|
@@ -8,7 +8,8 @@ ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium
|
||||
ENV XDG_CONFIG_HOME=/tmp/.chromium
|
||||
ENV XDG_CACHE_HOME=/tmp/.chromium
|
||||
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 autoclean -y
|
||||
|
||||
|
Reference in New Issue
Block a user