mirror of
https://github.com/C9Glax/tranga.git
synced 2025-07-07 03:14:17 +02:00
Compare commits
157 Commits
4b4e24c6a0
...
JobQueue-S
Author | SHA1 | Date | |
---|---|---|---|
6b4317834d | |||
88fef8417c | |||
eb9fc08b2d | |||
9743bb6e8e | |||
e8d612557f | |||
cf2dbeaf6a | |||
84940c414c | |||
ea627081b8 | |||
a90a6fb200 | |||
c3a0bb03e9 | |||
f8ccd2d69e | |||
ad224190a2 | |||
f05f2cc8e0 | |||
d6f0630a99 | |||
0ac4c23ac9 | |||
d6847d769e | |||
f6f5e21151 | |||
da3b5078af | |||
681d56710a | |||
6f5823596a | |||
8a06ed648c | |||
4dcd6ee035 | |||
e327e93163 | |||
6cd836540a | |||
91c91e4989 | |||
57bb87120a | |||
07880fedb5 | |||
f1d3203ae1 | |||
314700aa8e | |||
b299cc9109 | |||
7e9ba7090a | |||
ea73d03b8f | |||
e9d9bebcd7 | |||
e5937d2654 | |||
7c9e0eddf9 | |||
ae0c6c8240 | |||
e063cf1fd9 | |||
2eb0d941b2 | |||
8170e1d762 | |||
254383b006 | |||
df431e533a | |||
9a4cc0cbaf | |||
861cf7e166 | |||
7e34b3b91e | |||
29d36484f9 | |||
2c6e8e4d16 | |||
fab2886684 | |||
d9ccf71b21 | |||
f36f34f212 | |||
ff10432c79 | |||
776e1e4890 | |||
db0643fa19 | |||
3eeb563ca1 | |||
7a88b1f7ee | |||
b5411e9c6c | |||
07b260dea6 | |||
71ad32de31 | |||
ecd2c2722f | |||
ff1e467ada | |||
24f68b4a8e | |||
e51e90aabc | |||
dc2c27f4bd | |||
406d8eef51 | |||
1fba599c79 | |||
a668a16035 | |||
f89b8e1977 | |||
11290062c0 | |||
f46910fac6 | |||
f974c5ddd1 | |||
a01963a125 | |||
8a877ee465 | |||
c370e656f1 | |||
58ed976737 | |||
1b6af73a0c | |||
70fe23857b | |||
0027af2d36 | |||
1a8f70f501 | |||
aa67c11050 | |||
7b38d0aa2b | |||
64e31fad54 | |||
49a70e2341 | |||
9659f2a68a | |||
d474868116 | |||
b1312c4164 | |||
33856f9927 | |||
02ab3d8cae | |||
7974c58fd5 | |||
503d9dfb5f | |||
62b035f6c5 | |||
40c70fbf19 | |||
49bd66ccab | |||
9b251169a5 | |||
aa29c45094 | |||
bd60fda05a | |||
8ecbdb91b2 | |||
cb1c68f295 | |||
421a25ec31 | |||
2d122a918f | |||
100cb06ba0 | |||
6125b036bf | |||
3fe3fc09b0 | |||
96d5b09391 | |||
84aecda916 | |||
0803a92a66 | |||
7f55aaf85d | |||
3853e2daa2 | |||
852fbf5ae8 | |||
4e7a725fee | |||
698d138642 | |||
8efb60652b | |||
fe60b98cb8 | |||
63442e9af6 | |||
703e32a30e | |||
4ddfe4a54c | |||
fb2b4d6920 | |||
496b49ccb3 | |||
b3efcf19d9 | |||
0903ec606b | |||
6cfa29e3dd | |||
0519ed26de | |||
aacdb72d6a | |||
3283dd7290 | |||
937c5cb7a7 | |||
225b7f02ad | |||
6258e07f20 | |||
622198a09e | |||
49b382fe1f | |||
5a6dc5a5b2 | |||
4bc70eca68 | |||
63fee081e6 | |||
e45b72dcf9 | |||
021ad5e804 | |||
8e0c964883 | |||
d6e945741a | |||
3a3306240f | |||
110dd36166 | |||
065cac62af | |||
563afa1e6f | |||
be2adff57d | |||
adc7ee606e | |||
a764f381c9 | |||
590ccdd09a | |||
0f0a49f74f | |||
a1a5028858 | |||
1792952039 | |||
9e62eb53cb | |||
f3c4b012b0 | |||
7e1c65b470 | |||
4247ae7740 | |||
a5954ed5c8 | |||
d08544b892 | |||
f6f86deb7f | |||
16f5817a31 | |||
d5d9f44a5f | |||
83bc3b418b | |||
205f0a1629 | |||
a1c2942208 |
@ -12,6 +12,7 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Asp.Versioning.Mvc.ApiExplorer" Version="8.1.0" />
|
||||
<PackageReference Include="HtmlAgilityPack" Version="1.12.0" />
|
||||
<PackageReference Include="JikanDotNet" Version="2.9.1" />
|
||||
<PackageReference Include="log4net" Version="3.0.4" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="9.0.3" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.3" />
|
||||
@ -19,6 +20,7 @@
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Proxies" Version="9.0.5" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageReference Include="Npgsql" Version="9.0.3" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4" />
|
||||
@ -31,8 +33,4 @@
|
||||
<PackageReference Include="System.Drawing.Common" Version="9.0.3" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Migrations\" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
@ -1,14 +1,14 @@
|
||||
namespace API.APIEndpointRecords;
|
||||
|
||||
public record GotifyRecord(string endpoint, string appToken, int priority)
|
||||
public record GotifyRecord(string Name, string Endpoint, string AppToken, int Priority)
|
||||
{
|
||||
public bool Validate()
|
||||
{
|
||||
if (endpoint == string.Empty)
|
||||
if (Endpoint == string.Empty)
|
||||
return false;
|
||||
if (appToken == string.Empty)
|
||||
if (AppToken == string.Empty)
|
||||
return false;
|
||||
if (priority < 0 || priority > 10)
|
||||
if (Priority < 0 || Priority > 10)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
|
@ -1,16 +0,0 @@
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace API.APIEndpointRecords;
|
||||
|
||||
public record LunaseaRecord(string id)
|
||||
{
|
||||
private static Regex validateRex = new(@"(?:device|user)\/[0-9a-zA-Z\-]+");
|
||||
public bool Validate()
|
||||
{
|
||||
if (id == string.Empty)
|
||||
return false;
|
||||
if (!validateRex.IsMatch(id))
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
namespace API.APIEndpointRecords;
|
||||
|
||||
public record ModifyJobRecord(ulong? RecurrenceMs, bool? Enabled);
|
3
API/APIEndpointRecords/ModifyWorkerRecord.cs
Normal file
3
API/APIEndpointRecords/ModifyWorkerRecord.cs
Normal file
@ -0,0 +1,3 @@
|
||||
namespace API.APIEndpointRecords;
|
||||
|
||||
public record ModifyWorkerRecord(ulong? IntervalMs);
|
@ -1,13 +0,0 @@
|
||||
namespace API.APIEndpointRecords;
|
||||
|
||||
public record NewLibraryRecord(string path, string name)
|
||||
{
|
||||
public bool Validate()
|
||||
{
|
||||
if (path.Length < 1) //TODO Better Path validation
|
||||
return false;
|
||||
if (name.Length < 1)
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
}
|
@ -1,16 +1,16 @@
|
||||
namespace API.APIEndpointRecords;
|
||||
|
||||
public record NtfyRecord(string endpoint, string username, string password, string topic, int priority)
|
||||
public record NtfyRecord(string Name, string Endpoint, string Username, string Password, string Topic, int Priority)
|
||||
{
|
||||
public bool Validate()
|
||||
{
|
||||
if (endpoint == string.Empty)
|
||||
if (Endpoint == string.Empty)
|
||||
return false;
|
||||
if (username == string.Empty)
|
||||
if (Username == string.Empty)
|
||||
return false;
|
||||
if (password == string.Empty)
|
||||
if (Password == string.Empty)
|
||||
return false;
|
||||
if (priority < 1 || priority > 5)
|
||||
if (Priority < 1 || Priority > 5)
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
@ -1,12 +1,12 @@
|
||||
namespace API.APIEndpointRecords;
|
||||
|
||||
public record PushoverRecord(string apptoken, string user)
|
||||
public record PushoverRecord(string Name, string AppToken, string User)
|
||||
{
|
||||
public bool Validate()
|
||||
{
|
||||
if (apptoken == string.Empty)
|
||||
if (AppToken == string.Empty)
|
||||
return false;
|
||||
if (user == string.Empty)
|
||||
if (User == string.Empty)
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
134
API/Controllers/FileLibraryController.cs
Normal file
134
API/Controllers/FileLibraryController.cs
Normal file
@ -0,0 +1,134 @@
|
||||
using API.Schema.MangaContext;
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using static Microsoft.AspNetCore.Http.StatusCodes;
|
||||
// ReSharper disable InconsistentNaming
|
||||
|
||||
namespace API.Controllers;
|
||||
|
||||
[ApiVersion(2)]
|
||||
[ApiController]
|
||||
[Route("v{v:apiVersion}/[controller]")]
|
||||
public class FileLibraryController(MangaContext context) : Controller
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns all <see cref="FileLibrary"/>
|
||||
/// </summary>
|
||||
/// <response code="200"></response>
|
||||
[HttpGet]
|
||||
[ProducesResponseType<FileLibrary[]>(Status200OK, "application/json")]
|
||||
public IActionResult GetFileLibraries()
|
||||
{
|
||||
return Ok(context.FileLibraries.ToArray());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns <see cref="FileLibrary"/> with <paramref name="FileLibraryId"/>
|
||||
/// </summary>
|
||||
/// <param name="FileLibraryId"><see cref="FileLibrary"/>.Key</param>
|
||||
/// <response code="200"></response>
|
||||
/// <response code="404"><see cref="FileLibrary"/> with <paramref name="FileLibraryId"/> not found.</response>
|
||||
[HttpGet("{FileLibraryId}")]
|
||||
[ProducesResponseType<FileLibrary>(Status200OK, "application/json")]
|
||||
[ProducesResponseType(Status404NotFound)]
|
||||
public IActionResult GetFileLibrary(string FileLibraryId)
|
||||
{
|
||||
if (context.FileLibraries.Find(FileLibraryId) is not { } library)
|
||||
return NotFound();
|
||||
|
||||
return Ok(library);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Changes the <see cref="FileLibraryId"/>.BasePath with <paramref name="FileLibraryId"/>
|
||||
/// </summary>
|
||||
/// <param name="FileLibraryId"><see cref="FileLibrary"/>.Key</param>
|
||||
/// <param name="newBasePath">New <see cref="FileLibraryId"/>.BasePath</param>
|
||||
/// <response code="200"></response>
|
||||
/// <response code="404"><see cref="FileLibrary"/> with <paramref name="FileLibraryId"/> not found.</response>
|
||||
/// <response code="500">Error during Database Operation</response>
|
||||
[HttpPatch("{FileLibraryId}/ChangeBasePath")]
|
||||
[ProducesResponseType(Status200OK)]
|
||||
[ProducesResponseType(Status404NotFound)]
|
||||
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
|
||||
public IActionResult ChangeLibraryBasePath(string FileLibraryId, [FromBody]string newBasePath)
|
||||
{
|
||||
if (context.FileLibraries.Find(FileLibraryId) is not { } library)
|
||||
return NotFound();
|
||||
|
||||
//TODO Path check
|
||||
library.BasePath = newBasePath;
|
||||
|
||||
if(context.Sync() is { success: false } result)
|
||||
return StatusCode(Status500InternalServerError, result.exceptionMessage);
|
||||
return Ok();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Changes the <see cref="FileLibraryId"/>.LibraryName with <paramref name="FileLibraryId"/>
|
||||
/// </summary>
|
||||
/// <param name="FileLibraryId"><see cref="FileLibrary"/>.Key</param>
|
||||
/// <param name="newName">New <see cref="FileLibraryId"/>.LibraryName</param>
|
||||
/// <response code="200"></response>
|
||||
/// <response code="404"><see cref="FileLibrary"/> with <paramref name="FileLibraryId"/> not found.</response>
|
||||
/// <response code="500">Error during Database Operation</response>
|
||||
[HttpPatch("{FileLibraryId}/ChangeName")]
|
||||
[ProducesResponseType(Status200OK)]
|
||||
[ProducesResponseType(Status404NotFound)]
|
||||
[ProducesResponseType(Status400BadRequest)]
|
||||
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
|
||||
public IActionResult ChangeLibraryName(string FileLibraryId, [FromBody] string newName)
|
||||
{
|
||||
if (context.FileLibraries.Find(FileLibraryId) is not { } library)
|
||||
return NotFound();
|
||||
|
||||
//TODO Name check
|
||||
library.LibraryName = newName;
|
||||
|
||||
if(context.Sync() is { success: false } result)
|
||||
return StatusCode(Status500InternalServerError, result.exceptionMessage);
|
||||
return Ok();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates new <see cref="FileLibraryId"/>
|
||||
/// </summary>
|
||||
/// <param name="library">New <see cref="FileLibrary"/> to add</param>
|
||||
/// <response code="200"></response>
|
||||
/// <response code="500">Error during Database Operation</response>
|
||||
[HttpPut]
|
||||
[ProducesResponseType(Status201Created)]
|
||||
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
|
||||
public IActionResult CreateNewLibrary([FromBody]FileLibrary library)
|
||||
{
|
||||
|
||||
//TODO Parameter check
|
||||
context.FileLibraries.Add(library);
|
||||
|
||||
if(context.Sync() is { success: false } result)
|
||||
return StatusCode(Status500InternalServerError, result.exceptionMessage);
|
||||
return Created();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes the <see cref="FileLibraryId"/>.LibraryName with <paramref name="FileLibraryId"/>
|
||||
/// </summary>
|
||||
/// <param name="FileLibraryId"><see cref="FileLibrary"/>.Key</param>
|
||||
/// <response code="200"></response>
|
||||
/// <response code="500">Error during Database Operation</response>
|
||||
[HttpDelete("{FileLibraryId}")]
|
||||
[ProducesResponseType(Status200OK)]
|
||||
[ProducesResponseType(Status404NotFound)]
|
||||
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
|
||||
public IActionResult DeleteLocalLibrary(string FileLibraryId)
|
||||
{
|
||||
if (context.FileLibraries.Find(FileLibraryId) is not { } library)
|
||||
return NotFound();
|
||||
|
||||
context.FileLibraries.Remove(library);
|
||||
|
||||
if(context.Sync() is { success: false } result)
|
||||
return StatusCode(Status500InternalServerError, result.exceptionMessage);
|
||||
return Ok();
|
||||
}
|
||||
}
|
@ -1,364 +0,0 @@
|
||||
using API.APIEndpointRecords;
|
||||
using API.Schema;
|
||||
using API.Schema.Contexts;
|
||||
using API.Schema.Jobs;
|
||||
using Asp.Versioning;
|
||||
using log4net;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using static Microsoft.AspNetCore.Http.StatusCodes;
|
||||
// ReSharper disable InconsistentNaming
|
||||
|
||||
namespace API.Controllers;
|
||||
|
||||
[ApiVersion(2)]
|
||||
[ApiController]
|
||||
[Route("v{version:apiVersion}/[controller]")]
|
||||
public class JobController(PgsqlContext context, ILog Log) : Controller
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns all Jobs
|
||||
/// </summary>
|
||||
/// <response code="200"></response>
|
||||
[HttpGet]
|
||||
[ProducesResponseType<Job[]>(Status200OK, "application/json")]
|
||||
public IActionResult GetAllJobs()
|
||||
{
|
||||
Job[] ret = context.Jobs.ToArray();
|
||||
return Ok(ret);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns Jobs with requested Job-IDs
|
||||
/// </summary>
|
||||
/// <param name="ids">Array of Job-IDs</param>
|
||||
/// <response code="200"></response>
|
||||
[HttpPost("WithIDs")]
|
||||
[ProducesResponseType<Job[]>(Status200OK, "application/json")]
|
||||
public IActionResult GetJobs([FromBody]string[] ids)
|
||||
{
|
||||
Job[] ret = context.Jobs.Where(job => ids.Contains(job.JobId)).ToArray();
|
||||
return Ok(ret);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get all Jobs in requested State
|
||||
/// </summary>
|
||||
/// <param name="JobState">Requested Job-State</param>
|
||||
/// <response code="200"></response>
|
||||
[HttpGet("State/{JobState}")]
|
||||
[ProducesResponseType<Job[]>(Status200OK, "application/json")]
|
||||
public IActionResult GetJobsInState(JobState JobState)
|
||||
{
|
||||
Job[] jobsInState = context.Jobs.Where(job => job.state == JobState).ToArray();
|
||||
return Ok(jobsInState);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns all Jobs of requested Type
|
||||
/// </summary>
|
||||
/// <param name="JobType">Requested Job-Type</param>
|
||||
/// <response code="200"></response>
|
||||
[HttpGet("Type/{JobType}")]
|
||||
[ProducesResponseType<Job[]>(Status200OK, "application/json")]
|
||||
public IActionResult GetJobsOfType(JobType JobType)
|
||||
{
|
||||
Job[] jobsOfType = context.Jobs.Where(job => job.JobType == JobType).ToArray();
|
||||
return Ok(jobsOfType);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns all Jobs of requested Type and State
|
||||
/// </summary>
|
||||
/// <param name="JobType">Requested Job-Type</param>
|
||||
/// <param name="JobState">Requested Job-State</param>
|
||||
/// <response code="200"></response>
|
||||
[HttpGet("TypeAndState/{JobType}/{JobState}")]
|
||||
[ProducesResponseType<Job[]>(Status200OK, "application/json")]
|
||||
public IActionResult GetJobsOfType(JobType JobType, JobState JobState)
|
||||
{
|
||||
Job[] jobsOfType = context.Jobs.Where(job => job.JobType == JobType && job.state == JobState).ToArray();
|
||||
return Ok(jobsOfType);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Return Job with ID
|
||||
/// </summary>
|
||||
/// <param name="JobId">Job-ID</param>
|
||||
/// <response code="200"></response>
|
||||
/// <response code="404">Job with ID could not be found</response>
|
||||
[HttpGet("{JobId}")]
|
||||
[ProducesResponseType<Job>(Status200OK, "application/json")]
|
||||
[ProducesResponseType(Status404NotFound)]
|
||||
public IActionResult GetJob(string JobId)
|
||||
{
|
||||
Job? ret = context.Jobs.Find(JobId);
|
||||
return (ret is not null) switch
|
||||
{
|
||||
true => Ok(ret),
|
||||
false => NotFound()
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new DownloadAvailableChaptersJob
|
||||
/// </summary>
|
||||
/// <param name="MangaId">ID of Manga</param>
|
||||
/// <param name="record">Job-Configuration</param>
|
||||
/// <response code="201">Job-IDs</response>
|
||||
/// <response code="400">Could not find ToLibrary with ID</response>
|
||||
/// <response code="404">Could not find Manga with ID</response>
|
||||
/// <response code="500">Error during Database Operation</response>
|
||||
[HttpPut("DownloadAvailableChaptersJob/{MangaId}")]
|
||||
[ProducesResponseType<string[]>(Status201Created, "application/json")]
|
||||
[ProducesResponseType(Status400BadRequest)]
|
||||
[ProducesResponseType(Status404NotFound)]
|
||||
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
|
||||
public IActionResult CreateDownloadAvailableChaptersJob(string MangaId, [FromBody]DownloadAvailableChaptersJobRecord record)
|
||||
{
|
||||
if (context.Mangas.Find(MangaId) is not { } m)
|
||||
return NotFound();
|
||||
else
|
||||
{
|
||||
try
|
||||
{
|
||||
LocalLibrary? l = context.LocalLibraries.Find(record.localLibraryId);
|
||||
if (l is null)
|
||||
return BadRequest();
|
||||
m.Library = l;
|
||||
context.SaveChanges();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e);
|
||||
return StatusCode(500, e.Message);
|
||||
}
|
||||
}
|
||||
Job retrieveChapters = new RetrieveChaptersJob(m, record.language, record.recurrenceTimeMs);
|
||||
Job downloadChapters = new DownloadAvailableChaptersJob(m, record.recurrenceTimeMs, dependsOnJobs: [retrieveChapters]);
|
||||
return AddJobs([retrieveChapters, downloadChapters]);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new DownloadSingleChapterJob
|
||||
/// </summary>
|
||||
/// <param name="ChapterId">ID of the Chapter</param>
|
||||
/// <response code="201">Job-IDs</response>
|
||||
/// <response code="404">Could not find Chapter with ID</response>
|
||||
/// <response code="500">Error during Database Operation</response>
|
||||
[HttpPut("DownloadSingleChapterJob/{ChapterId}")]
|
||||
[ProducesResponseType<string[]>(Status201Created, "application/json")]
|
||||
[ProducesResponseType(Status404NotFound)]
|
||||
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
|
||||
public IActionResult CreateNewDownloadChapterJob(string ChapterId)
|
||||
{
|
||||
if(context.Chapters.Find(ChapterId) is not { } c)
|
||||
return NotFound();
|
||||
Job job = new DownloadSingleChapterJob(c);
|
||||
return AddJobs([job]);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new UpdateFilesDownloadedJob
|
||||
/// </summary>
|
||||
/// <param name="MangaId">ID of the Manga</param>
|
||||
/// <response code="201">Job-IDs</response>
|
||||
/// <response code="201">Could not find Manga with ID</response>
|
||||
/// <response code="500">Error during Database Operation</response>
|
||||
[HttpPut("UpdateFilesJob/{MangaId}")]
|
||||
[ProducesResponseType<string[]>(Status201Created, "application/json")]
|
||||
[ProducesResponseType(Status404NotFound)]
|
||||
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
|
||||
public IActionResult CreateUpdateFilesDownloadedJob(string MangaId)
|
||||
{
|
||||
if(context.Mangas.Find(MangaId) is not { } m)
|
||||
return NotFound();
|
||||
Job job = new UpdateFilesDownloadedJob(m, 0);
|
||||
return AddJobs([job]);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new UpdateMetadataJob for all Manga
|
||||
/// </summary>
|
||||
/// <response code="201">Job-IDs</response>
|
||||
/// <response code="500">Error during Database Operation</response>
|
||||
[HttpPut("UpdateAllFilesJob")]
|
||||
[ProducesResponseType<string[]>(Status201Created, "application/json")]
|
||||
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
|
||||
public IActionResult CreateUpdateAllFilesDownloadedJob()
|
||||
{
|
||||
List<UpdateFilesDownloadedJob> jobs = context.Mangas.Select(m => new UpdateFilesDownloadedJob(m, 0, null, null)).ToList();
|
||||
try
|
||||
{
|
||||
context.Jobs.AddRange(jobs);
|
||||
context.SaveChanges();
|
||||
return Created();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e);
|
||||
return StatusCode(500, e.Message);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Not Implemented: Create a new UpdateMetadataJob
|
||||
/// </summary>
|
||||
/// <param name="MangaId">ID of the Manga</param>
|
||||
/// <response code="201">Job-IDs</response>
|
||||
/// <response code="404">Could not find Manga with ID</response>
|
||||
/// <response code="500">Error during Database Operation</response>
|
||||
[HttpPut("UpdateMetadataJob/{MangaId}")]
|
||||
[ProducesResponseType<string[]>(Status201Created, "application/json")]
|
||||
[ProducesResponseType(Status404NotFound)]
|
||||
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
|
||||
public IActionResult CreateUpdateMetadataJob(string MangaId)
|
||||
{
|
||||
return StatusCode(Status501NotImplemented);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Not Implemented: Create a new UpdateMetadataJob for all Manga
|
||||
/// </summary>
|
||||
/// <response code="201">Job-IDs</response>
|
||||
/// <response code="500">Error during Database Operation</response>
|
||||
[HttpPut("UpdateAllMetadataJob")]
|
||||
[ProducesResponseType<string[]>(Status201Created, "application/json")]
|
||||
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
|
||||
public IActionResult CreateUpdateAllMetadataJob()
|
||||
{
|
||||
return StatusCode(Status501NotImplemented);
|
||||
}
|
||||
|
||||
private IActionResult AddJobs(Job[] jobs)
|
||||
{
|
||||
try
|
||||
{
|
||||
context.Jobs.AddRange(jobs);
|
||||
context.SaveChanges();
|
||||
return new CreatedResult((string?)null, jobs.Select(j => j.JobId).ToArray());
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e);
|
||||
return StatusCode(500, e.Message);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Delete Job with ID and all children
|
||||
/// </summary>
|
||||
/// <param name="JobId">Job-ID</param>
|
||||
/// <response code="200"></response>
|
||||
/// <response code="404">Job could not be found</response>
|
||||
/// <response code="500">Error during Database Operation</response>
|
||||
[HttpDelete("{JobId}")]
|
||||
[ProducesResponseType(Status200OK)]
|
||||
[ProducesResponseType(Status404NotFound)]
|
||||
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
|
||||
public IActionResult DeleteJob(string JobId)
|
||||
{
|
||||
try
|
||||
{
|
||||
if(context.Jobs.Find(JobId) is not { } ret)
|
||||
return NotFound();
|
||||
|
||||
context.Remove(ret);
|
||||
context.SaveChanges();
|
||||
return Ok();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e);
|
||||
return StatusCode(500, e.Message);
|
||||
}
|
||||
}
|
||||
|
||||
private IQueryable<Job> GetChildJobs(string parentJobId)
|
||||
{
|
||||
IQueryable<Job> children = context.Jobs.Where(j => j.ParentJobId == parentJobId);
|
||||
foreach (Job child in children)
|
||||
foreach (Job grandChild in GetChildJobs(child.JobId))
|
||||
children.Append(grandChild);
|
||||
return children;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Modify Job with ID
|
||||
/// </summary>
|
||||
/// <param name="JobId">Job-ID</param>
|
||||
/// <param name="modifyJobRecord">Fields to modify, set to null to keep previous value</param>
|
||||
/// <response code="202">Job modified</response>
|
||||
/// <response code="400">Malformed request</response>
|
||||
/// <response code="404">Job with ID not found</response>
|
||||
/// <response code="500">Error during Database Operation</response>
|
||||
[HttpPatch("{JobId}")]
|
||||
[ProducesResponseType<Job>(Status202Accepted, "application/json")]
|
||||
[ProducesResponseType(Status400BadRequest)]
|
||||
[ProducesResponseType(Status404NotFound)]
|
||||
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
|
||||
public IActionResult ModifyJob(string JobId, [FromBody]ModifyJobRecord modifyJobRecord)
|
||||
{
|
||||
try
|
||||
{
|
||||
Job? ret = context.Jobs.Find(JobId);
|
||||
if(ret is null)
|
||||
return NotFound();
|
||||
|
||||
ret.RecurrenceMs = modifyJobRecord.RecurrenceMs ?? ret.RecurrenceMs;
|
||||
ret.Enabled = modifyJobRecord.Enabled ?? ret.Enabled;
|
||||
|
||||
context.SaveChanges();
|
||||
return new AcceptedResult(ret.JobId, ret);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e);
|
||||
return StatusCode(500, e.Message);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Starts the Job with the requested ID
|
||||
/// </summary>
|
||||
/// <param name="JobId">Job-ID</param>
|
||||
/// <response code="202">Job started</response>
|
||||
/// <response code="404">Job with ID not found</response>
|
||||
/// <response code="409">Job was already running</response>
|
||||
/// <response code="500">Error during Database Operation</response>
|
||||
[HttpPost("{JobId}/Start")]
|
||||
[ProducesResponseType(Status202Accepted)]
|
||||
[ProducesResponseType(Status404NotFound)]
|
||||
[ProducesResponseType(Status409Conflict)]
|
||||
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
|
||||
public IActionResult StartJob(string JobId)
|
||||
{
|
||||
Job? ret = context.Jobs.Find(JobId);
|
||||
if (ret is null)
|
||||
return NotFound();
|
||||
try
|
||||
{
|
||||
if (ret.state >= JobState.Running && ret.state < JobState.Completed)
|
||||
return new ConflictResult();
|
||||
ret.LastExecution = DateTime.UnixEpoch;
|
||||
context.SaveChanges();
|
||||
return Accepted();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e);
|
||||
return StatusCode(500, e.Message);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stops the Job with the requested ID
|
||||
/// </summary>
|
||||
/// <param name="JobId">Job-ID</param>
|
||||
/// <remarks><h1>NOT IMPLEMENTED</h1></remarks>
|
||||
[HttpPost("{JobId}/Stop")]
|
||||
[ProducesResponseType(Status501NotImplemented)]
|
||||
public IActionResult StopJob(string JobId)
|
||||
{
|
||||
return StatusCode(Status501NotImplemented);
|
||||
}
|
||||
}
|
@ -1,20 +1,19 @@
|
||||
using API.Schema;
|
||||
using API.Schema.Contexts;
|
||||
using API.Schema.LibraryConnectors;
|
||||
using API.Schema.LibraryContext;
|
||||
using API.Schema.LibraryContext.LibraryConnectors;
|
||||
using Asp.Versioning;
|
||||
using log4net;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using static Microsoft.AspNetCore.Http.StatusCodes;
|
||||
// ReSharper disable InconsistentNaming
|
||||
|
||||
namespace API.Controllers;
|
||||
|
||||
[ApiVersion(2)]
|
||||
[ApiController]
|
||||
[Route("v{v:apiVersion}/[controller]")]
|
||||
public class LibraryConnectorController(LibraryContext context, ILog Log) : Controller
|
||||
public class LibraryConnectorController(LibraryContext context) : Controller
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets all configured ToLibrary-Connectors
|
||||
/// Gets all configured <see cref="LibraryConnector"/>
|
||||
/// </summary>
|
||||
/// <response code="200"></response>
|
||||
[HttpGet]
|
||||
@ -22,32 +21,31 @@ public class LibraryConnectorController(LibraryContext context, ILog Log) : Cont
|
||||
public IActionResult GetAllConnectors()
|
||||
{
|
||||
LibraryConnector[] connectors = context.LibraryConnectors.ToArray();
|
||||
|
||||
return Ok(connectors);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns ToLibrary-Connector with requested ID
|
||||
/// Returns <see cref="LibraryConnector"/> with <paramref name="LibraryConnectorId"/>
|
||||
/// </summary>
|
||||
/// <param name="LibraryControllerId">ToLibrary-Connector-ID</param>
|
||||
/// <param name="LibraryConnectorId"><see cref="LibraryConnector"/>.Key</param>
|
||||
/// <response code="200"></response>
|
||||
/// <response code="404">Connector with ID not found.</response>
|
||||
[HttpGet("{LibraryControllerId}")]
|
||||
/// <response code="404"><see cref="LibraryConnector"/> with <paramref name="LibraryConnectorId"/> not found.</response>
|
||||
[HttpGet("{LibraryConnectorId}")]
|
||||
[ProducesResponseType<LibraryConnector>(Status200OK, "application/json")]
|
||||
[ProducesResponseType(Status404NotFound)]
|
||||
public IActionResult GetConnector(string LibraryControllerId)
|
||||
public IActionResult GetConnector(string LibraryConnectorId)
|
||||
{
|
||||
LibraryConnector? ret = context.LibraryConnectors.Find(LibraryControllerId);
|
||||
return (ret is not null) switch
|
||||
{
|
||||
true => Ok(ret),
|
||||
false => NotFound()
|
||||
};
|
||||
if (context.LibraryConnectors.Find(LibraryConnectorId) is not { } connector)
|
||||
return NotFound();
|
||||
|
||||
return Ok(connector);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new ToLibrary-Connector
|
||||
/// Creates a new <see cref="LibraryConnector"/>
|
||||
/// </summary>
|
||||
/// <param name="libraryConnector">ToLibrary-Connector</param>
|
||||
/// <param name="libraryConnector"></param>
|
||||
/// <response code="201"></response>
|
||||
/// <response code="500">Error during Database Operation</response>
|
||||
[HttpPut]
|
||||
@ -55,46 +53,34 @@ public class LibraryConnectorController(LibraryContext context, ILog Log) : Cont
|
||||
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
|
||||
public IActionResult CreateConnector([FromBody]LibraryConnector libraryConnector)
|
||||
{
|
||||
try
|
||||
{
|
||||
|
||||
context.LibraryConnectors.Add(libraryConnector);
|
||||
context.SaveChanges();
|
||||
|
||||
if(context.Sync() is { success: false } result)
|
||||
return StatusCode(Status500InternalServerError, result.exceptionMessage);
|
||||
return Created();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e);
|
||||
return StatusCode(500, e.Message);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes the ToLibrary-Connector with the requested ID
|
||||
/// Deletes <see cref="LibraryConnector"/> with <paramref name="LibraryConnectorId"/>
|
||||
/// </summary>
|
||||
/// <param name="LibraryControllerId">ToLibrary-Connector-ID</param>
|
||||
/// <param name="LibraryConnectorId">ToFileLibrary-Connector-ID</param>
|
||||
/// <response code="200"></response>
|
||||
/// <response code="404">Connector with ID not found.</response>
|
||||
/// <response code="404"><see cref="LibraryConnector"/> with <<paramref name="LibraryConnectorId"/> not found.</response>
|
||||
/// <response code="500">Error during Database Operation</response>
|
||||
[HttpDelete("{LibraryControllerId}")]
|
||||
[HttpDelete("{LibraryConnectorId}")]
|
||||
[ProducesResponseType(Status200OK)]
|
||||
[ProducesResponseType(Status404NotFound)]
|
||||
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
|
||||
public IActionResult DeleteConnector(string LibraryControllerId)
|
||||
public IActionResult DeleteConnector(string LibraryConnectorId)
|
||||
{
|
||||
try
|
||||
{
|
||||
LibraryConnector? ret = context.LibraryConnectors.Find(LibraryControllerId);
|
||||
if (ret is null)
|
||||
if (context.LibraryConnectors.Find(LibraryConnectorId) is not { } connector)
|
||||
return NotFound();
|
||||
|
||||
context.Remove(ret);
|
||||
context.SaveChanges();
|
||||
context.LibraryConnectors.Remove(connector);
|
||||
|
||||
if(context.Sync() is { success: false } result)
|
||||
return StatusCode(Status500InternalServerError, result.exceptionMessage);
|
||||
return Ok();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e);
|
||||
return StatusCode(500, e.Message);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,164 +0,0 @@
|
||||
using API.APIEndpointRecords;
|
||||
using API.Schema;
|
||||
using API.Schema.Contexts;
|
||||
using Asp.Versioning;
|
||||
using log4net;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using static Microsoft.AspNetCore.Http.StatusCodes;
|
||||
|
||||
namespace API.Controllers;
|
||||
|
||||
[ApiVersion(2)]
|
||||
[ApiController]
|
||||
[Route("v{v:apiVersion}/[controller]")]
|
||||
public class LocalLibrariesController(PgsqlContext context, ILog Log) : Controller
|
||||
{
|
||||
[HttpGet]
|
||||
[ProducesResponseType<LocalLibrary[]>(Status200OK, "application/json")]
|
||||
public IActionResult GetLocalLibraries()
|
||||
{
|
||||
return Ok(context.LocalLibraries);
|
||||
}
|
||||
|
||||
[HttpGet("{LibraryId}")]
|
||||
[ProducesResponseType<LocalLibrary>(Status200OK, "application/json")]
|
||||
[ProducesResponseType(Status404NotFound)]
|
||||
public IActionResult GetLocalLibrary(string LibraryId)
|
||||
{
|
||||
LocalLibrary? library = context.LocalLibraries.Find(LibraryId);
|
||||
if (library is null)
|
||||
return NotFound();
|
||||
return Ok(library);
|
||||
}
|
||||
|
||||
[HttpPatch("{LibraryId}")]
|
||||
[ProducesResponseType(Status200OK)]
|
||||
[ProducesResponseType(Status404NotFound)]
|
||||
[ProducesResponseType(Status400BadRequest)]
|
||||
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
|
||||
public IActionResult UpdateLocalLibrary(string LibraryId, [FromBody]NewLibraryRecord record)
|
||||
{
|
||||
LocalLibrary? library = context.LocalLibraries.Find(LibraryId);
|
||||
if (library is null)
|
||||
return NotFound();
|
||||
if (record.Validate() == false)
|
||||
return BadRequest();
|
||||
|
||||
try
|
||||
{
|
||||
library.LibraryName = record.name;
|
||||
library.BasePath = record.path;
|
||||
context.SaveChanges();
|
||||
|
||||
return Ok();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e);
|
||||
return StatusCode(500, e.Message);
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPatch("{LibraryId}/ChangeBasePath")]
|
||||
[ProducesResponseType(Status200OK)]
|
||||
[ProducesResponseType(Status404NotFound)]
|
||||
[ProducesResponseType(Status400BadRequest)]
|
||||
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
|
||||
public IActionResult ChangeLibraryBasePath(string LibraryId, [FromBody] string newBasePath)
|
||||
{
|
||||
try
|
||||
{
|
||||
LocalLibrary? library = context.LocalLibraries.Find(LibraryId);
|
||||
if (library is null)
|
||||
return NotFound();
|
||||
|
||||
if (false) //TODO implement path check
|
||||
return BadRequest();
|
||||
|
||||
library.BasePath = newBasePath;
|
||||
context.SaveChanges();
|
||||
|
||||
return Ok();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e);
|
||||
return StatusCode(500, e.Message);
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPatch("{LibraryId}/ChangeName")]
|
||||
[ProducesResponseType(Status200OK)]
|
||||
[ProducesResponseType(Status404NotFound)]
|
||||
[ProducesResponseType(Status400BadRequest)]
|
||||
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
|
||||
public IActionResult ChangeLibraryName(string LibraryId, [FromBody] string newName)
|
||||
{
|
||||
try
|
||||
{
|
||||
LocalLibrary? library = context.LocalLibraries.Find(LibraryId);
|
||||
if (library is null)
|
||||
return NotFound();
|
||||
|
||||
if(newName.Length < 1)
|
||||
return BadRequest();
|
||||
|
||||
library.LibraryName = newName;
|
||||
context.SaveChanges();
|
||||
|
||||
return Ok();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e);
|
||||
return StatusCode(500, e.Message);
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPut]
|
||||
[ProducesResponseType<LocalLibrary>(Status200OK, "application/json")]
|
||||
[ProducesResponseType(Status400BadRequest)]
|
||||
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
|
||||
public IActionResult CreateNewLibrary([FromBody]NewLibraryRecord library)
|
||||
{
|
||||
if (library.Validate() == false)
|
||||
return BadRequest();
|
||||
try
|
||||
{
|
||||
LocalLibrary newLibrary = new (library.path, library.name);
|
||||
context.LocalLibraries.Add(newLibrary);
|
||||
context.SaveChanges();
|
||||
|
||||
return Ok(newLibrary);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e);
|
||||
return StatusCode(500, e.Message);
|
||||
}
|
||||
}
|
||||
|
||||
[HttpDelete("{LibraryId}")]
|
||||
[ProducesResponseType(Status200OK)]
|
||||
[ProducesResponseType(Status404NotFound)]
|
||||
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
|
||||
public IActionResult DeleteLocalLibrary(string LibraryId)
|
||||
{
|
||||
|
||||
try
|
||||
{
|
||||
LocalLibrary? library = context.LocalLibraries.Find(LibraryId);
|
||||
if (library is null)
|
||||
return NotFound();
|
||||
context.Remove(library);
|
||||
context.SaveChanges();
|
||||
|
||||
return Ok();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e);
|
||||
return StatusCode(500, e.Message);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,82 +1,90 @@
|
||||
using API.Schema.Contexts;
|
||||
using API.Schema.MangaConnectors;
|
||||
using API.Schema.MangaContext;
|
||||
using API.Schema.MangaContext.MangaConnectors;
|
||||
using Asp.Versioning;
|
||||
using log4net;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using static Microsoft.AspNetCore.Http.StatusCodes;
|
||||
// ReSharper disable InconsistentNaming
|
||||
|
||||
namespace API.Controllers;
|
||||
|
||||
[ApiVersion(2)]
|
||||
[ApiController]
|
||||
[Route("v{v:apiVersion}/[controller]")]
|
||||
public class MangaConnectorController(PgsqlContext context, ILog Log) : Controller
|
||||
public class MangaConnectorController(MangaContext context) : Controller
|
||||
{
|
||||
/// <summary>
|
||||
/// Get all available Connectors (Scanlation-Sites)
|
||||
/// Get all <see cref="MangaConnector"/> (Scanlation-Sites)
|
||||
/// </summary>
|
||||
/// <response code="200"></response>
|
||||
/// <response code="200">Names of <see cref="MangaConnector"/> (Scanlation-Sites)</response>
|
||||
[HttpGet]
|
||||
[ProducesResponseType<MangaConnector[]>(Status200OK, "application/json")]
|
||||
public IActionResult GetConnectors()
|
||||
{
|
||||
MangaConnector[] connectors = context.MangaConnectors.ToArray();
|
||||
return Ok(connectors);
|
||||
return Ok(context.MangaConnectors.Select(c => c.Name).ToArray());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get all enabled Connectors (Scanlation-Sites)
|
||||
/// Returns the <see cref="MangaConnector"/> (Scanlation-Sites) with the requested Name
|
||||
/// </summary>
|
||||
/// <param name="MangaConnectorName"><see cref="MangaConnector"/>.Name</param>
|
||||
/// <response code="200"></response>
|
||||
/// <response code="404"><see cref="MangaConnector"/> (Scanlation-Sites) with Name not found.</response>
|
||||
[HttpGet("{MangaConnectorName}")]
|
||||
[ProducesResponseType<MangaConnector>(Status200OK, "application/json")]
|
||||
[ProducesResponseType(Status404NotFound)]
|
||||
public IActionResult GetConnector(string MangaConnectorName)
|
||||
{
|
||||
if(context.MangaConnectors.Find(MangaConnectorName) is not { } connector)
|
||||
return NotFound();
|
||||
|
||||
return Ok(connector);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get all enabled <see cref="MangaConnector"/> (Scanlation-Sites)
|
||||
/// </summary>
|
||||
/// <response code="200"></response>
|
||||
[HttpGet("enabled")]
|
||||
[HttpGet("Enabled")]
|
||||
[ProducesResponseType<MangaConnector[]>(Status200OK, "application/json")]
|
||||
public IActionResult GetEnabledConnectors()
|
||||
{
|
||||
MangaConnector[] connectors = context.MangaConnectors.Where(c => c.Enabled == true).ToArray();
|
||||
return Ok(connectors);
|
||||
|
||||
return Ok(context.MangaConnectors.Where(c => c.Enabled).ToArray());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get all disabled Connectors (Scanlation-Sites)
|
||||
/// Get all disabled <see cref="MangaConnector"/> (Scanlation-Sites)
|
||||
/// </summary>
|
||||
/// <response code="200"></response>
|
||||
[HttpGet("disabled")]
|
||||
[HttpGet("Disabled")]
|
||||
[ProducesResponseType<MangaConnector[]>(Status200OK, "application/json")]
|
||||
public IActionResult GetDisabledConnectors()
|
||||
{
|
||||
MangaConnector[] connectors = context.MangaConnectors.Where(c => c.Enabled == false).ToArray();
|
||||
return Ok(connectors);
|
||||
|
||||
return Ok(context.MangaConnectors.Where(c => c.Enabled == false).ToArray());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enabled or disables a Connector
|
||||
/// Enabled or disables <see cref="MangaConnector"/> (Scanlation-Sites) with Name
|
||||
/// </summary>
|
||||
/// <param name="MangaConnectorName">ID of the connector</param>
|
||||
/// <param name="enabled">Set true to enable</param>
|
||||
/// <response code="200"></response>
|
||||
/// <response code="404">Connector with ID not found.</response>
|
||||
/// <param name="MangaConnectorName"><see cref="MangaConnector"/>.Name</param>
|
||||
/// <param name="Enabled">Set true to enable, false to disable</param>
|
||||
/// <response code="202"></response>
|
||||
/// <response code="404"><see cref="MangaConnector"/> (Scanlation-Sites) with Name not found.</response>
|
||||
/// <response code="500">Error during Database Operation</response>
|
||||
[HttpPatch("{MangaConnectorName}/SetEnabled/{enabled}")]
|
||||
[ProducesResponseType(Status200OK)]
|
||||
[HttpPatch("{MangaConnectorName}/SetEnabled/{Enabled}")]
|
||||
[ProducesResponseType(Status202Accepted)]
|
||||
[ProducesResponseType(Status404NotFound)]
|
||||
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
|
||||
public IActionResult SetEnabled(string MangaConnectorName, bool enabled)
|
||||
public IActionResult SetEnabled(string MangaConnectorName, bool Enabled)
|
||||
{
|
||||
try
|
||||
{
|
||||
MangaConnector? connector = context.MangaConnectors.Find(MangaConnectorName);
|
||||
if (connector is null)
|
||||
if(context.MangaConnectors.Find(MangaConnectorName) is not { } connector)
|
||||
return NotFound();
|
||||
|
||||
connector.Enabled = enabled;
|
||||
context.SaveChanges();
|
||||
connector.Enabled = Enabled;
|
||||
|
||||
return Ok();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e);
|
||||
return StatusCode(500, e.Message);
|
||||
}
|
||||
if(context.Sync() is { success: false } result)
|
||||
return StatusCode(Status500InternalServerError, result.exceptionMessage);
|
||||
return Accepted();
|
||||
}
|
||||
}
|
@ -1,9 +1,9 @@
|
||||
using API.Schema;
|
||||
using API.Schema.Contexts;
|
||||
using API.Schema.Jobs;
|
||||
using API.Schema.MangaContext;
|
||||
using API.Schema.MangaContext.MangaConnectors;
|
||||
using API.Workers;
|
||||
using Asp.Versioning;
|
||||
using log4net;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.Formats.Jpeg;
|
||||
using SixLabors.ImageSharp.Processing;
|
||||
@ -16,10 +16,10 @@ namespace API.Controllers;
|
||||
[ApiVersion(2)]
|
||||
[ApiController]
|
||||
[Route("v{v:apiVersion}/[controller]")]
|
||||
public class MangaController(PgsqlContext context, ILog Log) : Controller
|
||||
public class MangaController(MangaContext context) : Controller
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns all cached Manga
|
||||
/// Returns all cached <see cref="Manga"/>
|
||||
/// </summary>
|
||||
/// <response code="200"></response>
|
||||
[HttpGet]
|
||||
@ -31,41 +31,40 @@ public class MangaController(PgsqlContext context, ILog Log) : Controller
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns all cached Manga with IDs
|
||||
/// Returns all cached <see cref="Manga"/> with <paramref name="MangaIds"/>
|
||||
/// </summary>
|
||||
/// <param name="ids">Array of Manga-IDs</param>
|
||||
/// <param name="MangaIds">Array of <<see cref="Manga"/>.Key</param>
|
||||
/// <response code="200"></response>
|
||||
[HttpPost("WithIDs")]
|
||||
[ProducesResponseType<Manga[]>(Status200OK, "application/json")]
|
||||
public IActionResult GetManga([FromBody]string[] ids)
|
||||
public IActionResult GetManga([FromBody]string[] MangaIds)
|
||||
{
|
||||
Manga[] ret = context.Mangas.Where(m => ids.Contains(m.MangaId)).ToArray();
|
||||
Manga[] ret = context.Mangas.Where(m => MangaIds.Contains(m.Key)).ToArray();
|
||||
return Ok(ret);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Return Manga with ID
|
||||
/// Return <see cref="Manga"/> with <paramref name="MangaId"/>
|
||||
/// </summary>
|
||||
/// <param name="MangaId">Manga-ID</param>
|
||||
/// <param name="MangaId"><see cref="Manga"/>.Key</param>
|
||||
/// <response code="200"></response>
|
||||
/// <response code="404">Manga with ID not found</response>
|
||||
/// <response code="404"><see cref="Manga"/> with <paramref name="MangaId"/> not found</response>
|
||||
[HttpGet("{MangaId}")]
|
||||
[ProducesResponseType<Manga>(Status200OK, "application/json")]
|
||||
[ProducesResponseType(Status404NotFound)]
|
||||
public IActionResult GetManga(string MangaId)
|
||||
{
|
||||
Manga? ret = context.Mangas.Find(MangaId);
|
||||
if (ret is null)
|
||||
return NotFound();
|
||||
return Ok(ret);
|
||||
if (context.Mangas.Find(MangaId) is not { } manga)
|
||||
return NotFound(nameof(MangaId));
|
||||
return Ok(manga);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Delete Manga with ID
|
||||
/// Delete <see cref="Manga"/> with <paramref name="MangaId"/>
|
||||
/// </summary>
|
||||
/// <param name="MangaId">Manga-ID</param>
|
||||
/// <param name="MangaId"><see cref="Manga"/>.Key</param>
|
||||
/// <response code="200"></response>
|
||||
/// <response code="404">Manga with ID 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)]
|
||||
@ -73,33 +72,50 @@ public class MangaController(PgsqlContext context, ILog Log) : Controller
|
||||
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
|
||||
public IActionResult DeleteManga(string MangaId)
|
||||
{
|
||||
try
|
||||
{
|
||||
Manga? ret = context.Mangas.Find(MangaId);
|
||||
if (ret is null)
|
||||
return NotFound();
|
||||
if (context.Mangas.Find(MangaId) is not { } manga)
|
||||
return NotFound(nameof(MangaId));
|
||||
|
||||
context.Remove(ret);
|
||||
context.SaveChanges();
|
||||
context.Mangas.Remove(manga);
|
||||
|
||||
if(context.Sync() is { success: false } result)
|
||||
return StatusCode(Status500InternalServerError, result.exceptionMessage);
|
||||
return Ok();
|
||||
}
|
||||
catch (Exception e)
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Merge two <see cref="Manga"/> into one. THIS IS NOT REVERSIBLE!
|
||||
/// </summary>
|
||||
/// <param name="MangaIdFrom"><see cref="Manga"/>.Key of <see cref="Manga"/> merging data from (getting deleted)</param>
|
||||
/// <param name="MangaIdInto"><see cref="Manga"/>.Key of <see cref="Manga"/> merging data into</param>
|
||||
/// <response code="200"></response>
|
||||
/// <response code="404"><see cref="Manga"/> with <paramref name="MangaIdFrom"/> or <paramref name="MangaIdInto"/> not found</response>
|
||||
[HttpPatch("{MangaIdFrom}/MergeInto/{MangaIdInto}")]
|
||||
[ProducesResponseType<byte[]>(Status200OK,"image/jpeg")]
|
||||
[ProducesResponseType(Status404NotFound)]
|
||||
public IActionResult MergeIntoManga(string MangaIdFrom, string MangaIdInto)
|
||||
{
|
||||
Log.Error(e);
|
||||
return StatusCode(500, e.Message);
|
||||
}
|
||||
if (context.Mangas.Find(MangaIdFrom) is not { } from)
|
||||
return NotFound(nameof(MangaIdFrom));
|
||||
if (context.Mangas.Find(MangaIdInto) is not { } into)
|
||||
return NotFound(nameof(MangaIdInto));
|
||||
|
||||
BaseWorker[] newJobs = into.MergeFrom(from, context);
|
||||
Tranga.AddWorkers(newJobs);
|
||||
|
||||
return Ok();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns Cover of Manga
|
||||
/// Returns Cover of <see cref="Manga"/> with <paramref name="MangaId"/>
|
||||
/// </summary>
|
||||
/// <param name="MangaId">Manga-ID</param>
|
||||
/// <param name="width">If width is provided, height needs to also be provided</param>
|
||||
/// <param name="height">If height is provided, width needs to also be provided</param>
|
||||
/// <param name="MangaId"><see cref="Manga"/>.Key</param>
|
||||
/// <param name="width">If <paramref name="width"/> is provided, <paramref name="height"/> needs to also be provided</param>
|
||||
/// <param name="height">If <paramref name="height"/> is provided, <paramref name="width"/> needs to also be provided</param>
|
||||
/// <response code="200">JPEG Image</response>
|
||||
/// <response code="204">Cover not loaded</response>
|
||||
/// <response code="400">The formatting-request was invalid</response>
|
||||
/// <response code="404">Manga with ID not found</response>
|
||||
/// <response code="404"><see cref="Manga"/> with <paramref name="MangaId"/> not found</response>
|
||||
/// <response code="503">Retry later, downloading cover</response>
|
||||
[HttpGet("{MangaId}/Cover")]
|
||||
[ProducesResponseType<byte[]>(Status200OK,"image/jpeg")]
|
||||
@ -109,24 +125,21 @@ public class MangaController(PgsqlContext context, ILog Log) : Controller
|
||||
[ProducesResponseType<int>(Status503ServiceUnavailable, "text/plain")]
|
||||
public IActionResult GetCover(string MangaId, [FromQuery]int? width, [FromQuery]int? height)
|
||||
{
|
||||
DateTime requestStarted = HttpContext.Features.Get<IHttpRequestTimeFeature>()?.RequestTime ?? DateTime.Now;
|
||||
Manga? m = context.Mangas.Find(MangaId);
|
||||
if (m is null)
|
||||
return NotFound();
|
||||
if (context.Mangas.Find(MangaId) is not { } manga)
|
||||
return NotFound(nameof(MangaId));
|
||||
|
||||
if (!System.IO.File.Exists(m.CoverFileNameInCache))
|
||||
if (!System.IO.File.Exists(manga.CoverFileNameInCache))
|
||||
{
|
||||
List<Job> coverDownloadJobs = context.Jobs.Where(j => j.JobType == JobType.DownloadMangaCoverJob).ToList();
|
||||
if (coverDownloadJobs.Any(j => j is DownloadMangaCoverJob dmc && dmc.MangaId == MangaId))
|
||||
if (Tranga.GetRunningWorkers().Any(worker => worker is DownloadCoverFromMangaconnectorWorker w && context.MangaConnectorToManga.Find(w.MangaConnectorIdId)?.ObjId == MangaId))
|
||||
{
|
||||
Response.Headers.Add("Retry-After", $"{TrangaSettings.startNewJobTimeoutMs * coverDownloadJobs.Count() * 2 / 1000:D}");
|
||||
return StatusCode(Status503ServiceUnavailable, TrangaSettings.startNewJobTimeoutMs * coverDownloadJobs.Count() * 2 / 1000);
|
||||
Response.Headers.Append("Retry-After", $"{Tranga.Settings.WorkCycleTimeoutMs * 2 / 1000:D}");
|
||||
return StatusCode(Status503ServiceUnavailable, Tranga.Settings.WorkCycleTimeoutMs * 2 / 1000);
|
||||
}
|
||||
else
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
Image image = Image.Load(m.CoverFileNameInCache);
|
||||
Image image = Image.Load(manga.CoverFileNameInCache);
|
||||
|
||||
if (width is { } w && height is { } h)
|
||||
{
|
||||
@ -141,46 +154,46 @@ public class MangaController(PgsqlContext context, ILog Log) : Controller
|
||||
|
||||
using MemoryStream ms = new();
|
||||
image.Save(ms, new JpegEncoder(){Quality = 100});
|
||||
return File(ms.GetBuffer(), "image/jpeg");
|
||||
DateTime lastModified = new FileInfo(manga.CoverFileNameInCache).LastWriteTime;
|
||||
HttpContext.Response.Headers.CacheControl = "public";
|
||||
return File(ms.GetBuffer(), "image/jpeg", new DateTimeOffset(lastModified), EntityTagHeaderValue.Parse($"\"{lastModified.Ticks}\""));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns all Chapters of Manga
|
||||
/// Returns all <see cref="Chapter"/> of <see cref="Manga"/> with <paramref name="MangaId"/>
|
||||
/// </summary>
|
||||
/// <param name="MangaId">Manga-ID</param>
|
||||
/// <param name="MangaId"><see cref="Manga"/>.Key</param>
|
||||
/// <response code="200"></response>
|
||||
/// <response code="404">Manga with ID not found</response>
|
||||
/// <response code="404"><see cref="Manga"/> with <paramref name="MangaId"/> not found</response>
|
||||
[HttpGet("{MangaId}/Chapters")]
|
||||
[ProducesResponseType<Chapter[]>(Status200OK, "application/json")]
|
||||
[ProducesResponseType(Status404NotFound)]
|
||||
public IActionResult GetChapters(string MangaId)
|
||||
{
|
||||
Manga? m = context.Mangas.Find(MangaId);
|
||||
if (m is null)
|
||||
return NotFound();
|
||||
if (context.Mangas.Find(MangaId) is not { } manga)
|
||||
return NotFound(nameof(MangaId));
|
||||
|
||||
Chapter[] ret = context.Chapters.Where(c => c.ParentMangaId == m.MangaId).ToArray();
|
||||
return Ok(ret);
|
||||
Chapter[] chapters = manga.Chapters.ToArray();
|
||||
return Ok(chapters);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns all downloaded Chapters for Manga with ID
|
||||
/// Returns all downloaded <see cref="Chapter"/> for <see cref="Manga"/> with <paramref name="MangaId"/>
|
||||
/// </summary>
|
||||
/// <param name="MangaId">Manga-ID</param>
|
||||
/// <param name="MangaId"><see cref="Manga"/>.Key</param>
|
||||
/// <response code="200"></response>
|
||||
/// <response code="204">No available chapters</response>
|
||||
/// <response code="404">Manga with ID not found.</response>
|
||||
/// <response code="404"><see cref="Manga"/> with <paramref name="MangaId"/> not found.</response>
|
||||
[HttpGet("{MangaId}/Chapters/Downloaded")]
|
||||
[ProducesResponseType<Chapter[]>(Status200OK, "application/json")]
|
||||
[ProducesResponseType(Status204NoContent)]
|
||||
[ProducesResponseType(Status404NotFound)]
|
||||
public IActionResult GetChaptersDownloaded(string MangaId)
|
||||
{
|
||||
Manga? m = context.Mangas.Find(MangaId);
|
||||
if (m is null)
|
||||
return NotFound();
|
||||
if (context.Mangas.Find(MangaId) is not { } manga)
|
||||
return NotFound(nameof(MangaId));
|
||||
|
||||
List<Chapter> chapters = context.Chapters.Where(c => c.ParentMangaId == m.MangaId && c.Downloaded == true).ToList();
|
||||
List<Chapter> chapters = manga.Chapters.Where(c => c.Downloaded).ToList();
|
||||
if (chapters.Count == 0)
|
||||
return NoContent();
|
||||
|
||||
@ -188,23 +201,22 @@ public class MangaController(PgsqlContext context, ILog Log) : Controller
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns all Chapters not downloaded for Manga with ID
|
||||
/// Returns all <see cref="Chapter"/> not downloaded for <see cref="Manga"/> with <paramref name="MangaId"/>
|
||||
/// </summary>
|
||||
/// <param name="MangaId">Manga-ID</param>
|
||||
/// <param name="MangaId"><see cref="Manga"/>.Key</param>
|
||||
/// <response code="200"></response>
|
||||
/// <response code="204">No available chapters</response>
|
||||
/// <response code="404">Manga with ID not found.</response>
|
||||
/// <response code="404"><see cref="Manga"/> with <paramref name="MangaId"/> not found.</response>
|
||||
[HttpGet("{MangaId}/Chapters/NotDownloaded")]
|
||||
[ProducesResponseType<Chapter[]>(Status200OK, "application/json")]
|
||||
[ProducesResponseType(Status204NoContent)]
|
||||
[ProducesResponseType(Status404NotFound)]
|
||||
public IActionResult GetChaptersNotDownloaded(string MangaId)
|
||||
{
|
||||
Manga? m = context.Mangas.Find(MangaId);
|
||||
if (m is null)
|
||||
return NotFound();
|
||||
if (context.Mangas.Find(MangaId) is not { } manga)
|
||||
return NotFound(nameof(MangaId));
|
||||
|
||||
List<Chapter> chapters = context.Chapters.Where(c => c.ParentMangaId == m.MangaId && c.Downloaded == false).ToList();
|
||||
List<Chapter> chapters = manga.Chapters.Where(c => c.Downloaded == false).ToList();
|
||||
if (chapters.Count == 0)
|
||||
return NoContent();
|
||||
|
||||
@ -212,13 +224,13 @@ public class MangaController(PgsqlContext context, ILog Log) : Controller
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the latest Chapter of requested Manga available on Website
|
||||
/// Returns the latest <see cref="Chapter"/> of requested <see cref="Manga"/> available on <see cref="MangaConnector"/>
|
||||
/// </summary>
|
||||
/// <param name="MangaId">Manga-ID</param>
|
||||
/// <param name="MangaId"><see cref="Manga"/>.Key</param>
|
||||
/// <response code="200"></response>
|
||||
/// <response code="204">No available chapters</response>
|
||||
/// <response code="404">Manga with ID not found.</response>
|
||||
/// <response code="500">Could not retrieve the maximum chapter-number</response>
|
||||
/// <response code="404"><see cref="Manga"/> with <paramref name="MangaId"/> not found.</response>
|
||||
/// <response code="412">Could not retrieve the maximum chapter-number</response>
|
||||
/// <response code="503">Retry after timeout, updating value</response>
|
||||
[HttpGet("{MangaId}/Chapter/LatestAvailable")]
|
||||
[ProducesResponseType<Chapter>(Status200OK, "application/json")]
|
||||
@ -228,133 +240,151 @@ public class MangaController(PgsqlContext context, ILog Log) : Controller
|
||||
[ProducesResponseType<int>(Status503ServiceUnavailable, "text/plain")]
|
||||
public IActionResult GetLatestChapter(string MangaId)
|
||||
{
|
||||
Manga? m = context.Mangas.Find(MangaId);
|
||||
if (m is null)
|
||||
return NotFound();
|
||||
if (context.Mangas.Find(MangaId) is not { } manga)
|
||||
return NotFound(nameof(MangaId));
|
||||
|
||||
List<Chapter> chapters = context.Chapters.Where(c => c.ParentMangaId == m.MangaId).ToList();
|
||||
List<Chapter> chapters = manga.Chapters.ToList();
|
||||
if (chapters.Count == 0)
|
||||
{
|
||||
List<Job> retrieveChapterJobs = context.Jobs.Where(j => j.JobType == JobType.RetrieveChaptersJob).ToList();
|
||||
if (retrieveChapterJobs.Any(j => j is RetrieveChaptersJob rcj && rcj.MangaId == MangaId))
|
||||
if (Tranga.GetRunningWorkers().Any(worker => worker is RetrieveMangaChaptersFromMangaconnectorWorker w && context.MangaConnectorToManga.Find(w.MangaConnectorIdId)?.ObjId == MangaId && w.State < WorkerExecutionState.Completed))
|
||||
{
|
||||
Response.Headers.Add("Retry-After", $"{TrangaSettings.startNewJobTimeoutMs * retrieveChapterJobs.Count() * 2 / 1000:D}");
|
||||
return StatusCode(Status503ServiceUnavailable, TrangaSettings.startNewJobTimeoutMs * retrieveChapterJobs.Count() * 2/ 1000);
|
||||
Response.Headers.Append("Retry-After", $"{Tranga.Settings.WorkCycleTimeoutMs * 2 / 1000:D}");
|
||||
return StatusCode(Status503ServiceUnavailable, Tranga.Settings.WorkCycleTimeoutMs * 2/ 1000);
|
||||
}else
|
||||
return NoContent();
|
||||
return Ok(0);
|
||||
}
|
||||
|
||||
Chapter? max = chapters.Max();
|
||||
if (max is null)
|
||||
return StatusCode(500, "Max chapter could not be found");
|
||||
return StatusCode(Status500InternalServerError, "Max chapter could not be found");
|
||||
|
||||
return Ok(max);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the latest Chapter of requested Manga that is downloaded
|
||||
/// Returns the latest <see cref="Chapter"/> of requested <see cref="Manga"/> that is downloaded
|
||||
/// </summary>
|
||||
/// <param name="MangaId">Manga-ID</param>
|
||||
/// <param name="MangaId"><see cref="Manga"/>.Key</param>
|
||||
/// <response code="200"></response>
|
||||
/// <response code="204">No available chapters</response>
|
||||
/// <response code="404">Manga with ID not found.</response>
|
||||
/// <response code="500">Could not retrieve the maximum chapter-number</response>
|
||||
/// <response code="404"><see cref="Manga"/> with <paramref name="MangaId"/> not found.</response>
|
||||
/// <response code="412">Could not retrieve the maximum chapter-number</response>
|
||||
/// <response code="503">Retry after timeout, updating value</response>
|
||||
[HttpGet("{MangaId}/Chapter/LatestDownloaded")]
|
||||
[ProducesResponseType<Chapter>(Status200OK, "application/json")]
|
||||
[ProducesResponseType(Status204NoContent)]
|
||||
[ProducesResponseType(Status404NotFound)]
|
||||
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
|
||||
[ProducesResponseType<string>(Status412PreconditionFailed, "text/plain")]
|
||||
[ProducesResponseType<int>(Status503ServiceUnavailable, "text/plain")]
|
||||
public IActionResult GetLatestChapterDownloaded(string MangaId)
|
||||
{
|
||||
Manga? m = context.Mangas.Find(MangaId);
|
||||
if (m is null)
|
||||
return NotFound();
|
||||
if (context.Mangas.Find(MangaId) is not { } manga)
|
||||
return NotFound(nameof(MangaId));
|
||||
|
||||
|
||||
List<Chapter> chapters = context.Chapters.Where(c => c.ParentMangaId == m.MangaId && c.Downloaded == true).ToList();
|
||||
List<Chapter> chapters = manga.Chapters.ToList();
|
||||
if (chapters.Count == 0)
|
||||
{
|
||||
List<Job> retrieveChapterJobs = context.Jobs.Where(j => j.JobType == JobType.RetrieveChaptersJob).ToList();
|
||||
if (retrieveChapterJobs.Any(j => j is RetrieveChaptersJob rcj && rcj.MangaId == MangaId))
|
||||
if (Tranga.GetRunningWorkers().Any(worker => worker is RetrieveMangaChaptersFromMangaconnectorWorker w && context.MangaConnectorToManga.Find(w.MangaConnectorIdId)?.ObjId == MangaId && w.State < WorkerExecutionState.Completed))
|
||||
{
|
||||
Response.Headers.Add("Retry-After", $"{TrangaSettings.startNewJobTimeoutMs * retrieveChapterJobs.Count() * 2 / 1000:D}");
|
||||
return StatusCode(Status503ServiceUnavailable, TrangaSettings.startNewJobTimeoutMs * retrieveChapterJobs.Count() * 2 / 1000);
|
||||
Response.Headers.Append("Retry-After", $"{Tranga.Settings.WorkCycleTimeoutMs * 2 / 1000:D}");
|
||||
return StatusCode(Status503ServiceUnavailable, Tranga.Settings.WorkCycleTimeoutMs * 2/ 1000);
|
||||
}else
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
Chapter? max = chapters.Max();
|
||||
if (max is null)
|
||||
return StatusCode(500, "Max chapter could not be found");
|
||||
return StatusCode(Status412PreconditionFailed, "Max chapter could not be found");
|
||||
|
||||
return Ok(max);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configure the cut-off for Manga
|
||||
/// Configure the <see cref="Chapter"/> cut-off for <see cref="Manga"/>
|
||||
/// </summary>
|
||||
/// <param name="MangaId">Manga-ID</param>
|
||||
/// <param name="chapterThreshold">Threshold (Chapter Number)</param>
|
||||
/// <response code="200"></response>
|
||||
/// <response code="404">Manga with ID not found.</response>
|
||||
/// <param name="MangaId"><see cref="Manga"/>.Key</param>
|
||||
/// <param name="chapterThreshold">Threshold (<see cref="Chapter"/> ChapterNumber)</param>
|
||||
/// <response code="202"></response>
|
||||
/// <response code="404"><see cref="Manga"/> with <paramref name="MangaId"/> not found.</response>
|
||||
/// <response code="500">Error during Database Operation</response>
|
||||
[HttpPatch("{MangaId}/IgnoreChaptersBefore")]
|
||||
[ProducesResponseType(Status200OK)]
|
||||
[ProducesResponseType(Status202Accepted)]
|
||||
[ProducesResponseType(Status404NotFound)]
|
||||
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
|
||||
public IActionResult IgnoreChaptersBefore(string MangaId, [FromBody]float chapterThreshold)
|
||||
{
|
||||
Manga? m = context.Mangas.Find(MangaId);
|
||||
if (m is null)
|
||||
if (context.Mangas.Find(MangaId) is not { } manga)
|
||||
return NotFound();
|
||||
|
||||
try
|
||||
{
|
||||
m.IgnoreChaptersBefore = chapterThreshold;
|
||||
context.SaveChanges();
|
||||
return Ok();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e);
|
||||
return StatusCode(500, e.Message);
|
||||
}
|
||||
manga.IgnoreChaptersBefore = chapterThreshold;
|
||||
if(context.Sync() is { success: false } result)
|
||||
return StatusCode(Status500InternalServerError, result.exceptionMessage);
|
||||
|
||||
return Accepted();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Move Manga to different ToLibrary
|
||||
/// Move <see cref="Manga"/> to different <see cref="FileLibrary"/>
|
||||
/// </summary>
|
||||
/// <param name="MangaId">Manga-ID</param>
|
||||
/// <param name="LibraryId">ToLibrary-Id</param>
|
||||
/// <param name="MangaId"><see cref="Manga"/>.Key</param>
|
||||
/// <param name="LibraryId"><see cref="FileLibrary"/>.Key</param>
|
||||
/// <response code="202">Folder is going to be moved</response>
|
||||
/// <response code="404">MangaId or LibraryId not found</response>
|
||||
/// <response code="500">Error during Database Operation</response>
|
||||
/// <response code="404"><paramref name="MangaId"/> or <paramref name="LibraryId"/> not found</response>
|
||||
[HttpPost("{MangaId}/ChangeLibrary/{LibraryId}")]
|
||||
[ProducesResponseType(Status202Accepted)]
|
||||
[ProducesResponseType(Status404NotFound)]
|
||||
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
|
||||
public IActionResult MoveFolder(string MangaId, string LibraryId)
|
||||
{
|
||||
if (context.Mangas.Find(MangaId) is not { } manga)
|
||||
return NotFound();
|
||||
if(context.LocalLibraries.Find(LibraryId) is not { } library)
|
||||
return NotFound();
|
||||
return NotFound(nameof(MangaId));
|
||||
if(context.FileLibraries.Find(LibraryId) is not { } library)
|
||||
return NotFound(nameof(LibraryId));
|
||||
|
||||
MoveMangaLibraryJob moveLibrary = new(manga, library);
|
||||
UpdateFilesDownloadedJob updateDownloadedFiles = new(manga, 0, dependsOnJobs: [moveLibrary]);
|
||||
MoveMangaLibraryWorker moveLibrary = new(manga, library);
|
||||
|
||||
Tranga.AddWorkers([moveLibrary]);
|
||||
|
||||
try
|
||||
{
|
||||
context.Jobs.AddRange(moveLibrary, updateDownloadedFiles);
|
||||
context.SaveChanges();
|
||||
return Accepted();
|
||||
}
|
||||
catch (Exception e)
|
||||
|
||||
/// <summary>
|
||||
/// (Un-)Marks <see cref="Manga"/> as requested for Download from <see cref="MangaConnector"/>
|
||||
/// </summary>
|
||||
/// <param name="MangaId"><see cref="Manga"/> with <paramref name="MangaId"/></param>
|
||||
/// <param name="MangaConnectorName"><see cref="MangaConnector"/> with <paramref name="MangaConnectorName"/></param>
|
||||
/// <param name="IsRequested">true to mark as requested, false to mark as not-requested</param>
|
||||
/// <response code="200"></response>
|
||||
/// <response code="404"><paramref name="MangaId"/> or <paramref name="MangaConnectorName"/> not found</response>
|
||||
/// <response code="412"><see cref="Manga"/> was not linked to <see cref="MangaConnector"/>, so nothing changed</response>
|
||||
/// <response code="428"><see cref="Manga"/> is not linked to <see cref="MangaConnector"/> yet. Search for <see cref="Manga"/> on <see cref="MangaConnector"/> first (to create a <see cref="MangaConnectorId{T}"/>).</response>
|
||||
/// <response code="500">Error during Database Operation</response>
|
||||
[HttpPost("{MangaId}/SetAsDownloadFrom/{MangaConnectorName}/{IsRequested}")]
|
||||
[ProducesResponseType(Status200OK)]
|
||||
[ProducesResponseType<string>(Status404NotFound, "text/plain")]
|
||||
[ProducesResponseType<string>(Status412PreconditionFailed, "text/plain")]
|
||||
[ProducesResponseType<string>(Status428PreconditionRequired, "text/plain")]
|
||||
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
|
||||
public IActionResult MarkAsRequested(string MangaId, string MangaConnectorName, bool IsRequested)
|
||||
{
|
||||
Log.Error(e);
|
||||
return StatusCode(500, e.Message);
|
||||
}
|
||||
if (context.Mangas.Find(MangaId) is null)
|
||||
return NotFound(nameof(MangaId));
|
||||
if(context.MangaConnectors.Find(MangaConnectorName) is null)
|
||||
return NotFound(nameof(MangaConnectorName));
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
121
API/Controllers/MetadataFetcherController.cs
Normal file
121
API/Controllers/MetadataFetcherController.cs
Normal file
@ -0,0 +1,121 @@
|
||||
using API.Schema.MangaContext;
|
||||
using API.Schema.MangaContext.MetadataFetchers;
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
using static Microsoft.AspNetCore.Http.StatusCodes;
|
||||
// ReSharper disable InconsistentNaming
|
||||
|
||||
namespace API.Controllers;
|
||||
|
||||
[ApiVersion(2)]
|
||||
[ApiController]
|
||||
[Route("v{v:apiVersion}/[controller]")]
|
||||
public class MetadataFetcherController(MangaContext context) : Controller
|
||||
{
|
||||
/// <summary>
|
||||
/// Get all <see cref="MetadataFetcher"/> (Metadata-Sites)
|
||||
/// </summary>
|
||||
/// <response code="200">Names of <see cref="MetadataFetcher"/> (Metadata-Sites)</response>
|
||||
[HttpGet]
|
||||
[ProducesResponseType<string[]>(Status200OK, "application/json")]
|
||||
public IActionResult GetConnectors()
|
||||
{
|
||||
return Ok(Tranga.MetadataFetchers.Select(m => m.Name).ToArray());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns all <see cref="MetadataEntry"/>
|
||||
/// </summary>
|
||||
/// <response code="200"></response>
|
||||
[HttpGet("Links")]
|
||||
[ProducesResponseType<MetadataEntry[]>(Status200OK, "application/json")]
|
||||
public IActionResult GetLinkedEntries()
|
||||
{
|
||||
return Ok(context.MetadataEntries.ToArray());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Searches <see cref="MetadataFetcher"/> (Metadata-Sites) for Manga-Metadata
|
||||
/// </summary>
|
||||
/// <param name="MangaId"><see cref="Manga"/>.Key</param>
|
||||
/// <param name="MetadataFetcherName"><see cref="MetadataFetcher"/>.Name</param>
|
||||
/// <param name="searchTerm">Instead of using the <paramref name="MangaId"/> for search on Website, use a specific term</param>
|
||||
/// <response code="200"></response>
|
||||
/// <response code="400"><see cref="MetadataFetcher"/> (Metadata-Sites) with <paramref name="MetadataFetcherName"/> does not exist</response>
|
||||
/// <response code="404"><see cref="Manga"/> with <paramref name="MangaId"/> not found</response>
|
||||
[HttpPost("{MetadataFetcherName}/SearchManga/{MangaId}")]
|
||||
[ProducesResponseType<MetadataSearchResult[]>(Status200OK, "application/json")]
|
||||
[ProducesResponseType(Status400BadRequest)]
|
||||
[ProducesResponseType(Status404NotFound)]
|
||||
public IActionResult SearchMangaMetadata(string MangaId, string MetadataFetcherName, [FromBody(EmptyBodyBehavior = EmptyBodyBehavior.Allow)]string? searchTerm = null)
|
||||
{
|
||||
if(context.Mangas.Find(MangaId) is not { } manga)
|
||||
return NotFound();
|
||||
if(Tranga.MetadataFetchers.FirstOrDefault(f => f.Name == MetadataFetcherName) is not { } fetcher)
|
||||
return BadRequest();
|
||||
|
||||
MetadataSearchResult[] searchResults = searchTerm is null ? fetcher.SearchMetadataEntry(manga) : fetcher.SearchMetadataEntry(searchTerm);
|
||||
return Ok(searchResults);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Links <see cref="MetadataFetcher"/> (Metadata-Sites) using Provider-Specific Identifier to <see cref="Manga"/>
|
||||
/// </summary>
|
||||
/// <param name="MangaId"><see cref="Manga"/>.Key</param>
|
||||
/// <param name="MetadataFetcherName"><see cref="MetadataFetcher"/>.Name</param>
|
||||
/// <param name="Identifier"><see cref="MetadataFetcherName"/>-Specific ID</param>
|
||||
/// <response code="200"></response>
|
||||
/// <response code="400"><see cref="MetadataFetcher"/> (Metadata-Sites) with <paramref name="MetadataFetcherName"/> does not exist</response>
|
||||
/// <response code="404"><see cref="Manga"/> with <paramref name="MangaId"/> not found</response>
|
||||
/// <response code="500">Error during Database Operation</response>
|
||||
[HttpPost("{MetadataFetcherName}/Link/{MangaId}")]
|
||||
[ProducesResponseType<MetadataEntry>(Status200OK, "application/json")]
|
||||
[ProducesResponseType(Status400BadRequest)]
|
||||
[ProducesResponseType(Status404NotFound)]
|
||||
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
|
||||
public IActionResult LinkMangaMetadata(string MangaId, string MetadataFetcherName, [FromBody]string Identifier)
|
||||
{
|
||||
if(context.Mangas.Find(MangaId) is not { } manga)
|
||||
return NotFound();
|
||||
if(Tranga.MetadataFetchers.FirstOrDefault(f => f.Name == MetadataFetcherName) is not { } fetcher)
|
||||
return BadRequest();
|
||||
|
||||
MetadataEntry entry = fetcher.CreateMetadataEntry(manga, Identifier);
|
||||
context.MetadataEntries.Add(entry);
|
||||
|
||||
if(context.Sync() is { } errorMessage)
|
||||
return StatusCode(Status500InternalServerError, errorMessage);
|
||||
return Ok(entry);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Un-Links <see cref="MetadataFetcher"/> (Metadata-Sites) from <see cref="Manga"/>
|
||||
/// </summary>
|
||||
/// <response code="200"></response>
|
||||
/// <response code="400"><see cref="MetadataFetcher"/> (Metadata-Sites) with <paramref name="MetadataFetcherName"/> does not exist</response>
|
||||
/// <response code="404"><see cref="Manga"/> with <paramref name="MangaId"/> not found</response>
|
||||
/// <response code="412">No <see cref="MetadataEntry"/> linking <see cref="Manga"/> and <see cref="MetadataFetcher"/> found</response>
|
||||
/// <response code="500">Error during Database Operation</response>
|
||||
[HttpPost("{MetadataFetcherName}/Unlink/{MangaId}")]
|
||||
[ProducesResponseType(Status200OK)]
|
||||
[ProducesResponseType(Status400BadRequest)]
|
||||
[ProducesResponseType(Status404NotFound)]
|
||||
[ProducesResponseType<string>(Status412PreconditionFailed, "text/plain")]
|
||||
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
|
||||
public IActionResult UnlinkMangaMetadata(string MangaId, string MetadataFetcherName)
|
||||
{
|
||||
if(context.Mangas.Find(MangaId) is null)
|
||||
return NotFound();
|
||||
if(Tranga.MetadataFetchers.FirstOrDefault(f => f.Name == MetadataFetcherName) is null)
|
||||
return BadRequest();
|
||||
if(context.MetadataEntries.FirstOrDefault(e => e.MangaId == MangaId && e.MetadataFetcherName == MetadataFetcherName) is not { } entry)
|
||||
return StatusCode(Status412PreconditionFailed, "No entry found");
|
||||
|
||||
context.Remove(entry);
|
||||
|
||||
if(context.Sync() is { success: false } result)
|
||||
return StatusCode(Status500InternalServerError, result.exceptionMessage);
|
||||
return Ok();
|
||||
}
|
||||
}
|
@ -1,11 +1,11 @@
|
||||
using System.Text;
|
||||
using API.APIEndpointRecords;
|
||||
using API.Schema.Contexts;
|
||||
using API.Schema.NotificationConnectors;
|
||||
using API.Schema.NotificationsContext;
|
||||
using API.Schema.NotificationsContext.NotificationConnectors;
|
||||
using Asp.Versioning;
|
||||
using log4net;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using static Microsoft.AspNetCore.Http.StatusCodes;
|
||||
// ReSharper disable InconsistentNaming
|
||||
|
||||
namespace API.Controllers;
|
||||
|
||||
@ -13,121 +13,100 @@ namespace API.Controllers;
|
||||
[ApiController]
|
||||
[Produces("application/json")]
|
||||
[Route("v{v:apiVersion}/[controller]")]
|
||||
public class NotificationConnectorController(NotificationsContext context, ILog Log) : Controller
|
||||
public class NotificationConnectorController(NotificationsContext context) : Controller
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets all configured Notification-Connectors
|
||||
/// Gets all configured <see cref="NotificationConnector"/>
|
||||
/// </summary>
|
||||
/// <response code="200"></response>
|
||||
[HttpGet]
|
||||
[ProducesResponseType<NotificationConnector[]>(Status200OK, "application/json")]
|
||||
public IActionResult GetAllConnectors()
|
||||
{
|
||||
NotificationConnector[] ret = context.NotificationConnectors.ToArray();
|
||||
return Ok(ret);
|
||||
|
||||
return Ok(context.NotificationConnectors.ToArray());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns Notification-Connector with requested ID
|
||||
/// Returns <see cref="NotificationConnector"/> with requested Name
|
||||
/// </summary>
|
||||
/// <param name="NotificationConnectorId">Notification-Connector-ID</param>
|
||||
/// <param name="Name"><see cref="NotificationConnector"/>.Name</param>
|
||||
/// <response code="200"></response>
|
||||
/// <response code="404">NotificationConnector with ID not found</response>
|
||||
[HttpGet("{NotificationConnectorId}")]
|
||||
/// <response code="404"><see cref="NotificationConnector"/> with <paramref name="Name"/> not found</response>
|
||||
[HttpGet("{Name}")]
|
||||
[ProducesResponseType<NotificationConnector>(Status200OK, "application/json")]
|
||||
[ProducesResponseType(Status404NotFound)]
|
||||
public IActionResult GetConnector(string NotificationConnectorId)
|
||||
public IActionResult GetConnector(string Name)
|
||||
{
|
||||
NotificationConnector? ret = context.NotificationConnectors.Find(NotificationConnectorId);
|
||||
return (ret is not null) switch
|
||||
{
|
||||
true => Ok(ret),
|
||||
false => NotFound()
|
||||
};
|
||||
if(context.NotificationConnectors.Find(Name) is not { } connector)
|
||||
return NotFound();
|
||||
|
||||
return Ok(connector);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new REST-Notification-Connector
|
||||
/// 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>
|
||||
/// <param name="notificationConnector">Notification-Connector</param>
|
||||
/// <response code="201">ID of new connector</response>
|
||||
/// <response code="409">A NotificationConnector with name already exists</response>
|
||||
/// <response code="201"></response>
|
||||
/// <response code="500">Error during Database Operation</response>
|
||||
[HttpPut]
|
||||
[ProducesResponseType<string>(Status201Created, "application/json")]
|
||||
[ProducesResponseType(Status201Created)]
|
||||
[ProducesResponseType(Status409Conflict)]
|
||||
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
|
||||
public IActionResult CreateConnector([FromBody]NotificationConnector notificationConnector)
|
||||
{
|
||||
if (context.NotificationConnectors.Find(notificationConnector.Name) is not null)
|
||||
return Conflict();
|
||||
try
|
||||
{
|
||||
|
||||
context.NotificationConnectors.Add(notificationConnector);
|
||||
context.SaveChanges();
|
||||
return Created(notificationConnector.Name, notificationConnector);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e);
|
||||
return StatusCode(500, e.Message);
|
||||
}
|
||||
|
||||
if(context.Sync() is { success: false } result)
|
||||
return StatusCode(Status500InternalServerError, result.exceptionMessage);
|
||||
return Created();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new Gotify-Notification-Connector
|
||||
/// Creates a new Gotify-<see cref="NotificationConnector"/>
|
||||
/// </summary>
|
||||
/// <remarks>Priority needs to be between 0 and 10</remarks>
|
||||
/// <response code="201">ID of new connector</response>
|
||||
/// <response code="400"></response>
|
||||
/// <response code="409">A NotificationConnector with name already exists</response>
|
||||
/// <response code="201"></response>
|
||||
/// <response code="500">Error during Database Operation</response>
|
||||
[HttpPut("Gotify")]
|
||||
[ProducesResponseType<string>(Status201Created, "application/json")]
|
||||
[ProducesResponseType(Status400BadRequest)]
|
||||
[ProducesResponseType(Status409Conflict)]
|
||||
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
|
||||
public IActionResult CreateGotifyConnector([FromBody]GotifyRecord gotifyData)
|
||||
{
|
||||
if(!gotifyData.Validate())
|
||||
return BadRequest();
|
||||
//TODO Validate Data
|
||||
|
||||
NotificationConnector gotifyConnector = new NotificationConnector(TokenGen.CreateToken("Gotify"),
|
||||
gotifyData.endpoint,
|
||||
new Dictionary<string, string>() { { "X-Gotify-Key", gotifyData.appToken } },
|
||||
NotificationConnector gotifyConnector = new (gotifyData.Name,
|
||||
gotifyData.Endpoint,
|
||||
new Dictionary<string, string>() { { "X-Gotify-IDOnConnector", gotifyData.AppToken } },
|
||||
"POST",
|
||||
$"{{\"message\": \"%text\", \"title\": \"%title\", \"priority\": {gotifyData.priority}}}");
|
||||
$"{{\"message\": \"%text\", \"title\": \"%title\", \"Priority\": {gotifyData.Priority}}}");
|
||||
return CreateConnector(gotifyConnector);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new Ntfy-Notification-Connector
|
||||
/// Creates a new Ntfy-<see cref="NotificationConnector"/>
|
||||
/// </summary>
|
||||
/// <remarks>Priority needs to be between 1 and 5</remarks>
|
||||
/// <response code="201">ID of new connector</response>
|
||||
/// <response code="400"></response>
|
||||
/// <response code="409">A NotificationConnector with name already exists</response>
|
||||
/// <response code="201"></response>
|
||||
/// <response code="500">Error during Database Operation</response>
|
||||
[HttpPut("Ntfy")]
|
||||
[ProducesResponseType<string>(Status201Created, "application/json")]
|
||||
[ProducesResponseType(Status400BadRequest)]
|
||||
[ProducesResponseType(Status409Conflict)]
|
||||
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
|
||||
public IActionResult CreateNtfyConnector([FromBody]NtfyRecord ntfyRecord)
|
||||
{
|
||||
if(!ntfyRecord.Validate())
|
||||
return BadRequest();
|
||||
//TODO Validate Data
|
||||
|
||||
string authHeader = "Basic " + Convert.ToBase64String(Encoding.UTF8.GetBytes($"{ntfyRecord.username}:{ntfyRecord.password}"));
|
||||
string authHeader = "Basic " + Convert.ToBase64String(Encoding.UTF8.GetBytes($"{ntfyRecord.Username}:{ntfyRecord.Password}"));
|
||||
string auth = Convert.ToBase64String(Encoding.UTF8.GetBytes(authHeader)).Replace("=","");
|
||||
|
||||
NotificationConnector ntfyConnector = new (TokenGen.CreateToken("Ntfy"),
|
||||
$"{ntfyRecord.endpoint}?auth={auth}",
|
||||
NotificationConnector ntfyConnector = new (ntfyRecord.Name,
|
||||
$"{ntfyRecord.Endpoint}/{ntfyRecord.Topic}?auth={auth}",
|
||||
new Dictionary<string, string>()
|
||||
{
|
||||
{"Title", "%title"},
|
||||
{"Priority", ntfyRecord.priority.ToString()},
|
||||
{"Priority", ntfyRecord.Priority.ToString()},
|
||||
},
|
||||
"POST",
|
||||
"%text");
|
||||
@ -135,84 +114,46 @@ public class NotificationConnectorController(NotificationsContext context, ILog
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new Lunasea-Notification-Connector
|
||||
/// </summary>
|
||||
/// <remarks>https://docs.lunasea.app/lunasea/notifications/custom-notifications for id. Either device/:device_id or user/:user_id</remarks>
|
||||
/// <response code="201">ID of new connector</response>
|
||||
/// <response code="400"></response>
|
||||
/// <response code="409">A NotificationConnector with name already exists</response>
|
||||
/// <response code="500">Error during Database Operation</response>
|
||||
[HttpPut("Lunasea")]
|
||||
[ProducesResponseType<string>(Status201Created, "application/json")]
|
||||
[ProducesResponseType(Status400BadRequest)]
|
||||
[ProducesResponseType(Status409Conflict)]
|
||||
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
|
||||
public IActionResult CreateLunaseaConnector([FromBody]LunaseaRecord lunaseaRecord)
|
||||
{
|
||||
if(!lunaseaRecord.Validate())
|
||||
return BadRequest();
|
||||
|
||||
NotificationConnector lunaseaConnector = new (TokenGen.CreateToken("Lunasea"),
|
||||
$"https://notify.lunasea.app/v1/custom/{lunaseaRecord.id}",
|
||||
new Dictionary<string, string>(),
|
||||
"POST",
|
||||
"{\"title\": \"%title\", \"body\": \"%text\"}");
|
||||
return CreateConnector(lunaseaConnector);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new Pushover-Notification-Connector
|
||||
/// Creates a new Pushover-<see cref="NotificationConnector"/>
|
||||
/// </summary>
|
||||
/// <remarks>https://pushover.net/api</remarks>
|
||||
/// <response code="201">ID of new connector</response>
|
||||
/// <response code="400"></response>
|
||||
/// <response code="409">A NotificationConnector with name already exists</response>
|
||||
/// <response code="500">Error during Database Operation</response>
|
||||
[HttpPut("Pushover")]
|
||||
[ProducesResponseType<string>(Status201Created, "application/json")]
|
||||
[ProducesResponseType(Status400BadRequest)]
|
||||
[ProducesResponseType(Status409Conflict)]
|
||||
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
|
||||
public IActionResult CreatePushoverConnector([FromBody]PushoverRecord pushoverRecord)
|
||||
{
|
||||
if(!pushoverRecord.Validate())
|
||||
return BadRequest();
|
||||
//TODO Validate Data
|
||||
|
||||
NotificationConnector pushoverConnector = new (TokenGen.CreateToken("Pushover"),
|
||||
NotificationConnector pushoverConnector = new (pushoverRecord.Name,
|
||||
$"https://api.pushover.net/1/messages.json",
|
||||
new Dictionary<string, string>(),
|
||||
"POST",
|
||||
$"{{\"token\": \"{pushoverRecord.apptoken}\", \"user\": \"{pushoverRecord.user}\", \"message:\":\"%text\", \"%title\" }}");
|
||||
$"{{\"token\": \"{pushoverRecord.AppToken}\", \"user\": \"{pushoverRecord.User}\", \"message:\":\"%text\", \"%title\" }}");
|
||||
return CreateConnector(pushoverConnector);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes the Notification-Connector with the requested ID
|
||||
/// Deletes the <see cref="NotificationConnector"/> with the requested Name
|
||||
/// </summary>
|
||||
/// <param name="NotificationConnectorId">Notification-Connector-ID</param>
|
||||
/// <param name="Name"><see cref="NotificationConnector"/>.Name</param>
|
||||
/// <response code="200"></response>
|
||||
/// <response code="404">NotificationConnector with ID not found</response>
|
||||
/// <response code="404"><see cref="NotificationConnector"/> with Name not found</response>
|
||||
/// <response code="500">Error during Database Operation</response>
|
||||
[HttpDelete("{NotificationConnectorId}")]
|
||||
[HttpDelete("{Name}")]
|
||||
[ProducesResponseType(Status200OK)]
|
||||
[ProducesResponseType(Status404NotFound)]
|
||||
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
|
||||
public IActionResult DeleteConnector(string NotificationConnectorId)
|
||||
public IActionResult DeleteConnector(string Name)
|
||||
{
|
||||
try
|
||||
{
|
||||
NotificationConnector? ret = context.NotificationConnectors.Find(NotificationConnectorId);
|
||||
if(ret is null)
|
||||
if(context.NotificationConnectors.Find(Name) is not { } connector)
|
||||
return NotFound();
|
||||
|
||||
context.Remove(ret);
|
||||
context.SaveChanges();
|
||||
return Ok();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e);
|
||||
return StatusCode(500, e.Message);
|
||||
}
|
||||
context.NotificationConnectors.Remove(connector);
|
||||
|
||||
if(context.Sync() is { success: false } result)
|
||||
return StatusCode(Status500InternalServerError, result.exceptionMessage);
|
||||
return Created();
|
||||
}
|
||||
}
|
@ -1,7 +1,5 @@
|
||||
using API.Schema;
|
||||
using API.Schema.Contexts;
|
||||
using API.Schema.MangaContext;
|
||||
using Asp.Versioning;
|
||||
using log4net;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using static Microsoft.AspNetCore.Http.StatusCodes;
|
||||
// ReSharper disable InconsistentNaming
|
||||
@ -11,102 +9,70 @@ namespace API.Controllers;
|
||||
[ApiVersion(2)]
|
||||
[ApiController]
|
||||
[Route("v{v:apiVersion}/[controller]")]
|
||||
public class QueryController(PgsqlContext context, ILog Log) : Controller
|
||||
public class QueryController(MangaContext context) : Controller
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns the Author-Information for Author-ID
|
||||
/// Returns the <see cref="Author"/> with <paramref name="AuthorId"/>
|
||||
/// </summary>
|
||||
/// <param name="AuthorId">Author-Id</param>
|
||||
/// <param name="AuthorId"><see cref="Author"/>.Key</param>
|
||||
/// <response code="200"></response>
|
||||
/// <response code="404">Author with ID not found</response>
|
||||
/// <response code="404"><see cref="Author"/> with <paramref name="AuthorId"/> not found</response>
|
||||
[HttpGet("Author/{AuthorId}")]
|
||||
[ProducesResponseType<Author>(Status200OK, "application/json")]
|
||||
[ProducesResponseType(Status404NotFound)]
|
||||
public IActionResult GetAuthor(string AuthorId)
|
||||
{
|
||||
Author? ret = context.Authors.Find(AuthorId);
|
||||
if (ret is null)
|
||||
if (context.Authors.Find(AuthorId) is not { } author)
|
||||
return NotFound();
|
||||
return Ok(ret);
|
||||
|
||||
return Ok(author);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns all Mangas which where Authored by Author with AuthorId
|
||||
/// Returns all <see cref="Manga"/> which where Authored by <see cref="Author"/> with <paramref name="AuthorId"/>
|
||||
/// </summary>
|
||||
/// <param name="AuthorId">Author-ID</param>
|
||||
/// <param name="AuthorId"><see cref="Author"/>.Key</param>
|
||||
/// <response code="200"></response>
|
||||
/// <response code="404">Author not found</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 { } a)
|
||||
if (context.Authors.Find(AuthorId) is not { } author)
|
||||
return NotFound();
|
||||
return Ok(context.Mangas.Where(m => m.Authors.Contains(a)));
|
||||
}
|
||||
/*
|
||||
/// <summary>
|
||||
/// Returns Link-Information for Link-Id
|
||||
/// </summary>
|
||||
/// <param name="LinkId"></param>
|
||||
/// <response code="200"></response>
|
||||
/// <response code="404">Link with ID not found</response>
|
||||
[HttpGet("Link/{LinkId}")]
|
||||
[ProducesResponseType<Link>(Status200OK, "application/json")]
|
||||
[ProducesResponseType(Status404NotFound)]
|
||||
public IActionResult GetLink(string LinkId)
|
||||
{
|
||||
Link? ret = context.Links.Find(LinkId);
|
||||
if (ret is null)
|
||||
return NotFound();
|
||||
return Ok(ret);
|
||||
|
||||
return Ok(context.Mangas.Where(m => m.Authors.Contains(author)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns AltTitle-Information for AltTitle-Id
|
||||
/// Returns all <see cref="Manga"/> with <see cref="Tag"/>
|
||||
/// </summary>
|
||||
/// <param name="AltTitleId"></param>
|
||||
/// <param name="Tag"><see cref="Tag"/>.Tag</param>
|
||||
/// <response code="200"></response>
|
||||
/// <response code="404">AltTitle with ID not found</response>
|
||||
[HttpGet("AltTitle/{AltTitleId}")]
|
||||
[ProducesResponseType<MangaAltTitle>(Status200OK, "application/json")]
|
||||
[ProducesResponseType(Status404NotFound)]
|
||||
public IActionResult GetAltTitle(string AltTitleId)
|
||||
{
|
||||
MangaAltTitle? ret = context.AltTitles.Find(AltTitleId);
|
||||
if (ret is null)
|
||||
return NotFound();
|
||||
return Ok(ret);
|
||||
}*/
|
||||
|
||||
/// <summary>
|
||||
/// Returns all Manga with Tag
|
||||
/// </summary>
|
||||
/// <param name="Tag"></param>
|
||||
/// <response code="200"></response>
|
||||
/// <response code="404">Tag not found</response>
|
||||
/// <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 { } t)
|
||||
if (context.Tags.Find(Tag) is not { } tag)
|
||||
return NotFound();
|
||||
return Ok(context.Mangas.Where(m => m.MangaTags.Contains(t)));
|
||||
|
||||
return Ok(context.Mangas.Where(m => m.MangaTags.Contains(tag)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns Chapter-Information for Chapter-Id
|
||||
/// Returns <see cref="Chapter"/> with <paramref name="ChapterId"/>
|
||||
/// </summary>
|
||||
/// <param name="ChapterId"></param>
|
||||
/// <param name="ChapterId"><see cref="Chapter"/>.Key</param>
|
||||
/// <response code="200"></response>
|
||||
/// <response code="404">Chapter with ID not found</response>
|
||||
/// <response code="404"><see cref="Chapter"/> with <paramref name="ChapterId"/> not found</response>
|
||||
[HttpGet("Chapter/{ChapterId}")]
|
||||
[ProducesResponseType<Chapter>(Status200OK, "application/json")]
|
||||
public IActionResult GetChapter(string ChapterId)
|
||||
{
|
||||
Chapter? ret = context.Chapters.Find(ChapterId);
|
||||
if (ret is null)
|
||||
if (context.Chapters.Find(ChapterId) is not { } chapter)
|
||||
return NotFound();
|
||||
return Ok(ret);
|
||||
|
||||
return Ok(chapter);
|
||||
}
|
||||
}
|
@ -1,11 +1,7 @@
|
||||
using API.Schema;
|
||||
using API.Schema.Contexts;
|
||||
using API.Schema.Jobs;
|
||||
using API.Schema.MangaConnectors;
|
||||
using API.Schema.MangaContext;
|
||||
using API.Schema.MangaContext.MangaConnectors;
|
||||
using Asp.Versioning;
|
||||
using log4net;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using static Microsoft.AspNetCore.Http.StatusCodes;
|
||||
// ReSharper disable InconsistentNaming
|
||||
|
||||
@ -14,136 +10,61 @@ namespace API.Controllers;
|
||||
[ApiVersion(2)]
|
||||
[ApiController]
|
||||
[Route("v{v:apiVersion}/[controller]")]
|
||||
public class SearchController(PgsqlContext context, ILog Log) : Controller
|
||||
public class SearchController(MangaContext context) : Controller
|
||||
{
|
||||
/// <summary>
|
||||
/// Initiate a search for a Manga on a specific Connector
|
||||
/// Initiate a search for a <see cref="Manga"/> on <see cref="MangaConnector"/> with searchTerm
|
||||
/// </summary>
|
||||
/// <param name="MangaConnectorName"></param>
|
||||
/// <param name="Query"></param>
|
||||
/// <param name="MangaConnectorName"><see cref="MangaConnector"/>.Name</param>
|
||||
/// <param name="Query">searchTerm</param>
|
||||
/// <response code="200"></response>
|
||||
/// <response code="404">MangaConnector with ID not found</response>
|
||||
/// <response code="406">MangaConnector with ID is disabled</response>
|
||||
/// <response code="500">Error during Database Operation</response>
|
||||
[HttpPost("{MangaConnectorName}/{Query}")]
|
||||
/// <response code="404"><see cref="MangaConnector"/> with Name not found</response>
|
||||
/// <response code="412"><see cref="MangaConnector"/> with Name is disabled</response>
|
||||
[HttpGet("{MangaConnectorName}/{Query}")]
|
||||
[ProducesResponseType<Manga[]>(Status200OK, "application/json")]
|
||||
[ProducesResponseType(Status404NotFound)]
|
||||
[ProducesResponseType(Status406NotAcceptable)]
|
||||
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
|
||||
public IActionResult SearchManga(string MangaConnectorName, string Query)
|
||||
{
|
||||
if(context.MangaConnectors.Find(MangaConnectorName) is not { } connector)
|
||||
return NotFound();
|
||||
else if (connector.Enabled is false)
|
||||
return StatusCode(Status406NotAcceptable);
|
||||
if (connector.Enabled is false)
|
||||
return StatusCode(Status412PreconditionFailed);
|
||||
|
||||
Manga[] mangas = connector.SearchManga(Query);
|
||||
(Manga, MangaConnectorId<Manga>)[] mangas = connector.SearchManga(Query);
|
||||
List<Manga> retMangas = new();
|
||||
foreach (Manga manga in mangas)
|
||||
foreach ((Manga manga, MangaConnectorId<Manga> mcId) manga in mangas)
|
||||
{
|
||||
try
|
||||
{
|
||||
if(AddMangaToContext(manga) is { } add)
|
||||
if(Tranga.AddMangaToContext(manga, context, out Manga? add))
|
||||
retMangas.Add(add);
|
||||
}
|
||||
catch (DbUpdateException e)
|
||||
{
|
||||
Log.Error(e);
|
||||
return StatusCode(Status500InternalServerError, e.Message);
|
||||
}
|
||||
}
|
||||
|
||||
return Ok(retMangas.ToArray());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns Manga from MangaConnector associated with URL
|
||||
/// Returns <see cref="Manga"/> from the <see cref="MangaConnector"/> associated with <paramref name="url"/>
|
||||
/// </summary>
|
||||
/// <param name="url">Manga-Page URL</param>
|
||||
/// <param name="url"></param>
|
||||
/// <response code="200"></response>
|
||||
/// <response code="300">Multiple connectors found for URL</response>
|
||||
/// <response code="400">No Manga at URL</response>
|
||||
/// <response code="404">No connector found for URL</response>
|
||||
/// <response code="300">Multiple <see cref="MangaConnector"/> found for URL</response>
|
||||
/// <response code="404"><see cref="Manga"/> not found</response>
|
||||
/// <response code="500">Error during Database Operation</response>
|
||||
[HttpPost("Url")]
|
||||
[ProducesResponseType<Manga>(Status200OK, "application/json")]
|
||||
[ProducesResponseType(Status300MultipleChoices)]
|
||||
[ProducesResponseType(Status400BadRequest)]
|
||||
[ProducesResponseType(Status404NotFound)]
|
||||
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
|
||||
[ProducesResponseType(Status500InternalServerError)]
|
||||
public IActionResult GetMangaFromUrl([FromBody]string url)
|
||||
{
|
||||
List<MangaConnector> connectors = context.MangaConnectors.AsEnumerable().Where(c => c.UrlMatchesConnector(url)).ToList();
|
||||
if (connectors.Count == 0)
|
||||
if (context.MangaConnectors.Find("Global") is not { } connector)
|
||||
return StatusCode(Status500InternalServerError, "Could not find Global Connector.");
|
||||
|
||||
if(connector.GetMangaFromUrl(url) is not { } manga)
|
||||
return NotFound();
|
||||
else if (connectors.Count > 1)
|
||||
return StatusCode(Status300MultipleChoices);
|
||||
|
||||
if(connectors.First().GetMangaFromUrl(url) is not { } manga)
|
||||
return BadRequest();
|
||||
try
|
||||
{
|
||||
if(AddMangaToContext(manga) is { } add)
|
||||
if(Tranga.AddMangaToContext(manga, context, out Manga? add) == false)
|
||||
return StatusCode(Status500InternalServerError);
|
||||
|
||||
return Ok(add);
|
||||
return StatusCode(500);
|
||||
}
|
||||
catch (DbUpdateException e)
|
||||
{
|
||||
Log.Error(e);
|
||||
return StatusCode(500, e.Message);
|
||||
}
|
||||
}
|
||||
|
||||
private Manga? AddMangaToContext(Manga manga)
|
||||
{
|
||||
Manga? existing = context.Mangas.Find(manga.MangaId);
|
||||
|
||||
IEnumerable<MangaTag> mergedTags = manga.MangaTags.Select(mt =>
|
||||
{
|
||||
MangaTag? inDb = context.Tags.Find(mt.Tag);
|
||||
return inDb ?? mt;
|
||||
});
|
||||
manga.MangaTags = mergedTags.ToList();
|
||||
|
||||
IEnumerable<Author> mergedAuthors = manga.Authors.Select(ma =>
|
||||
{
|
||||
Author? inDb = context.Authors.Find(ma.AuthorId);
|
||||
return inDb ?? ma;
|
||||
});
|
||||
manga.Authors = mergedAuthors.ToList();
|
||||
|
||||
/*
|
||||
IEnumerable<Link> mergedLinks = manga.Links.Select(ml =>
|
||||
{
|
||||
Link? inDb = context.Links.Find(ml.LinkId);
|
||||
return inDb ?? ml;
|
||||
});
|
||||
manga.Links = mergedLinks.ToList();
|
||||
|
||||
IEnumerable<MangaAltTitle> mergedAltTitles = manga.AltTitles.Select(mat =>
|
||||
{
|
||||
MangaAltTitle? inDb = context.AltTitles.Find(mat.AltTitleId);
|
||||
return inDb ?? mat;
|
||||
});
|
||||
manga.AltTitles = mergedAltTitles.ToList();
|
||||
*/
|
||||
try
|
||||
{
|
||||
|
||||
if (context.Mangas.Find(manga.MangaId) is { } r)
|
||||
{
|
||||
context.Mangas.Remove(r);
|
||||
context.SaveChanges();
|
||||
}
|
||||
context.Mangas.Add(manga);
|
||||
context.Jobs.Add(new DownloadMangaCoverJob(manga));
|
||||
context.SaveChanges();
|
||||
}
|
||||
catch (DbUpdateException e)
|
||||
{
|
||||
Log.Error(e);
|
||||
return null;
|
||||
}
|
||||
return manga;
|
||||
}
|
||||
}
|
@ -1,29 +1,25 @@
|
||||
using API.MangaDownloadClients;
|
||||
using API.Schema;
|
||||
using API.Schema.Contexts;
|
||||
using API.Schema.Jobs;
|
||||
using Asp.Versioning;
|
||||
using log4net;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using static Microsoft.AspNetCore.Http.StatusCodes;
|
||||
// ReSharper disable InconsistentNaming
|
||||
|
||||
namespace API.Controllers;
|
||||
|
||||
[ApiVersion(2)]
|
||||
[ApiController]
|
||||
[Route("v{v:apiVersion}/[controller]")]
|
||||
public class SettingsController(PgsqlContext context, ILog Log) : Controller
|
||||
public class SettingsController() : Controller
|
||||
{
|
||||
/// <summary>
|
||||
/// Get all Settings
|
||||
/// Get all <see cref="Tranga.Settings"/>
|
||||
/// </summary>
|
||||
/// <response code="200"></response>
|
||||
[HttpGet]
|
||||
[ProducesResponseType<JObject>(Status200OK, "application/json")]
|
||||
[ProducesResponseType<TrangaSettings>(Status200OK, "application/json")]
|
||||
public IActionResult GetSettings()
|
||||
{
|
||||
return Ok(JObject.Parse(TrangaSettings.Serialize()));
|
||||
return Ok(Tranga.Settings);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -34,7 +30,7 @@ public class SettingsController(PgsqlContext context, ILog Log) : Controller
|
||||
[ProducesResponseType<string>(Status200OK, "text/plain")]
|
||||
public IActionResult GetUserAgent()
|
||||
{
|
||||
return Ok(TrangaSettings.userAgent);
|
||||
return Ok(Tranga.Settings.UserAgent);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -45,7 +41,8 @@ public class SettingsController(PgsqlContext context, ILog Log) : Controller
|
||||
[ProducesResponseType(Status200OK)]
|
||||
public IActionResult SetUserAgent([FromBody]string userAgent)
|
||||
{
|
||||
TrangaSettings.UpdateUserAgent(userAgent);
|
||||
//TODO Validate
|
||||
Tranga.Settings.SetUserAgent(userAgent);
|
||||
return Ok();
|
||||
}
|
||||
|
||||
@ -57,7 +54,7 @@ public class SettingsController(PgsqlContext context, ILog Log) : Controller
|
||||
[ProducesResponseType(Status200OK)]
|
||||
public IActionResult ResetUserAgent()
|
||||
{
|
||||
TrangaSettings.UpdateUserAgent(TrangaSettings.DefaultUserAgent);
|
||||
Tranga.Settings.SetUserAgent(TrangaSettings.DefaultUserAgent);
|
||||
return Ok();
|
||||
}
|
||||
|
||||
@ -69,7 +66,7 @@ public class SettingsController(PgsqlContext context, ILog Log) : Controller
|
||||
[ProducesResponseType<Dictionary<RequestType,int>>(Status200OK, "application/json")]
|
||||
public IActionResult GetRequestLimits()
|
||||
{
|
||||
return Ok(TrangaSettings.requestLimits);
|
||||
return Ok(Tranga.Settings.RequestLimits);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -97,7 +94,7 @@ public class SettingsController(PgsqlContext context, ILog Log) : Controller
|
||||
{
|
||||
if (requestLimit <= 0)
|
||||
return BadRequest();
|
||||
TrangaSettings.UpdateRequestLimit(RequestType, requestLimit);
|
||||
Tranga.Settings.SetRequestLimit(RequestType, requestLimit);
|
||||
return Ok();
|
||||
}
|
||||
|
||||
@ -109,7 +106,7 @@ public class SettingsController(PgsqlContext context, ILog Log) : Controller
|
||||
[ProducesResponseType<string>(Status200OK)]
|
||||
public IActionResult ResetRequestLimits(RequestType RequestType)
|
||||
{
|
||||
TrangaSettings.UpdateRequestLimit(RequestType, TrangaSettings.DefaultRequestLimits[RequestType]);
|
||||
Tranga.Settings.SetRequestLimit(RequestType, TrangaSettings.DefaultRequestLimits[RequestType]);
|
||||
return Ok();
|
||||
}
|
||||
|
||||
@ -121,35 +118,35 @@ public class SettingsController(PgsqlContext context, ILog Log) : Controller
|
||||
[ProducesResponseType<string>(Status200OK)]
|
||||
public IActionResult ResetRequestLimits()
|
||||
{
|
||||
TrangaSettings.ResetRequestLimits();
|
||||
Tranga.Settings.ResetRequestLimits();
|
||||
return Ok();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns Level of Image-Compression for Images
|
||||
/// </summary>
|
||||
/// <response code="200">JPEG compression-level as Integer</response>
|
||||
[HttpGet("ImageCompression")]
|
||||
/// <response code="200">JPEG ImageCompression-level as Integer</response>
|
||||
[HttpGet("ImageCompressionLevel")]
|
||||
[ProducesResponseType<int>(Status200OK, "text/plain")]
|
||||
public IActionResult GetImageCompression()
|
||||
{
|
||||
return Ok(TrangaSettings.compression);
|
||||
return Ok(Tranga.Settings.ImageCompression);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set the Image-Compression-Level for Images
|
||||
/// </summary>
|
||||
/// <param name="level">100 to disable, 0-99 for JPEG compression-Level</param>
|
||||
/// <param name="level">100 to disable, 0-99 for JPEG ImageCompression-Level</param>
|
||||
/// <response code="200"></response>
|
||||
/// <response code="400">Level outside permitted range</response>
|
||||
[HttpPatch("ImageCompression")]
|
||||
[HttpPatch("ImageCompressionLevel/{level}")]
|
||||
[ProducesResponseType(Status200OK)]
|
||||
[ProducesResponseType(Status400BadRequest)]
|
||||
public IActionResult SetImageCompression([FromBody]int level)
|
||||
public IActionResult SetImageCompression(int level)
|
||||
{
|
||||
if (level < 1 || level > 100)
|
||||
return BadRequest();
|
||||
TrangaSettings.UpdateCompressImages(level);
|
||||
Tranga.Settings.UpdateImageCompression(level);
|
||||
return Ok();
|
||||
}
|
||||
|
||||
@ -161,7 +158,7 @@ public class SettingsController(PgsqlContext context, ILog Log) : Controller
|
||||
[ProducesResponseType<bool>(Status200OK, "text/plain")]
|
||||
public IActionResult GetBwImagesToggle()
|
||||
{
|
||||
return Ok(TrangaSettings.bwImages);
|
||||
return Ok(Tranga.Settings.BlackWhiteImages);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -169,37 +166,11 @@ public class SettingsController(PgsqlContext context, ILog Log) : Controller
|
||||
/// </summary>
|
||||
/// <param name="enabled">true to enable</param>
|
||||
/// <response code="200"></response>
|
||||
[HttpPatch("BWImages")]
|
||||
[HttpPatch("BWImages/{enabled}")]
|
||||
[ProducesResponseType(Status200OK)]
|
||||
public IActionResult SetBwImagesToggle([FromBody]bool enabled)
|
||||
public IActionResult SetBwImagesToggle(bool enabled)
|
||||
{
|
||||
TrangaSettings.UpdateBwImages(enabled);
|
||||
return Ok();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get state of April Fools Mode
|
||||
/// </summary>
|
||||
/// <remarks>April Fools Mode disables all downloads on April 1st</remarks>
|
||||
/// <response code="200">True if enabled</response>
|
||||
[HttpGet("AprilFoolsMode")]
|
||||
[ProducesResponseType<bool>(Status200OK, "text/plain")]
|
||||
public IActionResult GetAprilFoolsMode()
|
||||
{
|
||||
return Ok(TrangaSettings.aprilFoolsMode);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enable/Disable April Fools Mode
|
||||
/// </summary>
|
||||
/// <remarks>April Fools Mode disables all downloads on April 1st</remarks>
|
||||
/// <param name="enabled">true to enable</param>
|
||||
/// <response code="200"></response>
|
||||
[HttpPatch("AprilFoolsMode")]
|
||||
[ProducesResponseType(Status200OK)]
|
||||
public IActionResult SetAprilFoolsMode([FromBody]bool enabled)
|
||||
{
|
||||
TrangaSettings.UpdateAprilFoolsMode(enabled);
|
||||
Tranga.Settings.SetBlackWhiteImageEnabled(enabled);
|
||||
return Ok();
|
||||
}
|
||||
|
||||
@ -208,14 +179,14 @@ public class SettingsController(PgsqlContext context, ILog Log) : Controller
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Placeholders:
|
||||
/// %M Manga Name
|
||||
/// %M Obj Name
|
||||
/// %V Volume
|
||||
/// %C Chapter
|
||||
/// %T Title
|
||||
/// %A Author (first in list)
|
||||
/// %I Chapter Internal ID
|
||||
/// %i Manga Internal ID
|
||||
/// %Y Year (Manga)
|
||||
/// %i Obj Internal ID
|
||||
/// %Y Year (Obj)
|
||||
///
|
||||
/// ?_(...) replace _ with a value from above:
|
||||
/// Everything inside the braces will only be added if the value of %_ is not null
|
||||
@ -225,7 +196,7 @@ public class SettingsController(PgsqlContext context, ILog Log) : Controller
|
||||
[ProducesResponseType<string>(Status200OK, "text/plain")]
|
||||
public IActionResult GetCustomNamingScheme()
|
||||
{
|
||||
return Ok(TrangaSettings.chapterNamingScheme);
|
||||
return Ok(Tranga.Settings.ChapterNamingScheme);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -233,38 +204,89 @@ public class SettingsController(PgsqlContext context, ILog Log) : Controller
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Placeholders:
|
||||
/// %M Manga Name
|
||||
/// %M Obj Name
|
||||
/// %V Volume
|
||||
/// %C Chapter
|
||||
/// %T Title
|
||||
/// %A Author (first in list)
|
||||
/// %I Chapter Internal ID
|
||||
/// %i Manga Internal ID
|
||||
/// %Y Year (Manga)
|
||||
/// %Y Year (Obj)
|
||||
///
|
||||
/// ?_(...) replace _ with a value from above:
|
||||
/// Everything inside the braces will only be added if the value of %_ is not null
|
||||
/// </remarks>
|
||||
/// <response code="200"></response>
|
||||
/// <response code="500">Error during Database Operation</response>
|
||||
[HttpPatch("ChapterNamingScheme")]
|
||||
[ProducesResponseType(Status200OK)]
|
||||
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
|
||||
public IActionResult SetCustomNamingScheme([FromBody]string namingScheme)
|
||||
{
|
||||
try
|
||||
{
|
||||
Dictionary<Chapter, string> oldPaths = context.Chapters.ToDictionary(c => c, c => c.FullArchiveFilePath);
|
||||
TrangaSettings.UpdateChapterNamingScheme(namingScheme);
|
||||
MoveFileOrFolderJob[] newJobs = oldPaths
|
||||
.Select(kv => new MoveFileOrFolderJob(kv.Value, kv.Key.FullArchiveFilePath)).ToArray();
|
||||
context.Jobs.AddRange(newJobs);
|
||||
//TODO Move old Chapters
|
||||
Tranga.Settings.SetChapterNamingScheme(namingScheme);
|
||||
|
||||
return Ok();
|
||||
}
|
||||
catch (Exception e)
|
||||
|
||||
/// <summary>
|
||||
/// Sets the FlareSolverr-URL
|
||||
/// </summary>
|
||||
/// <param name="flareSolverrUrl">URL of FlareSolverr-Instance</param>
|
||||
/// <response code="200"></response>
|
||||
[HttpPost("FlareSolverr/Url")]
|
||||
[ProducesResponseType(Status200OK)]
|
||||
public IActionResult SetFlareSolverrUrl([FromBody]string flareSolverrUrl)
|
||||
{
|
||||
Log.Error(e);
|
||||
return StatusCode(500, e);
|
||||
Tranga.Settings.SetFlareSolverrUrl(flareSolverrUrl);
|
||||
return Ok();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resets the FlareSolverr-URL (HttpClient does not use FlareSolverr anymore)
|
||||
/// </summary>
|
||||
/// <response code="200"></response>
|
||||
[HttpDelete("FlareSolverr/Url")]
|
||||
[ProducesResponseType(Status200OK)]
|
||||
public IActionResult ClearFlareSolverrUrl()
|
||||
{
|
||||
Tranga.Settings.SetFlareSolverrUrl(string.Empty);
|
||||
return Ok();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test FlareSolverr
|
||||
/// </summary>
|
||||
/// <response code="200">FlareSolverr is working!</response>
|
||||
/// <response code="500">FlareSolverr is not working</response>
|
||||
[HttpPost("FlareSolverr/Test")]
|
||||
[ProducesResponseType(Status200OK)]
|
||||
[ProducesResponseType(Status500InternalServerError)]
|
||||
public IActionResult TestFlareSolverrReachable()
|
||||
{
|
||||
const string knownProtectedUrl = "https://prowlarr.servarr.com/v1/ping";
|
||||
FlareSolverrDownloadClient client = new();
|
||||
RequestResult result = client.MakeRequestInternal(knownProtectedUrl);
|
||||
return (int)result.statusCode >= 200 && (int)result.statusCode < 300 ? Ok() : StatusCode(500, result.statusCode);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the language in which Manga are downloaded
|
||||
/// </summary>
|
||||
/// <response code="200"></response>
|
||||
[HttpGet("DownloadLanguage")]
|
||||
[ProducesResponseType<string>(Status200OK, "text/plain")]
|
||||
public IActionResult GetDownloadLanguage()
|
||||
{
|
||||
return Ok(Tranga.Settings.DownloadLanguage);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the language in which Manga are downloaded
|
||||
/// </summary>
|
||||
/// <response code="200"></response>
|
||||
[HttpPatch("DownloadLanguage/{Language}")]
|
||||
[ProducesResponseType(Status200OK)]
|
||||
public IActionResult SetDownloadLanguage(string Language)
|
||||
{
|
||||
//TODO Validation
|
||||
Tranga.Settings.SetDownloadLanguage(Language);
|
||||
return Ok();
|
||||
}
|
||||
}
|
154
API/Controllers/WorkerController.cs
Normal file
154
API/Controllers/WorkerController.cs
Normal file
@ -0,0 +1,154 @@
|
||||
using API.APIEndpointRecords;
|
||||
using API.Workers;
|
||||
using Asp.Versioning;
|
||||
using log4net;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using static Microsoft.AspNetCore.Http.StatusCodes;
|
||||
// ReSharper disable InconsistentNaming
|
||||
|
||||
namespace API.Controllers;
|
||||
|
||||
[ApiVersion(2)]
|
||||
[ApiController]
|
||||
[Route("v{version:apiVersion}/[controller]")]
|
||||
public class WorkerController() : Controller
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns all <see cref="BaseWorker"/>
|
||||
/// </summary>
|
||||
/// <response code="200"></response>
|
||||
[HttpGet]
|
||||
[ProducesResponseType<BaseWorker[]>(Status200OK, "application/json")]
|
||||
public IActionResult GetAllWorkers()
|
||||
{
|
||||
return Ok(Tranga.AllWorkers.ToArray());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns <see cref="BaseWorker"/> with requested <paramref name="WorkerIds"/>
|
||||
/// </summary>
|
||||
/// <param name="WorkerIds">Array of <see cref="BaseWorker"/>.Key</param>
|
||||
/// <response code="200"></response>
|
||||
[HttpPost("WithIDs")]
|
||||
[ProducesResponseType<BaseWorker[]>(Status200OK, "application/json")]
|
||||
public IActionResult GetJobs([FromBody]string[] WorkerIds)
|
||||
{
|
||||
return Ok(Tranga.AllWorkers.Where(worker => WorkerIds.Contains(worker.Key)).ToArray());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get all <see cref="BaseWorker"/> in requested <see cref="WorkerExecutionState"/>
|
||||
/// </summary>
|
||||
/// <param name="State">Requested <see cref="WorkerExecutionState"/></param>
|
||||
/// <response code="200"></response>
|
||||
[HttpGet("State/{State}")]
|
||||
[ProducesResponseType<BaseWorker[]>(Status200OK, "application/json")]
|
||||
public IActionResult GetJobsInState(WorkerExecutionState State)
|
||||
{
|
||||
return Ok(Tranga.AllWorkers.Where(worker => worker.State == State).ToArray());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Return <see cref="BaseWorker"/> with <paramref name="WorkerId"/>
|
||||
/// </summary>
|
||||
/// <param name="WorkerId"><see cref="BaseWorker"/>.Key</param>
|
||||
/// <response code="200"></response>
|
||||
/// <response code="404"><see cref="BaseWorker"/> with <paramref name="WorkerId"/> could not be found</response>
|
||||
[HttpGet("{WorkerId}")]
|
||||
[ProducesResponseType<BaseWorker>(Status200OK, "application/json")]
|
||||
[ProducesResponseType(Status404NotFound)]
|
||||
public IActionResult GetJob(string WorkerId)
|
||||
{
|
||||
if(Tranga.AllWorkers.FirstOrDefault(w => w.Key == WorkerId) is not { } worker)
|
||||
return NotFound(nameof(WorkerId));
|
||||
return Ok(worker);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Delete <see cref="BaseWorker"/> with <paramref name="WorkerId"/> and all child-<see cref="BaseWorker"/>s
|
||||
/// </summary>
|
||||
/// <param name="WorkerId"><see cref="BaseWorker"/>.Key</param>
|
||||
/// <response code="200"></response>
|
||||
/// <response code="404"><see cref="BaseWorker"/> with <paramref name="WorkerId"/> could not be found</response>
|
||||
[HttpDelete("{WorkerId}")]
|
||||
[ProducesResponseType(Status200OK)]
|
||||
[ProducesResponseType(Status404NotFound)]
|
||||
public IActionResult DeleteJob(string WorkerId)
|
||||
{
|
||||
if(Tranga.AllWorkers.FirstOrDefault(w => w.Key == WorkerId) is not { } worker)
|
||||
return NotFound(nameof(WorkerId));
|
||||
Tranga.RemoveWorker(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>
|
||||
/// <param name="WorkerId"><see cref="BaseWorker"/>.Key</param>
|
||||
/// <response code="200"></response>
|
||||
/// <response code="404"><see cref="BaseWorker"/> with <paramref name="WorkerId"/> could not be found</response>
|
||||
/// <response code="412"><see cref="BaseWorker"/> was already running</response>
|
||||
[HttpPost("{WorkerId}/Start")]
|
||||
[ProducesResponseType(Status202Accepted)]
|
||||
[ProducesResponseType(Status404NotFound)]
|
||||
[ProducesResponseType<string>(Status412PreconditionFailed, "text/plain")]
|
||||
public IActionResult StartJob(string WorkerId)
|
||||
{
|
||||
if(Tranga.AllWorkers.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);
|
||||
return Ok();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stops <see cref="BaseWorker"/> with <paramref name="WorkerId"/>
|
||||
/// </summary>
|
||||
/// <param name="WorkerId"><see cref="BaseWorker"/>.Key</param>
|
||||
/// <response code="200"></response>
|
||||
/// <response code="404"><see cref="BaseWorker"/> with <paramref name="WorkerId"/> could not be found</response>
|
||||
/// <response code="208"><see cref="BaseWorker"/> was not running</response>
|
||||
[HttpPost("{WorkerId}/Stop")]
|
||||
[ProducesResponseType(Status501NotImplemented)]
|
||||
public IActionResult StopJob(string WorkerId)
|
||||
{
|
||||
if(Tranga.AllWorkers.FirstOrDefault(w => w.Key == WorkerId) is not { } worker)
|
||||
return NotFound(nameof(WorkerId));
|
||||
|
||||
if(worker.State is < WorkerExecutionState.Running or >= WorkerExecutionState.Completed)
|
||||
return StatusCode(Status208AlreadyReported, "Not running");
|
||||
|
||||
Tranga.StopWorker(worker);
|
||||
return Ok();
|
||||
}
|
||||
}
|
@ -43,7 +43,7 @@ internal class ChromiumDownloadClient : DownloadClient
|
||||
{
|
||||
Thread.Sleep(TimeSpan.FromHours(1));
|
||||
Log.Debug("Removing stale pages");
|
||||
foreach ((IPage? key, DateTime value) in _openPages.Where(kv => kv.Value.Subtract(DateTime.Now) > TimeSpan.FromHours(1)))
|
||||
foreach ((IPage key, DateTime _) in _openPages.Where(kv => kv.Value.Subtract(DateTime.Now) > TimeSpan.FromHours(1)))
|
||||
{
|
||||
Log.Debug($"Closing {key.Url}");
|
||||
key.CloseAsync().Wait();
|
||||
|
@ -3,7 +3,7 @@ using log4net;
|
||||
|
||||
namespace API.MangaDownloadClients;
|
||||
|
||||
internal abstract class DownloadClient
|
||||
public abstract class DownloadClient
|
||||
{
|
||||
private static readonly Dictionary<RequestType, DateTime> LastExecutedRateLimit = new();
|
||||
protected ILog Log { get; init; }
|
||||
@ -15,22 +15,22 @@ internal abstract class DownloadClient
|
||||
|
||||
public RequestResult MakeRequest(string url, RequestType requestType, string? referrer = null, string? clickButton = null)
|
||||
{
|
||||
Log.Debug($"Requesting {url}");
|
||||
if (!TrangaSettings.requestLimits.ContainsKey(requestType))
|
||||
Log.Debug($"Requesting {requestType} {url}");
|
||||
if (!Tranga.Settings.RequestLimits.ContainsKey(requestType))
|
||||
{
|
||||
return new RequestResult(HttpStatusCode.NotAcceptable, null, Stream.Null);
|
||||
}
|
||||
|
||||
int rateLimit = TrangaSettings.userAgent == TrangaSettings.DefaultUserAgent
|
||||
int rateLimit = Tranga.Settings.UserAgent == TrangaSettings.DefaultUserAgent
|
||||
? TrangaSettings.DefaultRequestLimits[requestType]
|
||||
: TrangaSettings.requestLimits[requestType];
|
||||
: Tranga.Settings.RequestLimits[requestType];
|
||||
|
||||
TimeSpan timeBetweenRequests = TimeSpan.FromMinutes(1).Divide(rateLimit);
|
||||
DateTime now = DateTime.Now;
|
||||
LastExecutedRateLimit.TryAdd(requestType, now.Subtract(timeBetweenRequests));
|
||||
|
||||
TimeSpan rateLimitTimeout = timeBetweenRequests.Subtract(now.Subtract(LastExecutedRateLimit[requestType]));
|
||||
Log.Debug($"Request limit {rateLimit}/Minute timeBetweenRequests: {timeBetweenRequests:ss'.'fffff} Timeout: {rateLimitTimeout:ss'.'fffff}");
|
||||
Log.Debug($"Request limit {requestType} {rateLimit}/Minute timeBetweenRequests: {timeBetweenRequests:ss'.'fffff} Timeout: {rateLimitTimeout:ss'.'fffff}");
|
||||
|
||||
if (rateLimitTimeout > TimeSpan.Zero)
|
||||
{
|
||||
@ -39,6 +39,7 @@ internal abstract class DownloadClient
|
||||
|
||||
RequestResult result = MakeRequestInternal(url, referrer, clickButton);
|
||||
LastExecutedRateLimit[requestType] = DateTime.UtcNow;
|
||||
Log.Debug($"Result {url}: {result}");
|
||||
return result;
|
||||
}
|
||||
|
||||
|
180
API/MangaDownloadClients/FlareSolverrDownloadClient.cs
Normal file
180
API/MangaDownloadClients/FlareSolverrDownloadClient.cs
Normal file
@ -0,0 +1,180 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using HtmlAgilityPack;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace API.MangaDownloadClients;
|
||||
|
||||
public class FlareSolverrDownloadClient : DownloadClient
|
||||
{
|
||||
|
||||
|
||||
internal override RequestResult MakeRequestInternal(string url, string? referrer = null, string? clickButton = null)
|
||||
{
|
||||
if (clickButton is not null)
|
||||
Log.Warn("Client can not click button");
|
||||
if(referrer is not null)
|
||||
Log.Warn("Client can not set referrer");
|
||||
if (Tranga.Settings.FlareSolverrUrl == string.Empty)
|
||||
{
|
||||
Log.Error("FlareSolverr URL is empty");
|
||||
return new(HttpStatusCode.InternalServerError, null, Stream.Null);
|
||||
}
|
||||
|
||||
Uri flareSolverrUri = new (Tranga.Settings.FlareSolverrUrl);
|
||||
if (flareSolverrUri.Segments.Last() != "v1")
|
||||
flareSolverrUri = new UriBuilder(flareSolverrUri)
|
||||
{
|
||||
Path = "v1"
|
||||
}.Uri;
|
||||
|
||||
HttpClient client = new()
|
||||
{
|
||||
Timeout = TimeSpan.FromSeconds(10),
|
||||
DefaultVersionPolicy = HttpVersionPolicy.RequestVersionOrHigher,
|
||||
DefaultRequestHeaders = { { "User-Agent", Tranga.Settings.UserAgent } }
|
||||
};
|
||||
|
||||
JObject requestObj = new()
|
||||
{
|
||||
["cmd"] = "request.get",
|
||||
["url"] = url
|
||||
};
|
||||
|
||||
HttpRequestMessage requestMessage = new(HttpMethod.Post, flareSolverrUri)
|
||||
{
|
||||
Content = new StringContent(JsonConvert.SerializeObject(requestObj)),
|
||||
};
|
||||
requestMessage.Content.Headers.ContentType = new ("application/json");
|
||||
Log.Debug($"Requesting {url}");
|
||||
|
||||
HttpResponseMessage? response;
|
||||
try
|
||||
{
|
||||
response = client.Send(requestMessage);
|
||||
}
|
||||
catch (HttpRequestException e)
|
||||
{
|
||||
Log.Error(e);
|
||||
return new (HttpStatusCode.Unused, null, Stream.Null);
|
||||
}
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
Log.Debug($"Request returned status code {(int)response.StatusCode} {response.StatusCode}:\n" +
|
||||
$"=====\n" +
|
||||
$"Request:\n" +
|
||||
$"{requestMessage.Method} {requestMessage.RequestUri}\n" +
|
||||
$"{requestMessage.Version} {requestMessage.VersionPolicy}\n" +
|
||||
$"Headers:\n\t{string.Join("\n\t", requestMessage.Headers.Select(h => $"{h.Key}: <{string.Join(">, <", h.Value)}"))}>\n" +
|
||||
$"{requestMessage.Content?.ReadAsStringAsync().Result}" +
|
||||
$"=====\n" +
|
||||
$"Response:\n" +
|
||||
$"{response.Version}\n" +
|
||||
$"Headers:\n\t{string.Join("\n\t", response.Headers.Select(h => $"{h.Key}: <{string.Join(">, <", h.Value)}"))}>\n" +
|
||||
$"{response.Content.ReadAsStringAsync().Result}");
|
||||
return new (response.StatusCode, null, Stream.Null);
|
||||
}
|
||||
|
||||
string responseString = response.Content.ReadAsStringAsync().Result;
|
||||
JObject responseObj = JObject.Parse(responseString);
|
||||
if (!IsInCorrectFormat(responseObj, out string? reason))
|
||||
{
|
||||
Log.Error($"Wrong format: {reason}");
|
||||
return new(HttpStatusCode.Unused, null, Stream.Null);
|
||||
}
|
||||
|
||||
string statusResponse = responseObj["status"]!.Value<string>()!;
|
||||
if (statusResponse != "ok")
|
||||
{
|
||||
Log.Debug($"Status is not ok: {statusResponse}");
|
||||
return new(HttpStatusCode.Unused, null, Stream.Null);
|
||||
}
|
||||
JObject solution = (responseObj["solution"] as JObject)!;
|
||||
|
||||
if (!Enum.TryParse(solution["status"]!.Value<int>().ToString(), out HttpStatusCode statusCode))
|
||||
{
|
||||
Log.Error($"Wrong format: Cant parse status code: {solution["status"]!.Value<int>()}");
|
||||
return new(HttpStatusCode.Unused, null, Stream.Null);
|
||||
}
|
||||
if (statusCode < HttpStatusCode.OK || statusCode >= HttpStatusCode.MultipleChoices)
|
||||
{
|
||||
Log.Debug($"Status is: {statusCode}");
|
||||
return new(statusCode, null, Stream.Null);
|
||||
}
|
||||
|
||||
if (solution["response"]!.Value<string>() is not { } htmlString)
|
||||
{
|
||||
Log.Error("Wrong format: Cant find response in solution");
|
||||
return new(HttpStatusCode.Unused, null, Stream.Null);
|
||||
}
|
||||
|
||||
if (IsJson(htmlString, out HtmlDocument document, out string? json))
|
||||
{
|
||||
MemoryStream ms = new();
|
||||
ms.Write(Encoding.UTF8.GetBytes(json));
|
||||
ms.Position = 0;
|
||||
return new(statusCode, document, ms);
|
||||
}
|
||||
else
|
||||
{
|
||||
MemoryStream ms = new();
|
||||
ms.Write(Encoding.UTF8.GetBytes(htmlString));
|
||||
ms.Position = 0;
|
||||
return new(statusCode, document, ms);
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsInCorrectFormat(JObject responseObj, [NotNullWhen(false)]out string? reason)
|
||||
{
|
||||
reason = null;
|
||||
if (!responseObj.ContainsKey("status"))
|
||||
{
|
||||
reason = "Cant find status on response";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (responseObj["solution"] is not JObject solution)
|
||||
{
|
||||
reason = "Cant find solution";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!solution.ContainsKey("status"))
|
||||
{
|
||||
reason = "Wrong format: Cant find status in solution";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!solution.ContainsKey("response"))
|
||||
{
|
||||
|
||||
reason = "Wrong format: Cant find response in solution";
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool IsJson(string htmlString, out HtmlDocument document, [NotNullWhen(true)]out string? jsonString)
|
||||
{
|
||||
jsonString = null;
|
||||
document = new();
|
||||
document.LoadHtml(htmlString);
|
||||
|
||||
HtmlNode pre = document.DocumentNode.SelectSingleNode("//pre");
|
||||
try
|
||||
{
|
||||
using JsonDocument _ = JsonDocument.Parse(pre.InnerText);
|
||||
jsonString = pre.InnerText;
|
||||
return true;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
@ -5,46 +5,54 @@ namespace API.MangaDownloadClients;
|
||||
|
||||
internal class HttpDownloadClient : DownloadClient
|
||||
{
|
||||
private static readonly HttpClient Client = new()
|
||||
{
|
||||
Timeout = TimeSpan.FromSeconds(10)
|
||||
};
|
||||
|
||||
public HttpDownloadClient()
|
||||
{
|
||||
Client.DefaultRequestHeaders.TryAddWithoutValidation("User-Agent", TrangaSettings.userAgent);
|
||||
}
|
||||
|
||||
internal override RequestResult MakeRequestInternal(string url, string? referrer = null, string? clickButton = null)
|
||||
{
|
||||
if (clickButton is not null)
|
||||
Log.Warn("Can not click button on static site.");
|
||||
HttpResponseMessage? response = null;
|
||||
while (response is null)
|
||||
{
|
||||
HttpRequestMessage requestMessage = new(HttpMethod.Get, url);
|
||||
Log.Warn("Client can not click button");
|
||||
HttpClient client = new();
|
||||
client.Timeout = TimeSpan.FromSeconds(10);
|
||||
client.DefaultVersionPolicy = HttpVersionPolicy.RequestVersionOrHigher;
|
||||
client.DefaultRequestHeaders.Add("User-Agent", Tranga.Settings.UserAgent);
|
||||
HttpResponseMessage? response;
|
||||
Uri uri = new(url);
|
||||
HttpRequestMessage requestMessage = new(HttpMethod.Get, uri);
|
||||
if (referrer is not null)
|
||||
requestMessage.Headers.Referrer = new Uri(referrer);
|
||||
requestMessage.Headers.Referrer = new (referrer);
|
||||
Log.Debug($"Requesting {url}");
|
||||
try
|
||||
{
|
||||
response = Client.Send(requestMessage);
|
||||
response = client.Send(requestMessage);
|
||||
}
|
||||
catch (Exception e)
|
||||
catch (HttpRequestException e)
|
||||
{
|
||||
switch (e)
|
||||
{
|
||||
case TaskCanceledException:
|
||||
return new RequestResult(HttpStatusCode.RequestTimeout, null, Stream.Null);
|
||||
case HttpRequestException:
|
||||
return new RequestResult(HttpStatusCode.BadRequest, null, Stream.Null);
|
||||
}
|
||||
}
|
||||
Log.Error(e);
|
||||
return new (HttpStatusCode.Unused, null, Stream.Null);
|
||||
}
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
return new RequestResult(response.StatusCode, null, Stream.Null);
|
||||
Log.Debug($"Request returned status code {(int)response.StatusCode} {response.StatusCode}");
|
||||
if (response.Headers.Server.Any(s =>
|
||||
(s.Product?.Name ?? "").Contains("cloudflare", StringComparison.InvariantCultureIgnoreCase)))
|
||||
{
|
||||
Log.Debug("Retrying with FlareSolverr!");
|
||||
return new FlareSolverrDownloadClient().MakeRequestInternal(url, referrer, clickButton);
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Debug($"Request returned status code {(int)response.StatusCode} {response.StatusCode}:\n" +
|
||||
$"=====\n" +
|
||||
$"Request:\n" +
|
||||
$"{requestMessage.Method} {requestMessage.RequestUri}\n" +
|
||||
$"{requestMessage.Version} {requestMessage.VersionPolicy}\n" +
|
||||
$"Headers:\n\t{string.Join("\n\t", requestMessage.Headers.Select(h => $"{h.Key}: <{string.Join(">, <", h.Value)}"))}>\n" +
|
||||
$"{requestMessage.Content?.ReadAsStringAsync().Result}" +
|
||||
$"=====\n" +
|
||||
$"Response:\n" +
|
||||
$"{response.Version}\n" +
|
||||
$"Headers:\n\t{string.Join("\n\t", response.Headers.Select(h => $"{h.Key}: <{string.Join(">, <", h.Value)}"))}>\n" +
|
||||
$"{response.Content.ReadAsStringAsync().Result}");
|
||||
}
|
||||
}
|
||||
|
||||
Stream stream;
|
||||
@ -55,7 +63,7 @@ internal class HttpDownloadClient : DownloadClient
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e);
|
||||
return new RequestResult(HttpStatusCode.InternalServerError, null, Stream.Null);
|
||||
return new (HttpStatusCode.Unused, null, Stream.Null);
|
||||
}
|
||||
|
||||
HtmlDocument? document = null;
|
||||
@ -69,12 +77,11 @@ internal class HttpDownloadClient : DownloadClient
|
||||
}
|
||||
|
||||
// Request has been redirected to another page. For example, it redirects directly to the results when there is only 1 result
|
||||
if (response.RequestMessage is not null && response.RequestMessage.RequestUri is not null)
|
||||
if (response.RequestMessage is not null && response.RequestMessage.RequestUri is not null && response.RequestMessage.RequestUri != uri)
|
||||
{
|
||||
return new RequestResult(response.StatusCode, document, stream, true,
|
||||
response.RequestMessage.RequestUri.AbsoluteUri);
|
||||
return new (response.StatusCode, document, stream, true, response.RequestMessage.RequestUri.AbsoluteUri);
|
||||
}
|
||||
|
||||
return new RequestResult(response.StatusCode, document, stream);
|
||||
return new (response.StatusCode, document, stream);
|
||||
}
|
||||
}
|
@ -24,4 +24,10 @@ public struct RequestResult
|
||||
this.hasBeenRedirected = hasBeenRedirected;
|
||||
redirectedToUrl = redirectedTo;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return
|
||||
$"{(int)statusCode} {statusCode.ToString()} {(hasBeenRedirected ? "Redirected: " : "")} {redirectedToUrl}";
|
||||
}
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
// <auto-generated />
|
||||
using API.Schema.Contexts;
|
||||
using API.Schema.LibraryContext;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
@ -8,10 +8,10 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace API.Migrations.library
|
||||
namespace API.Migrations.Library
|
||||
{
|
||||
[DbContext(typeof(LibraryContext))]
|
||||
[Migration("20250515120732_Initial")]
|
||||
[Migration("20250703191925_Initial")]
|
||||
partial class Initial
|
||||
{
|
||||
/// <inheritdoc />
|
||||
@ -19,16 +19,15 @@ namespace API.Migrations.library
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "9.0.3")
|
||||
.HasAnnotation("ProductVersion", "9.0.5")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("API.Schema.LibraryConnectors.LibraryConnector", b =>
|
||||
modelBuilder.Entity("API.Schema.LibraryContext.LibraryConnectors.LibraryConnector", b =>
|
||||
{
|
||||
b.Property<string>("LibraryConnectorId")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
b.Property<string>("Key")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Auth")
|
||||
.IsRequired()
|
||||
@ -43,7 +42,7 @@ namespace API.Migrations.library
|
||||
b.Property<byte>("LibraryType")
|
||||
.HasColumnType("smallint");
|
||||
|
||||
b.HasKey("LibraryConnectorId");
|
||||
b.HasKey("Key");
|
||||
|
||||
b.ToTable("LibraryConnectors");
|
||||
|
||||
@ -52,16 +51,16 @@ namespace API.Migrations.library
|
||||
b.UseTphMappingStrategy();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.LibraryConnectors.Kavita", b =>
|
||||
modelBuilder.Entity("API.Schema.LibraryContext.LibraryConnectors.Kavita", b =>
|
||||
{
|
||||
b.HasBaseType("API.Schema.LibraryConnectors.LibraryConnector");
|
||||
b.HasBaseType("API.Schema.LibraryContext.LibraryConnectors.LibraryConnector");
|
||||
|
||||
b.HasDiscriminator().HasValue((byte)1);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.LibraryConnectors.Komga", b =>
|
||||
modelBuilder.Entity("API.Schema.LibraryContext.LibraryConnectors.Komga", b =>
|
||||
{
|
||||
b.HasBaseType("API.Schema.LibraryConnectors.LibraryConnector");
|
||||
b.HasBaseType("API.Schema.LibraryContext.LibraryConnectors.LibraryConnector");
|
||||
|
||||
b.HasDiscriminator().HasValue((byte)0);
|
||||
});
|
@ -2,7 +2,7 @@
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace API.Migrations.library
|
||||
namespace API.Migrations.Library
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class Initial : Migration
|
||||
@ -14,14 +14,14 @@ namespace API.Migrations.library
|
||||
name: "LibraryConnectors",
|
||||
columns: table => new
|
||||
{
|
||||
LibraryConnectorId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
|
||||
Key = table.Column<string>(type: "text", nullable: false),
|
||||
LibraryType = table.Column<byte>(type: "smallint", nullable: false),
|
||||
BaseUrl = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
|
||||
Auth = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_LibraryConnectors", x => x.LibraryConnectorId);
|
||||
table.PrimaryKey("PK_LibraryConnectors", x => x.Key);
|
||||
});
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
// <auto-generated />
|
||||
using API.Schema.Contexts;
|
||||
using API.Schema.LibraryContext;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
@ -7,7 +7,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace API.Migrations.library
|
||||
namespace API.Migrations.Library
|
||||
{
|
||||
[DbContext(typeof(LibraryContext))]
|
||||
partial class LibraryContextModelSnapshot : ModelSnapshot
|
||||
@ -16,16 +16,15 @@ namespace API.Migrations.library
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "9.0.3")
|
||||
.HasAnnotation("ProductVersion", "9.0.5")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("API.Schema.LibraryConnectors.LibraryConnector", b =>
|
||||
modelBuilder.Entity("API.Schema.LibraryContext.LibraryConnectors.LibraryConnector", b =>
|
||||
{
|
||||
b.Property<string>("LibraryConnectorId")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
b.Property<string>("Key")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Auth")
|
||||
.IsRequired()
|
||||
@ -40,7 +39,7 @@ namespace API.Migrations.library
|
||||
b.Property<byte>("LibraryType")
|
||||
.HasColumnType("smallint");
|
||||
|
||||
b.HasKey("LibraryConnectorId");
|
||||
b.HasKey("Key");
|
||||
|
||||
b.ToTable("LibraryConnectors");
|
||||
|
||||
@ -49,16 +48,16 @@ namespace API.Migrations.library
|
||||
b.UseTphMappingStrategy();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.LibraryConnectors.Kavita", b =>
|
||||
modelBuilder.Entity("API.Schema.LibraryContext.LibraryConnectors.Kavita", b =>
|
||||
{
|
||||
b.HasBaseType("API.Schema.LibraryConnectors.LibraryConnector");
|
||||
b.HasBaseType("API.Schema.LibraryContext.LibraryConnectors.LibraryConnector");
|
||||
|
||||
b.HasDiscriminator().HasValue((byte)1);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.LibraryConnectors.Komga", b =>
|
||||
modelBuilder.Entity("API.Schema.LibraryContext.LibraryConnectors.Komga", b =>
|
||||
{
|
||||
b.HasBaseType("API.Schema.LibraryConnectors.LibraryConnector");
|
||||
b.HasBaseType("API.Schema.LibraryContext.LibraryConnectors.LibraryConnector");
|
||||
|
||||
b.HasDiscriminator().HasValue((byte)0);
|
||||
});
|
547
API/Migrations/Manga/20250703192023_Initial.Designer.cs
generated
Normal file
547
API/Migrations/Manga/20250703192023_Initial.Designer.cs
generated
Normal file
@ -0,0 +1,547 @@
|
||||
// <auto-generated />
|
||||
using API.Schema.MangaContext;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace API.Migrations.Manga
|
||||
{
|
||||
[DbContext(typeof(MangaContext))]
|
||||
[Migration("20250703192023_Initial")]
|
||||
partial class Initial
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "9.0.5")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("API.Schema.MangaContext.Author", b =>
|
||||
{
|
||||
b.Property<string>("Key")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("AuthorName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("character varying(128)");
|
||||
|
||||
b.HasKey("Key");
|
||||
|
||||
b.ToTable("Authors");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.MangaContext.Chapter", b =>
|
||||
{
|
||||
b.Property<string>("Key")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("ChapterNumber")
|
||||
.IsRequired()
|
||||
.HasMaxLength(10)
|
||||
.HasColumnType("character varying(10)");
|
||||
|
||||
b.Property<bool>("Downloaded")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("FileName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<string>("ParentMangaId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<int?>("VolumeNumber")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Key");
|
||||
|
||||
b.HasIndex("ParentMangaId");
|
||||
|
||||
b.ToTable("Chapters");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.MangaContext.FileLibrary", b =>
|
||||
{
|
||||
b.Property<string>("Key")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("BasePath")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<string>("LibraryName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("character varying(512)");
|
||||
|
||||
b.HasKey("Key");
|
||||
|
||||
b.ToTable("FileLibraries");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.MangaContext.Manga", b =>
|
||||
{
|
||||
b.Property<string>("Key")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("CoverFileNameInCache")
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("character varying(512)");
|
||||
|
||||
b.Property<string>("CoverUrl")
|
||||
.IsRequired()
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("character varying(512)");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("DirectoryName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)");
|
||||
|
||||
b.Property<float>("IgnoreChaptersBefore")
|
||||
.HasColumnType("real");
|
||||
|
||||
b.Property<string>("LibraryId")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("character varying(512)");
|
||||
|
||||
b.Property<string>("OriginalLanguage")
|
||||
.HasMaxLength(8)
|
||||
.HasColumnType("character varying(8)");
|
||||
|
||||
b.Property<byte>("ReleaseStatus")
|
||||
.HasColumnType("smallint");
|
||||
|
||||
b.Property<long?>("Year")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.HasKey("Key");
|
||||
|
||||
b.HasIndex("LibraryId");
|
||||
|
||||
b.ToTable("Mangas");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.MangaContext.MangaConnectorId<API.Schema.MangaContext.Chapter>", b =>
|
||||
{
|
||||
b.Property<string>("Key")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("IdOnConnectorSite")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<string>("MangaConnectorName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)");
|
||||
|
||||
b.Property<string>("ObjId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<bool>("UseForDownload")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("WebsiteUrl")
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("character varying(512)");
|
||||
|
||||
b.HasKey("Key");
|
||||
|
||||
b.HasIndex("MangaConnectorName");
|
||||
|
||||
b.HasIndex("ObjId");
|
||||
|
||||
b.ToTable("MangaConnectorToChapter");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.MangaContext.MangaConnectorId<API.Schema.MangaContext.Manga>", b =>
|
||||
{
|
||||
b.Property<string>("Key")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("IdOnConnectorSite")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<string>("MangaConnectorName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)");
|
||||
|
||||
b.Property<string>("ObjId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<bool>("UseForDownload")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("WebsiteUrl")
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("character varying(512)");
|
||||
|
||||
b.HasKey("Key");
|
||||
|
||||
b.HasIndex("MangaConnectorName");
|
||||
|
||||
b.HasIndex("ObjId");
|
||||
|
||||
b.ToTable("MangaConnectorToManga");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.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")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.HasKey("Tag");
|
||||
|
||||
b.ToTable("Tags");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.MangaContext.MetadataFetchers.MetadataEntry", b =>
|
||||
{
|
||||
b.Property<string>("MetadataFetcherName")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Identifier")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("MangaId")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("MetadataFetcherName", "Identifier");
|
||||
|
||||
b.HasIndex("MangaId");
|
||||
|
||||
b.ToTable("MetadataEntries");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.MangaContext.MetadataFetchers.MetadataFetcher", b =>
|
||||
{
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("MetadataEntry")
|
||||
.IsRequired()
|
||||
.HasMaxLength(21)
|
||||
.HasColumnType("character varying(21)");
|
||||
|
||||
b.HasKey("Name");
|
||||
|
||||
b.ToTable("MetadataFetcher");
|
||||
|
||||
b.HasDiscriminator<string>("MetadataEntry").HasValue("MetadataFetcher");
|
||||
|
||||
b.UseTphMappingStrategy();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AuthorToManga", b =>
|
||||
{
|
||||
b.Property<string>("AuthorIds")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("MangaIds")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("AuthorIds", "MangaIds");
|
||||
|
||||
b.HasIndex("MangaIds");
|
||||
|
||||
b.ToTable("AuthorToManga");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MangaTagToManga", b =>
|
||||
{
|
||||
b.Property<string>("MangaTagIds")
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<string>("MangaIds")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("MangaTagIds", "MangaIds");
|
||||
|
||||
b.HasIndex("MangaIds");
|
||||
|
||||
b.ToTable("MangaTagToManga");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.MangaContext.MangaConnectors.ComickIo", b =>
|
||||
{
|
||||
b.HasBaseType("API.Schema.MangaContext.MangaConnectors.MangaConnector");
|
||||
|
||||
b.HasDiscriminator().HasValue("ComickIo");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.MangaContext.MangaConnectors.Global", b =>
|
||||
{
|
||||
b.HasBaseType("API.Schema.MangaContext.MangaConnectors.MangaConnector");
|
||||
|
||||
b.HasDiscriminator().HasValue("Global");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.MangaContext.MangaConnectors.MangaDex", b =>
|
||||
{
|
||||
b.HasBaseType("API.Schema.MangaContext.MangaConnectors.MangaConnector");
|
||||
|
||||
b.HasDiscriminator().HasValue("MangaDex");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.MangaContext.MetadataFetchers.MyAnimeList", b =>
|
||||
{
|
||||
b.HasBaseType("API.Schema.MangaContext.MetadataFetchers.MetadataFetcher");
|
||||
|
||||
b.HasDiscriminator().HasValue("MyAnimeList");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.MangaContext.Chapter", b =>
|
||||
{
|
||||
b.HasOne("API.Schema.MangaContext.Manga", "ParentManga")
|
||||
.WithMany("Chapters")
|
||||
.HasForeignKey("ParentMangaId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("ParentManga");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.MangaContext.Manga", b =>
|
||||
{
|
||||
b.HasOne("API.Schema.MangaContext.FileLibrary", "Library")
|
||||
.WithMany()
|
||||
.HasForeignKey("LibraryId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.OwnsMany("API.Schema.MangaContext.AltTitle", "AltTitles", b1 =>
|
||||
{
|
||||
b1.Property<string>("Key")
|
||||
.HasColumnType("text");
|
||||
|
||||
b1.Property<string>("Language")
|
||||
.IsRequired()
|
||||
.HasMaxLength(8)
|
||||
.HasColumnType("character varying(8)");
|
||||
|
||||
b1.Property<string>("MangaKey")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b1.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b1.HasKey("Key");
|
||||
|
||||
b1.HasIndex("MangaKey");
|
||||
|
||||
b1.ToTable("AltTitle");
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("MangaKey");
|
||||
});
|
||||
|
||||
b.OwnsMany("API.Schema.MangaContext.Link", "Links", b1 =>
|
||||
{
|
||||
b1.Property<string>("Key")
|
||||
.HasColumnType("text");
|
||||
|
||||
b1.Property<string>("LinkProvider")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b1.Property<string>("LinkUrl")
|
||||
.IsRequired()
|
||||
.HasMaxLength(2048)
|
||||
.HasColumnType("character varying(2048)");
|
||||
|
||||
b1.Property<string>("MangaKey")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b1.HasKey("Key");
|
||||
|
||||
b1.HasIndex("MangaKey");
|
||||
|
||||
b1.ToTable("Link");
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("MangaKey");
|
||||
});
|
||||
|
||||
b.Navigation("AltTitles");
|
||||
|
||||
b.Navigation("Library");
|
||||
|
||||
b.Navigation("Links");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.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)
|
||||
.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");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.MangaContext.MetadataFetchers.MetadataEntry", b =>
|
||||
{
|
||||
b.HasOne("API.Schema.MangaContext.Manga", "Manga")
|
||||
.WithMany()
|
||||
.HasForeignKey("MangaId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("API.Schema.MangaContext.MetadataFetchers.MetadataFetcher", "MetadataFetcher")
|
||||
.WithMany()
|
||||
.HasForeignKey("MetadataFetcherName")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Manga");
|
||||
|
||||
b.Navigation("MetadataFetcher");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AuthorToManga", b =>
|
||||
{
|
||||
b.HasOne("API.Schema.MangaContext.Author", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("AuthorIds")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("API.Schema.MangaContext.Manga", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("MangaIds")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MangaTagToManga", b =>
|
||||
{
|
||||
b.HasOne("API.Schema.MangaContext.Manga", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("MangaIds")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("API.Schema.MangaContext.MangaTag", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("MangaTagIds")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.MangaContext.Chapter", b =>
|
||||
{
|
||||
b.Navigation("MangaConnectorIds");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.MangaContext.Manga", b =>
|
||||
{
|
||||
b.Navigation("Chapters");
|
||||
|
||||
b.Navigation("MangaConnectorIds");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
@ -1,12 +1,11 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace API.Migrations.pgsql
|
||||
namespace API.Migrations.Manga
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class Initial1 : Migration
|
||||
public partial class Initial : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
@ -15,25 +14,25 @@ namespace API.Migrations.pgsql
|
||||
name: "Authors",
|
||||
columns: table => new
|
||||
{
|
||||
AuthorId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
|
||||
Key = table.Column<string>(type: "text", nullable: false),
|
||||
AuthorName = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Authors", x => x.AuthorId);
|
||||
table.PrimaryKey("PK_Authors", x => x.Key);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "LocalLibraries",
|
||||
name: "FileLibraries",
|
||||
columns: table => new
|
||||
{
|
||||
LocalLibraryId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
|
||||
Key = table.Column<string>(type: "text", nullable: false),
|
||||
BasePath = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
|
||||
LibraryName = table.Column<string>(type: "character varying(512)", maxLength: 512, nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_LocalLibraries", x => x.LocalLibraryId);
|
||||
table.PrimaryKey("PK_FileLibraries", x => x.Key);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
@ -51,6 +50,18 @@ namespace API.Migrations.pgsql
|
||||
table.PrimaryKey("PK_MangaConnectors", x => x.Name);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "MetadataFetcher",
|
||||
columns: table => new
|
||||
{
|
||||
Name = table.Column<string>(type: "text", nullable: false),
|
||||
MetadataEntry = table.Column<string>(type: "character varying(21)", maxLength: 21, nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_MetadataFetcher", x => x.Name);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Tags",
|
||||
columns: table => new
|
||||
@ -66,35 +77,46 @@ namespace API.Migrations.pgsql
|
||||
name: "Mangas",
|
||||
columns: table => new
|
||||
{
|
||||
MangaId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
|
||||
IdOnConnectorSite = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
|
||||
Key = table.Column<string>(type: "text", nullable: false),
|
||||
Name = table.Column<string>(type: "character varying(512)", maxLength: 512, nullable: false),
|
||||
Description = table.Column<string>(type: "text", nullable: false),
|
||||
WebsiteUrl = table.Column<string>(type: "character varying(512)", maxLength: 512, nullable: false),
|
||||
CoverUrl = table.Column<string>(type: "character varying(512)", maxLength: 512, nullable: false),
|
||||
ReleaseStatus = table.Column<byte>(type: "smallint", nullable: false),
|
||||
LibraryId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: true),
|
||||
MangaConnectorName = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: false),
|
||||
IgnoreChaptersBefore = table.Column<float>(type: "real", nullable: false),
|
||||
DirectoryName = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: false),
|
||||
CoverFileNameInCache = table.Column<string>(type: "character varying(512)", maxLength: 512, nullable: true),
|
||||
Year = table.Column<long>(type: "bigint", nullable: false),
|
||||
Year = table.Column<long>(type: "bigint", nullable: true),
|
||||
OriginalLanguage = table.Column<string>(type: "character varying(8)", maxLength: 8, nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Mangas", x => x.MangaId);
|
||||
table.PrimaryKey("PK_Mangas", x => x.Key);
|
||||
table.ForeignKey(
|
||||
name: "FK_Mangas_LocalLibraries_LibraryId",
|
||||
name: "FK_Mangas_FileLibraries_LibraryId",
|
||||
column: x => x.LibraryId,
|
||||
principalTable: "LocalLibraries",
|
||||
principalColumn: "LocalLibraryId",
|
||||
principalTable: "FileLibraries",
|
||||
principalColumn: "Key",
|
||||
onDelete: ReferentialAction.SetNull);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "AltTitle",
|
||||
columns: table => new
|
||||
{
|
||||
Key = table.Column<string>(type: "text", nullable: false),
|
||||
Language = table.Column<string>(type: "character varying(8)", maxLength: 8, nullable: false),
|
||||
Title = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
|
||||
MangaKey = table.Column<string>(type: "text", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_AltTitle", x => x.Key);
|
||||
table.ForeignKey(
|
||||
name: "FK_Mangas_MangaConnectors_MangaConnectorName",
|
||||
column: x => x.MangaConnectorName,
|
||||
principalTable: "MangaConnectors",
|
||||
principalColumn: "Name",
|
||||
name: "FK_AltTitle_Mangas_MangaKey",
|
||||
column: x => x.MangaKey,
|
||||
principalTable: "Mangas",
|
||||
principalColumn: "Key",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
@ -102,8 +124,8 @@ namespace API.Migrations.pgsql
|
||||
name: "AuthorToManga",
|
||||
columns: table => new
|
||||
{
|
||||
AuthorIds = table.Column<string>(type: "character varying(64)", nullable: false),
|
||||
MangaIds = table.Column<string>(type: "character varying(64)", nullable: false)
|
||||
AuthorIds = table.Column<string>(type: "text", nullable: false),
|
||||
MangaIds = table.Column<string>(type: "text", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
@ -112,13 +134,13 @@ namespace API.Migrations.pgsql
|
||||
name: "FK_AuthorToManga_Authors_AuthorIds",
|
||||
column: x => x.AuthorIds,
|
||||
principalTable: "Authors",
|
||||
principalColumn: "AuthorId",
|
||||
principalColumn: "Key",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_AuthorToManga_Mangas_MangaIds",
|
||||
column: x => x.MangaIds,
|
||||
principalTable: "Mangas",
|
||||
principalColumn: "MangaId",
|
||||
principalColumn: "Key",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
@ -126,23 +148,22 @@ namespace API.Migrations.pgsql
|
||||
name: "Chapters",
|
||||
columns: table => new
|
||||
{
|
||||
ChapterId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
|
||||
ParentMangaId = table.Column<string>(type: "character varying(64)", nullable: false),
|
||||
Key = table.Column<string>(type: "text", nullable: false),
|
||||
ParentMangaId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
|
||||
VolumeNumber = table.Column<int>(type: "integer", nullable: true),
|
||||
ChapterNumber = table.Column<string>(type: "character varying(10)", maxLength: 10, nullable: false),
|
||||
Url = table.Column<string>(type: "character varying(2048)", maxLength: 2048, nullable: false),
|
||||
Title = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
|
||||
FileName = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
|
||||
Downloaded = table.Column<bool>(type: "boolean", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Chapters", x => x.ChapterId);
|
||||
table.PrimaryKey("PK_Chapters", x => x.Key);
|
||||
table.ForeignKey(
|
||||
name: "FK_Chapters_Mangas_ParentMangaId",
|
||||
column: x => x.ParentMangaId,
|
||||
principalTable: "Mangas",
|
||||
principalColumn: "MangaId",
|
||||
principalColumn: "Key",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
@ -150,39 +171,47 @@ namespace API.Migrations.pgsql
|
||||
name: "Link",
|
||||
columns: table => new
|
||||
{
|
||||
LinkId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
|
||||
Key = table.Column<string>(type: "text", nullable: false),
|
||||
LinkProvider = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
|
||||
LinkUrl = table.Column<string>(type: "character varying(2048)", maxLength: 2048, nullable: false),
|
||||
MangaId = table.Column<string>(type: "character varying(64)", nullable: false)
|
||||
MangaKey = table.Column<string>(type: "text", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Link", x => x.LinkId);
|
||||
table.PrimaryKey("PK_Link", x => x.Key);
|
||||
table.ForeignKey(
|
||||
name: "FK_Link_Mangas_MangaId",
|
||||
column: x => x.MangaId,
|
||||
name: "FK_Link_Mangas_MangaKey",
|
||||
column: x => x.MangaKey,
|
||||
principalTable: "Mangas",
|
||||
principalColumn: "MangaId",
|
||||
principalColumn: "Key",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "MangaAltTitle",
|
||||
name: "MangaConnectorToManga",
|
||||
columns: table => new
|
||||
{
|
||||
AltTitleId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
|
||||
Language = table.Column<string>(type: "character varying(8)", maxLength: 8, nullable: false),
|
||||
Title = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
|
||||
MangaId = table.Column<string>(type: "character varying(64)", nullable: false)
|
||||
Key = table.Column<string>(type: "text", nullable: false),
|
||||
ObjId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
|
||||
MangaConnectorName = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: false),
|
||||
IdOnConnectorSite = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
|
||||
WebsiteUrl = table.Column<string>(type: "character varying(512)", maxLength: 512, nullable: true),
|
||||
UseForDownload = table.Column<bool>(type: "boolean", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_MangaAltTitle", x => x.AltTitleId);
|
||||
table.PrimaryKey("PK_MangaConnectorToManga", x => x.Key);
|
||||
table.ForeignKey(
|
||||
name: "FK_MangaAltTitle_Mangas_MangaId",
|
||||
column: x => x.MangaId,
|
||||
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,
|
||||
principalTable: "Mangas",
|
||||
principalColumn: "MangaId",
|
||||
principalColumn: "Key",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
@ -191,7 +220,7 @@ namespace API.Migrations.pgsql
|
||||
columns: table => new
|
||||
{
|
||||
MangaTagIds = table.Column<string>(type: "character varying(64)", nullable: false),
|
||||
MangaIds = table.Column<string>(type: "character varying(64)", nullable: false)
|
||||
MangaIds = table.Column<string>(type: "text", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
@ -200,7 +229,7 @@ namespace API.Migrations.pgsql
|
||||
name: "FK_MangaTagToManga_Mangas_MangaIds",
|
||||
column: x => x.MangaIds,
|
||||
principalTable: "Mangas",
|
||||
principalColumn: "MangaId",
|
||||
principalColumn: "Key",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_MangaTagToManga_Tags_MangaTagIds",
|
||||
@ -211,104 +240,62 @@ namespace API.Migrations.pgsql
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Jobs",
|
||||
name: "MetadataEntries",
|
||||
columns: table => new
|
||||
{
|
||||
JobId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
|
||||
ParentJobId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: true),
|
||||
JobType = table.Column<byte>(type: "smallint", nullable: false),
|
||||
RecurrenceMs = table.Column<decimal>(type: "numeric(20,0)", nullable: false),
|
||||
LastExecution = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||
state = table.Column<byte>(type: "smallint", nullable: false),
|
||||
Enabled = table.Column<bool>(type: "boolean", nullable: false),
|
||||
DownloadAvailableChaptersJob_MangaId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: true),
|
||||
MangaId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: true),
|
||||
ChapterId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: true),
|
||||
FromLocation = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
|
||||
ToLocation = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
|
||||
MoveMangaLibraryJob_MangaId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: true),
|
||||
ToLibraryId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: true),
|
||||
RetrieveChaptersJob_MangaId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: true),
|
||||
Language = table.Column<string>(type: "character varying(8)", maxLength: 8, nullable: true),
|
||||
UpdateFilesDownloadedJob_MangaId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: true)
|
||||
MetadataFetcherName = table.Column<string>(type: "text", nullable: false),
|
||||
Identifier = table.Column<string>(type: "text", nullable: false),
|
||||
MangaId = table.Column<string>(type: "text", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Jobs", x => x.JobId);
|
||||
table.PrimaryKey("PK_MetadataEntries", x => new { x.MetadataFetcherName, x.Identifier });
|
||||
table.ForeignKey(
|
||||
name: "FK_Jobs_Chapters_ChapterId",
|
||||
column: x => x.ChapterId,
|
||||
principalTable: "Chapters",
|
||||
principalColumn: "ChapterId",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_Jobs_Jobs_ParentJobId",
|
||||
column: x => x.ParentJobId,
|
||||
principalTable: "Jobs",
|
||||
principalColumn: "JobId",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_Jobs_LocalLibraries_ToLibraryId",
|
||||
column: x => x.ToLibraryId,
|
||||
principalTable: "LocalLibraries",
|
||||
principalColumn: "LocalLibraryId",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_Jobs_Mangas_DownloadAvailableChaptersJob_MangaId",
|
||||
column: x => x.DownloadAvailableChaptersJob_MangaId,
|
||||
principalTable: "Mangas",
|
||||
principalColumn: "MangaId",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_Jobs_Mangas_MangaId",
|
||||
name: "FK_MetadataEntries_Mangas_MangaId",
|
||||
column: x => x.MangaId,
|
||||
principalTable: "Mangas",
|
||||
principalColumn: "MangaId",
|
||||
principalColumn: "Key",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_Jobs_Mangas_MoveMangaLibraryJob_MangaId",
|
||||
column: x => x.MoveMangaLibraryJob_MangaId,
|
||||
principalTable: "Mangas",
|
||||
principalColumn: "MangaId",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_Jobs_Mangas_RetrieveChaptersJob_MangaId",
|
||||
column: x => x.RetrieveChaptersJob_MangaId,
|
||||
principalTable: "Mangas",
|
||||
principalColumn: "MangaId",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_Jobs_Mangas_UpdateFilesDownloadedJob_MangaId",
|
||||
column: x => x.UpdateFilesDownloadedJob_MangaId,
|
||||
principalTable: "Mangas",
|
||||
principalColumn: "MangaId",
|
||||
name: "FK_MetadataEntries_MetadataFetcher_MetadataFetcherName",
|
||||
column: x => x.MetadataFetcherName,
|
||||
principalTable: "MetadataFetcher",
|
||||
principalColumn: "Name",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "JobJob",
|
||||
name: "MangaConnectorToChapter",
|
||||
columns: table => new
|
||||
{
|
||||
DependsOnJobsJobId = table.Column<string>(type: "character varying(64)", nullable: false),
|
||||
JobId = table.Column<string>(type: "character varying(64)", nullable: false)
|
||||
Key = table.Column<string>(type: "text", nullable: false),
|
||||
ObjId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
|
||||
MangaConnectorName = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: false),
|
||||
IdOnConnectorSite = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
|
||||
WebsiteUrl = table.Column<string>(type: "character varying(512)", maxLength: 512, nullable: true),
|
||||
UseForDownload = table.Column<bool>(type: "boolean", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_JobJob", x => new { x.DependsOnJobsJobId, x.JobId });
|
||||
table.PrimaryKey("PK_MangaConnectorToChapter", x => x.Key);
|
||||
table.ForeignKey(
|
||||
name: "FK_JobJob_Jobs_DependsOnJobsJobId",
|
||||
column: x => x.DependsOnJobsJobId,
|
||||
principalTable: "Jobs",
|
||||
principalColumn: "JobId",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
name: "FK_MangaConnectorToChapter_Chapters_ObjId",
|
||||
column: x => x.ObjId,
|
||||
principalTable: "Chapters",
|
||||
principalColumn: "Key");
|
||||
table.ForeignKey(
|
||||
name: "FK_JobJob_Jobs_JobId",
|
||||
column: x => x.JobId,
|
||||
principalTable: "Jobs",
|
||||
principalColumn: "JobId",
|
||||
name: "FK_MangaConnectorToChapter_MangaConnectors_MangaConnectorName",
|
||||
column: x => x.MangaConnectorName,
|
||||
principalTable: "MangaConnectors",
|
||||
principalColumn: "Name",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_AltTitle_MangaKey",
|
||||
table: "AltTitle",
|
||||
column: "MangaKey");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_AuthorToManga_MangaIds",
|
||||
table: "AuthorToManga",
|
||||
@ -320,114 +307,90 @@ namespace API.Migrations.pgsql
|
||||
column: "ParentMangaId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_JobJob_JobId",
|
||||
table: "JobJob",
|
||||
column: "JobId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Jobs_ChapterId",
|
||||
table: "Jobs",
|
||||
column: "ChapterId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Jobs_DownloadAvailableChaptersJob_MangaId",
|
||||
table: "Jobs",
|
||||
column: "DownloadAvailableChaptersJob_MangaId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Jobs_MangaId",
|
||||
table: "Jobs",
|
||||
column: "MangaId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Jobs_MoveMangaLibraryJob_MangaId",
|
||||
table: "Jobs",
|
||||
column: "MoveMangaLibraryJob_MangaId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Jobs_ParentJobId",
|
||||
table: "Jobs",
|
||||
column: "ParentJobId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Jobs_RetrieveChaptersJob_MangaId",
|
||||
table: "Jobs",
|
||||
column: "RetrieveChaptersJob_MangaId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Jobs_ToLibraryId",
|
||||
table: "Jobs",
|
||||
column: "ToLibraryId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Jobs_UpdateFilesDownloadedJob_MangaId",
|
||||
table: "Jobs",
|
||||
column: "UpdateFilesDownloadedJob_MangaId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Link_MangaId",
|
||||
name: "IX_Link_MangaKey",
|
||||
table: "Link",
|
||||
column: "MangaId");
|
||||
column: "MangaKey");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_MangaAltTitle_MangaId",
|
||||
table: "MangaAltTitle",
|
||||
column: "MangaId");
|
||||
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",
|
||||
column: "ObjId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Mangas_LibraryId",
|
||||
table: "Mangas",
|
||||
column: "LibraryId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Mangas_MangaConnectorName",
|
||||
table: "Mangas",
|
||||
column: "MangaConnectorName");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_MangaTagToManga_MangaIds",
|
||||
table: "MangaTagToManga",
|
||||
column: "MangaIds");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_MetadataEntries_MangaId",
|
||||
table: "MetadataEntries",
|
||||
column: "MangaId");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "AuthorToManga");
|
||||
name: "AltTitle");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "JobJob");
|
||||
name: "AuthorToManga");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Link");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "MangaAltTitle");
|
||||
name: "MangaConnectorToChapter");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "MangaConnectorToManga");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "MangaTagToManga");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "MetadataEntries");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Authors");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Jobs");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Tags");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Chapters");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "MangaConnectors");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Tags");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "MetadataFetcher");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Mangas");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "LocalLibraries");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "MangaConnectors");
|
||||
name: "FileLibraries");
|
||||
}
|
||||
}
|
||||
}
|
544
API/Migrations/Manga/MangaContextModelSnapshot.cs
Normal file
544
API/Migrations/Manga/MangaContextModelSnapshot.cs
Normal file
@ -0,0 +1,544 @@
|
||||
// <auto-generated />
|
||||
using API.Schema.MangaContext;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace API.Migrations.Manga
|
||||
{
|
||||
[DbContext(typeof(MangaContext))]
|
||||
partial class MangaContextModelSnapshot : ModelSnapshot
|
||||
{
|
||||
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "9.0.5")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("API.Schema.MangaContext.Author", b =>
|
||||
{
|
||||
b.Property<string>("Key")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("AuthorName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("character varying(128)");
|
||||
|
||||
b.HasKey("Key");
|
||||
|
||||
b.ToTable("Authors");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.MangaContext.Chapter", b =>
|
||||
{
|
||||
b.Property<string>("Key")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("ChapterNumber")
|
||||
.IsRequired()
|
||||
.HasMaxLength(10)
|
||||
.HasColumnType("character varying(10)");
|
||||
|
||||
b.Property<bool>("Downloaded")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("FileName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<string>("ParentMangaId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<int?>("VolumeNumber")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Key");
|
||||
|
||||
b.HasIndex("ParentMangaId");
|
||||
|
||||
b.ToTable("Chapters");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.MangaContext.FileLibrary", b =>
|
||||
{
|
||||
b.Property<string>("Key")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("BasePath")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<string>("LibraryName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("character varying(512)");
|
||||
|
||||
b.HasKey("Key");
|
||||
|
||||
b.ToTable("FileLibraries");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.MangaContext.Manga", b =>
|
||||
{
|
||||
b.Property<string>("Key")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("CoverFileNameInCache")
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("character varying(512)");
|
||||
|
||||
b.Property<string>("CoverUrl")
|
||||
.IsRequired()
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("character varying(512)");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("DirectoryName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)");
|
||||
|
||||
b.Property<float>("IgnoreChaptersBefore")
|
||||
.HasColumnType("real");
|
||||
|
||||
b.Property<string>("LibraryId")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("character varying(512)");
|
||||
|
||||
b.Property<string>("OriginalLanguage")
|
||||
.HasMaxLength(8)
|
||||
.HasColumnType("character varying(8)");
|
||||
|
||||
b.Property<byte>("ReleaseStatus")
|
||||
.HasColumnType("smallint");
|
||||
|
||||
b.Property<long?>("Year")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.HasKey("Key");
|
||||
|
||||
b.HasIndex("LibraryId");
|
||||
|
||||
b.ToTable("Mangas");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.MangaContext.MangaConnectorId<API.Schema.MangaContext.Chapter>", b =>
|
||||
{
|
||||
b.Property<string>("Key")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("IdOnConnectorSite")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<string>("MangaConnectorName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)");
|
||||
|
||||
b.Property<string>("ObjId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<bool>("UseForDownload")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("WebsiteUrl")
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("character varying(512)");
|
||||
|
||||
b.HasKey("Key");
|
||||
|
||||
b.HasIndex("MangaConnectorName");
|
||||
|
||||
b.HasIndex("ObjId");
|
||||
|
||||
b.ToTable("MangaConnectorToChapter");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.MangaContext.MangaConnectorId<API.Schema.MangaContext.Manga>", b =>
|
||||
{
|
||||
b.Property<string>("Key")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("IdOnConnectorSite")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<string>("MangaConnectorName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)");
|
||||
|
||||
b.Property<string>("ObjId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<bool>("UseForDownload")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("WebsiteUrl")
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("character varying(512)");
|
||||
|
||||
b.HasKey("Key");
|
||||
|
||||
b.HasIndex("MangaConnectorName");
|
||||
|
||||
b.HasIndex("ObjId");
|
||||
|
||||
b.ToTable("MangaConnectorToManga");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.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")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.HasKey("Tag");
|
||||
|
||||
b.ToTable("Tags");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.MangaContext.MetadataFetchers.MetadataEntry", b =>
|
||||
{
|
||||
b.Property<string>("MetadataFetcherName")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Identifier")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("MangaId")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("MetadataFetcherName", "Identifier");
|
||||
|
||||
b.HasIndex("MangaId");
|
||||
|
||||
b.ToTable("MetadataEntries");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.MangaContext.MetadataFetchers.MetadataFetcher", b =>
|
||||
{
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("MetadataEntry")
|
||||
.IsRequired()
|
||||
.HasMaxLength(21)
|
||||
.HasColumnType("character varying(21)");
|
||||
|
||||
b.HasKey("Name");
|
||||
|
||||
b.ToTable("MetadataFetcher");
|
||||
|
||||
b.HasDiscriminator<string>("MetadataEntry").HasValue("MetadataFetcher");
|
||||
|
||||
b.UseTphMappingStrategy();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AuthorToManga", b =>
|
||||
{
|
||||
b.Property<string>("AuthorIds")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("MangaIds")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("AuthorIds", "MangaIds");
|
||||
|
||||
b.HasIndex("MangaIds");
|
||||
|
||||
b.ToTable("AuthorToManga");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MangaTagToManga", b =>
|
||||
{
|
||||
b.Property<string>("MangaTagIds")
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<string>("MangaIds")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("MangaTagIds", "MangaIds");
|
||||
|
||||
b.HasIndex("MangaIds");
|
||||
|
||||
b.ToTable("MangaTagToManga");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.MangaContext.MangaConnectors.ComickIo", b =>
|
||||
{
|
||||
b.HasBaseType("API.Schema.MangaContext.MangaConnectors.MangaConnector");
|
||||
|
||||
b.HasDiscriminator().HasValue("ComickIo");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.MangaContext.MangaConnectors.Global", b =>
|
||||
{
|
||||
b.HasBaseType("API.Schema.MangaContext.MangaConnectors.MangaConnector");
|
||||
|
||||
b.HasDiscriminator().HasValue("Global");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.MangaContext.MangaConnectors.MangaDex", b =>
|
||||
{
|
||||
b.HasBaseType("API.Schema.MangaContext.MangaConnectors.MangaConnector");
|
||||
|
||||
b.HasDiscriminator().HasValue("MangaDex");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.MangaContext.MetadataFetchers.MyAnimeList", b =>
|
||||
{
|
||||
b.HasBaseType("API.Schema.MangaContext.MetadataFetchers.MetadataFetcher");
|
||||
|
||||
b.HasDiscriminator().HasValue("MyAnimeList");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.MangaContext.Chapter", b =>
|
||||
{
|
||||
b.HasOne("API.Schema.MangaContext.Manga", "ParentManga")
|
||||
.WithMany("Chapters")
|
||||
.HasForeignKey("ParentMangaId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("ParentManga");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.MangaContext.Manga", b =>
|
||||
{
|
||||
b.HasOne("API.Schema.MangaContext.FileLibrary", "Library")
|
||||
.WithMany()
|
||||
.HasForeignKey("LibraryId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.OwnsMany("API.Schema.MangaContext.AltTitle", "AltTitles", b1 =>
|
||||
{
|
||||
b1.Property<string>("Key")
|
||||
.HasColumnType("text");
|
||||
|
||||
b1.Property<string>("Language")
|
||||
.IsRequired()
|
||||
.HasMaxLength(8)
|
||||
.HasColumnType("character varying(8)");
|
||||
|
||||
b1.Property<string>("MangaKey")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b1.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b1.HasKey("Key");
|
||||
|
||||
b1.HasIndex("MangaKey");
|
||||
|
||||
b1.ToTable("AltTitle");
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("MangaKey");
|
||||
});
|
||||
|
||||
b.OwnsMany("API.Schema.MangaContext.Link", "Links", b1 =>
|
||||
{
|
||||
b1.Property<string>("Key")
|
||||
.HasColumnType("text");
|
||||
|
||||
b1.Property<string>("LinkProvider")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b1.Property<string>("LinkUrl")
|
||||
.IsRequired()
|
||||
.HasMaxLength(2048)
|
||||
.HasColumnType("character varying(2048)");
|
||||
|
||||
b1.Property<string>("MangaKey")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b1.HasKey("Key");
|
||||
|
||||
b1.HasIndex("MangaKey");
|
||||
|
||||
b1.ToTable("Link");
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("MangaKey");
|
||||
});
|
||||
|
||||
b.Navigation("AltTitles");
|
||||
|
||||
b.Navigation("Library");
|
||||
|
||||
b.Navigation("Links");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.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)
|
||||
.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");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.MangaContext.MetadataFetchers.MetadataEntry", b =>
|
||||
{
|
||||
b.HasOne("API.Schema.MangaContext.Manga", "Manga")
|
||||
.WithMany()
|
||||
.HasForeignKey("MangaId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("API.Schema.MangaContext.MetadataFetchers.MetadataFetcher", "MetadataFetcher")
|
||||
.WithMany()
|
||||
.HasForeignKey("MetadataFetcherName")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Manga");
|
||||
|
||||
b.Navigation("MetadataFetcher");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AuthorToManga", b =>
|
||||
{
|
||||
b.HasOne("API.Schema.MangaContext.Author", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("AuthorIds")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("API.Schema.MangaContext.Manga", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("MangaIds")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MangaTagToManga", b =>
|
||||
{
|
||||
b.HasOne("API.Schema.MangaContext.Manga", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("MangaIds")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("API.Schema.MangaContext.MangaTag", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("MangaTagIds")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.MangaContext.Chapter", b =>
|
||||
{
|
||||
b.Navigation("MangaConnectorIds");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.MangaContext.Manga", b =>
|
||||
{
|
||||
b.Navigation("Chapters");
|
||||
|
||||
b.Navigation("MangaConnectorIds");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using API.Schema.Contexts;
|
||||
using API.Schema.NotificationsContext;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
@ -10,10 +10,10 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace API.Migrations.notifications
|
||||
namespace API.Migrations.Notifications
|
||||
{
|
||||
[DbContext(typeof(NotificationsContext))]
|
||||
[Migration("20250515120746_Initial")]
|
||||
[Migration("20250703191820_Initial")]
|
||||
partial class Initial
|
||||
{
|
||||
/// <inheritdoc />
|
||||
@ -21,21 +21,23 @@ namespace API.Migrations.notifications
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "9.0.3")
|
||||
.HasAnnotation("ProductVersion", "9.0.5")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "hstore");
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("API.Schema.Notification", b =>
|
||||
modelBuilder.Entity("API.Schema.NotificationsContext.Notification", b =>
|
||||
{
|
||||
b.Property<string>("NotificationId")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
b.Property<string>("Key")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTime>("Date")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<bool>("IsSent")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("Message")
|
||||
.IsRequired()
|
||||
.HasMaxLength(512)
|
||||
@ -49,12 +51,12 @@ namespace API.Migrations.notifications
|
||||
b.Property<byte>("Urgency")
|
||||
.HasColumnType("smallint");
|
||||
|
||||
b.HasKey("NotificationId");
|
||||
b.HasKey("Key");
|
||||
|
||||
b.ToTable("Notifications");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.NotificationConnectors.NotificationConnector", b =>
|
||||
modelBuilder.Entity("API.Schema.NotificationsContext.NotificationConnectors.NotificationConnector", b =>
|
||||
{
|
||||
b.Property<string>("Name")
|
||||
.HasMaxLength(64)
|
@ -4,7 +4,7 @@ using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace API.Migrations.notifications
|
||||
namespace API.Migrations.Notifications
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class Initial : Migration
|
||||
@ -34,15 +34,16 @@ namespace API.Migrations.notifications
|
||||
name: "Notifications",
|
||||
columns: table => new
|
||||
{
|
||||
NotificationId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
|
||||
Key = table.Column<string>(type: "text", nullable: false),
|
||||
Urgency = table.Column<byte>(type: "smallint", nullable: false),
|
||||
Title = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: false),
|
||||
Message = table.Column<string>(type: "character varying(512)", maxLength: 512, nullable: false),
|
||||
Date = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
|
||||
Date = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||
IsSent = table.Column<bool>(type: "boolean", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Notifications", x => x.NotificationId);
|
||||
table.PrimaryKey("PK_Notifications", x => x.Key);
|
||||
});
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using API.Schema.Contexts;
|
||||
using API.Schema.NotificationsContext;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
@ -9,7 +9,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace API.Migrations.notifications
|
||||
namespace API.Migrations.Notifications
|
||||
{
|
||||
[DbContext(typeof(NotificationsContext))]
|
||||
partial class NotificationsContextModelSnapshot : ModelSnapshot
|
||||
@ -18,21 +18,23 @@ namespace API.Migrations.notifications
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "9.0.3")
|
||||
.HasAnnotation("ProductVersion", "9.0.5")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "hstore");
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("API.Schema.Notification", b =>
|
||||
modelBuilder.Entity("API.Schema.NotificationsContext.Notification", b =>
|
||||
{
|
||||
b.Property<string>("NotificationId")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
b.Property<string>("Key")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTime>("Date")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<bool>("IsSent")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("Message")
|
||||
.IsRequired()
|
||||
.HasMaxLength(512)
|
||||
@ -46,12 +48,12 @@ namespace API.Migrations.notifications
|
||||
b.Property<byte>("Urgency")
|
||||
.HasColumnType("smallint");
|
||||
|
||||
b.HasKey("NotificationId");
|
||||
b.HasKey("Key");
|
||||
|
||||
b.ToTable("Notifications");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.NotificationConnectors.NotificationConnector", b =>
|
||||
modelBuilder.Entity("API.Schema.NotificationsContext.NotificationConnectors.NotificationConnector", b =>
|
||||
{
|
||||
b.Property<string>("Name")
|
||||
.HasMaxLength(64)
|
@ -1,682 +0,0 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using API.Schema.Contexts;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace API.Migrations.pgsql
|
||||
{
|
||||
[DbContext(typeof(PgsqlContext))]
|
||||
[Migration("20250515120724_Initial-1")]
|
||||
partial class Initial1
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "9.0.3")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("API.Schema.Author", b =>
|
||||
{
|
||||
b.Property<string>("AuthorId")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<string>("AuthorName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("character varying(128)");
|
||||
|
||||
b.HasKey("AuthorId");
|
||||
|
||||
b.ToTable("Authors");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.Chapter", b =>
|
||||
{
|
||||
b.Property<string>("ChapterId")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<string>("ChapterNumber")
|
||||
.IsRequired()
|
||||
.HasMaxLength(10)
|
||||
.HasColumnType("character varying(10)");
|
||||
|
||||
b.Property<bool>("Downloaded")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("FileName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<string>("ParentMangaId")
|
||||
.IsRequired()
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<string>("Url")
|
||||
.IsRequired()
|
||||
.HasMaxLength(2048)
|
||||
.HasColumnType("character varying(2048)");
|
||||
|
||||
b.Property<int?>("VolumeNumber")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("ChapterId");
|
||||
|
||||
b.HasIndex("ParentMangaId");
|
||||
|
||||
b.ToTable("Chapters");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.Jobs.Job", b =>
|
||||
{
|
||||
b.Property<string>("JobId")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<bool>("Enabled")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<byte>("JobType")
|
||||
.HasColumnType("smallint");
|
||||
|
||||
b.Property<DateTime>("LastExecution")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("ParentJobId")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<decimal>("RecurrenceMs")
|
||||
.HasColumnType("numeric(20,0)");
|
||||
|
||||
b.Property<byte>("state")
|
||||
.HasColumnType("smallint");
|
||||
|
||||
b.HasKey("JobId");
|
||||
|
||||
b.HasIndex("ParentJobId");
|
||||
|
||||
b.ToTable("Jobs");
|
||||
|
||||
b.HasDiscriminator<byte>("JobType");
|
||||
|
||||
b.UseTphMappingStrategy();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.LocalLibrary", b =>
|
||||
{
|
||||
b.Property<string>("LocalLibraryId")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<string>("BasePath")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<string>("LibraryName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("character varying(512)");
|
||||
|
||||
b.HasKey("LocalLibraryId");
|
||||
|
||||
b.ToTable("LocalLibraries");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.Manga", b =>
|
||||
{
|
||||
b.Property<string>("MangaId")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<string>("CoverFileNameInCache")
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("character varying(512)");
|
||||
|
||||
b.Property<string>("CoverUrl")
|
||||
.IsRequired()
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("character varying(512)");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("DirectoryName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)");
|
||||
|
||||
b.Property<string>("IdOnConnectorSite")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<float>("IgnoreChaptersBefore")
|
||||
.HasColumnType("real");
|
||||
|
||||
b.Property<string>("LibraryId")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<string>("MangaConnectorName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("character varying(512)");
|
||||
|
||||
b.Property<string>("OriginalLanguage")
|
||||
.HasMaxLength(8)
|
||||
.HasColumnType("character varying(8)");
|
||||
|
||||
b.Property<byte>("ReleaseStatus")
|
||||
.HasColumnType("smallint");
|
||||
|
||||
b.Property<string>("WebsiteUrl")
|
||||
.IsRequired()
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("character varying(512)");
|
||||
|
||||
b.Property<long>("Year")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.HasKey("MangaId");
|
||||
|
||||
b.HasIndex("LibraryId");
|
||||
|
||||
b.HasIndex("MangaConnectorName");
|
||||
|
||||
b.ToTable("Mangas");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.MangaConnectors.MangaConnector", b =>
|
||||
{
|
||||
b.Property<string>("Name")
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)");
|
||||
|
||||
b.PrimitiveCollection<string[]>("BaseUris")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("text[]");
|
||||
|
||||
b.Property<bool>("Enabled")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("IconUrl")
|
||||
.IsRequired()
|
||||
.HasMaxLength(2048)
|
||||
.HasColumnType("character varying(2048)");
|
||||
|
||||
b.PrimitiveCollection<string[]>("SupportedLanguages")
|
||||
.IsRequired()
|
||||
.HasMaxLength(8)
|
||||
.HasColumnType("text[]");
|
||||
|
||||
b.HasKey("Name");
|
||||
|
||||
b.ToTable("MangaConnectors");
|
||||
|
||||
b.HasDiscriminator<string>("Name").HasValue("MangaConnector");
|
||||
|
||||
b.UseTphMappingStrategy();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.MangaTag", b =>
|
||||
{
|
||||
b.Property<string>("Tag")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.HasKey("Tag");
|
||||
|
||||
b.ToTable("Tags");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AuthorToManga", b =>
|
||||
{
|
||||
b.Property<string>("AuthorIds")
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<string>("MangaIds")
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.HasKey("AuthorIds", "MangaIds");
|
||||
|
||||
b.HasIndex("MangaIds");
|
||||
|
||||
b.ToTable("AuthorToManga");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("JobJob", b =>
|
||||
{
|
||||
b.Property<string>("DependsOnJobsJobId")
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<string>("JobId")
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.HasKey("DependsOnJobsJobId", "JobId");
|
||||
|
||||
b.HasIndex("JobId");
|
||||
|
||||
b.ToTable("JobJob");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MangaTagToManga", b =>
|
||||
{
|
||||
b.Property<string>("MangaTagIds")
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<string>("MangaIds")
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.HasKey("MangaTagIds", "MangaIds");
|
||||
|
||||
b.HasIndex("MangaIds");
|
||||
|
||||
b.ToTable("MangaTagToManga");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.Jobs.DownloadAvailableChaptersJob", b =>
|
||||
{
|
||||
b.HasBaseType("API.Schema.Jobs.Job");
|
||||
|
||||
b.Property<string>("MangaId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.HasIndex("MangaId");
|
||||
|
||||
b.ToTable("Jobs", t =>
|
||||
{
|
||||
t.Property("MangaId")
|
||||
.HasColumnName("DownloadAvailableChaptersJob_MangaId");
|
||||
});
|
||||
|
||||
b.HasDiscriminator().HasValue((byte)1);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.Jobs.DownloadMangaCoverJob", b =>
|
||||
{
|
||||
b.HasBaseType("API.Schema.Jobs.Job");
|
||||
|
||||
b.Property<string>("MangaId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.HasIndex("MangaId");
|
||||
|
||||
b.HasDiscriminator().HasValue((byte)4);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.Jobs.DownloadSingleChapterJob", b =>
|
||||
{
|
||||
b.HasBaseType("API.Schema.Jobs.Job");
|
||||
|
||||
b.Property<string>("ChapterId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.HasIndex("ChapterId");
|
||||
|
||||
b.HasDiscriminator().HasValue((byte)0);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.Jobs.MoveFileOrFolderJob", b =>
|
||||
{
|
||||
b.HasBaseType("API.Schema.Jobs.Job");
|
||||
|
||||
b.Property<string>("FromLocation")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<string>("ToLocation")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.HasDiscriminator().HasValue((byte)3);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.Jobs.MoveMangaLibraryJob", b =>
|
||||
{
|
||||
b.HasBaseType("API.Schema.Jobs.Job");
|
||||
|
||||
b.Property<string>("MangaId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<string>("ToLibraryId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.HasIndex("MangaId");
|
||||
|
||||
b.HasIndex("ToLibraryId");
|
||||
|
||||
b.ToTable("Jobs", t =>
|
||||
{
|
||||
t.Property("MangaId")
|
||||
.HasColumnName("MoveMangaLibraryJob_MangaId");
|
||||
});
|
||||
|
||||
b.HasDiscriminator().HasValue((byte)7);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.Jobs.RetrieveChaptersJob", b =>
|
||||
{
|
||||
b.HasBaseType("API.Schema.Jobs.Job");
|
||||
|
||||
b.Property<string>("Language")
|
||||
.IsRequired()
|
||||
.HasMaxLength(8)
|
||||
.HasColumnType("character varying(8)");
|
||||
|
||||
b.Property<string>("MangaId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.HasIndex("MangaId");
|
||||
|
||||
b.ToTable("Jobs", t =>
|
||||
{
|
||||
t.Property("MangaId")
|
||||
.HasColumnName("RetrieveChaptersJob_MangaId");
|
||||
});
|
||||
|
||||
b.HasDiscriminator().HasValue((byte)5);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.Jobs.UpdateFilesDownloadedJob", b =>
|
||||
{
|
||||
b.HasBaseType("API.Schema.Jobs.Job");
|
||||
|
||||
b.Property<string>("MangaId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.HasIndex("MangaId");
|
||||
|
||||
b.ToTable("Jobs", t =>
|
||||
{
|
||||
t.Property("MangaId")
|
||||
.HasColumnName("UpdateFilesDownloadedJob_MangaId");
|
||||
});
|
||||
|
||||
b.HasDiscriminator().HasValue((byte)6);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.MangaConnectors.Global", b =>
|
||||
{
|
||||
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
|
||||
|
||||
b.HasDiscriminator().HasValue("Global");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.MangaConnectors.MangaDex", b =>
|
||||
{
|
||||
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
|
||||
|
||||
b.HasDiscriminator().HasValue("MangaDex");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.Chapter", b =>
|
||||
{
|
||||
b.HasOne("API.Schema.Manga", "ParentManga")
|
||||
.WithMany("Chapters")
|
||||
.HasForeignKey("ParentMangaId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("ParentManga");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.Jobs.Job", b =>
|
||||
{
|
||||
b.HasOne("API.Schema.Jobs.Job", "ParentJob")
|
||||
.WithMany()
|
||||
.HasForeignKey("ParentJobId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
b.Navigation("ParentJob");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.Manga", b =>
|
||||
{
|
||||
b.HasOne("API.Schema.LocalLibrary", "Library")
|
||||
.WithMany()
|
||||
.HasForeignKey("LibraryId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.HasOne("API.Schema.MangaConnectors.MangaConnector", "MangaConnector")
|
||||
.WithMany()
|
||||
.HasForeignKey("MangaConnectorName")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.OwnsMany("API.Schema.Link", "Links", b1 =>
|
||||
{
|
||||
b1.Property<string>("LinkId")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b1.Property<string>("LinkProvider")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b1.Property<string>("LinkUrl")
|
||||
.IsRequired()
|
||||
.HasMaxLength(2048)
|
||||
.HasColumnType("character varying(2048)");
|
||||
|
||||
b1.Property<string>("MangaId")
|
||||
.IsRequired()
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b1.HasKey("LinkId");
|
||||
|
||||
b1.HasIndex("MangaId");
|
||||
|
||||
b1.ToTable("Link");
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("MangaId");
|
||||
});
|
||||
|
||||
b.OwnsMany("API.Schema.MangaAltTitle", "AltTitles", b1 =>
|
||||
{
|
||||
b1.Property<string>("AltTitleId")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b1.Property<string>("Language")
|
||||
.IsRequired()
|
||||
.HasMaxLength(8)
|
||||
.HasColumnType("character varying(8)");
|
||||
|
||||
b1.Property<string>("MangaId")
|
||||
.IsRequired()
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b1.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b1.HasKey("AltTitleId");
|
||||
|
||||
b1.HasIndex("MangaId");
|
||||
|
||||
b1.ToTable("MangaAltTitle");
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("MangaId");
|
||||
});
|
||||
|
||||
b.Navigation("AltTitles");
|
||||
|
||||
b.Navigation("Library");
|
||||
|
||||
b.Navigation("Links");
|
||||
|
||||
b.Navigation("MangaConnector");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AuthorToManga", b =>
|
||||
{
|
||||
b.HasOne("API.Schema.Author", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("AuthorIds")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("API.Schema.Manga", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("MangaIds")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("JobJob", b =>
|
||||
{
|
||||
b.HasOne("API.Schema.Jobs.Job", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("DependsOnJobsJobId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("API.Schema.Jobs.Job", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("JobId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MangaTagToManga", b =>
|
||||
{
|
||||
b.HasOne("API.Schema.Manga", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("MangaIds")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("API.Schema.MangaTag", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("MangaTagIds")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.Jobs.DownloadAvailableChaptersJob", b =>
|
||||
{
|
||||
b.HasOne("API.Schema.Manga", "Manga")
|
||||
.WithMany()
|
||||
.HasForeignKey("MangaId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Manga");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.Jobs.DownloadMangaCoverJob", b =>
|
||||
{
|
||||
b.HasOne("API.Schema.Manga", "Manga")
|
||||
.WithMany()
|
||||
.HasForeignKey("MangaId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Manga");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.Jobs.DownloadSingleChapterJob", b =>
|
||||
{
|
||||
b.HasOne("API.Schema.Chapter", "Chapter")
|
||||
.WithMany()
|
||||
.HasForeignKey("ChapterId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Chapter");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.Jobs.MoveMangaLibraryJob", b =>
|
||||
{
|
||||
b.HasOne("API.Schema.Manga", "Manga")
|
||||
.WithMany()
|
||||
.HasForeignKey("MangaId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("API.Schema.LocalLibrary", "ToLibrary")
|
||||
.WithMany()
|
||||
.HasForeignKey("ToLibraryId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Manga");
|
||||
|
||||
b.Navigation("ToLibrary");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.Jobs.RetrieveChaptersJob", b =>
|
||||
{
|
||||
b.HasOne("API.Schema.Manga", "Manga")
|
||||
.WithMany()
|
||||
.HasForeignKey("MangaId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Manga");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.Jobs.UpdateFilesDownloadedJob", b =>
|
||||
{
|
||||
b.HasOne("API.Schema.Manga", "Manga")
|
||||
.WithMany()
|
||||
.HasForeignKey("MangaId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Manga");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.Manga", b =>
|
||||
{
|
||||
b.Navigation("Chapters");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
@ -1,679 +0,0 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using API.Schema.Contexts;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace API.Migrations.pgsql
|
||||
{
|
||||
[DbContext(typeof(PgsqlContext))]
|
||||
partial class PgsqlContextModelSnapshot : ModelSnapshot
|
||||
{
|
||||
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "9.0.3")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("API.Schema.Author", b =>
|
||||
{
|
||||
b.Property<string>("AuthorId")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<string>("AuthorName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("character varying(128)");
|
||||
|
||||
b.HasKey("AuthorId");
|
||||
|
||||
b.ToTable("Authors");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.Chapter", b =>
|
||||
{
|
||||
b.Property<string>("ChapterId")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<string>("ChapterNumber")
|
||||
.IsRequired()
|
||||
.HasMaxLength(10)
|
||||
.HasColumnType("character varying(10)");
|
||||
|
||||
b.Property<bool>("Downloaded")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("FileName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<string>("ParentMangaId")
|
||||
.IsRequired()
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<string>("Url")
|
||||
.IsRequired()
|
||||
.HasMaxLength(2048)
|
||||
.HasColumnType("character varying(2048)");
|
||||
|
||||
b.Property<int?>("VolumeNumber")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("ChapterId");
|
||||
|
||||
b.HasIndex("ParentMangaId");
|
||||
|
||||
b.ToTable("Chapters");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.Jobs.Job", b =>
|
||||
{
|
||||
b.Property<string>("JobId")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<bool>("Enabled")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<byte>("JobType")
|
||||
.HasColumnType("smallint");
|
||||
|
||||
b.Property<DateTime>("LastExecution")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("ParentJobId")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<decimal>("RecurrenceMs")
|
||||
.HasColumnType("numeric(20,0)");
|
||||
|
||||
b.Property<byte>("state")
|
||||
.HasColumnType("smallint");
|
||||
|
||||
b.HasKey("JobId");
|
||||
|
||||
b.HasIndex("ParentJobId");
|
||||
|
||||
b.ToTable("Jobs");
|
||||
|
||||
b.HasDiscriminator<byte>("JobType");
|
||||
|
||||
b.UseTphMappingStrategy();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.LocalLibrary", b =>
|
||||
{
|
||||
b.Property<string>("LocalLibraryId")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<string>("BasePath")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<string>("LibraryName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("character varying(512)");
|
||||
|
||||
b.HasKey("LocalLibraryId");
|
||||
|
||||
b.ToTable("LocalLibraries");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.Manga", b =>
|
||||
{
|
||||
b.Property<string>("MangaId")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<string>("CoverFileNameInCache")
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("character varying(512)");
|
||||
|
||||
b.Property<string>("CoverUrl")
|
||||
.IsRequired()
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("character varying(512)");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("DirectoryName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)");
|
||||
|
||||
b.Property<string>("IdOnConnectorSite")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<float>("IgnoreChaptersBefore")
|
||||
.HasColumnType("real");
|
||||
|
||||
b.Property<string>("LibraryId")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<string>("MangaConnectorName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("character varying(512)");
|
||||
|
||||
b.Property<string>("OriginalLanguage")
|
||||
.HasMaxLength(8)
|
||||
.HasColumnType("character varying(8)");
|
||||
|
||||
b.Property<byte>("ReleaseStatus")
|
||||
.HasColumnType("smallint");
|
||||
|
||||
b.Property<string>("WebsiteUrl")
|
||||
.IsRequired()
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("character varying(512)");
|
||||
|
||||
b.Property<long>("Year")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.HasKey("MangaId");
|
||||
|
||||
b.HasIndex("LibraryId");
|
||||
|
||||
b.HasIndex("MangaConnectorName");
|
||||
|
||||
b.ToTable("Mangas");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.MangaConnectors.MangaConnector", b =>
|
||||
{
|
||||
b.Property<string>("Name")
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)");
|
||||
|
||||
b.PrimitiveCollection<string[]>("BaseUris")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("text[]");
|
||||
|
||||
b.Property<bool>("Enabled")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("IconUrl")
|
||||
.IsRequired()
|
||||
.HasMaxLength(2048)
|
||||
.HasColumnType("character varying(2048)");
|
||||
|
||||
b.PrimitiveCollection<string[]>("SupportedLanguages")
|
||||
.IsRequired()
|
||||
.HasMaxLength(8)
|
||||
.HasColumnType("text[]");
|
||||
|
||||
b.HasKey("Name");
|
||||
|
||||
b.ToTable("MangaConnectors");
|
||||
|
||||
b.HasDiscriminator<string>("Name").HasValue("MangaConnector");
|
||||
|
||||
b.UseTphMappingStrategy();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.MangaTag", b =>
|
||||
{
|
||||
b.Property<string>("Tag")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.HasKey("Tag");
|
||||
|
||||
b.ToTable("Tags");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AuthorToManga", b =>
|
||||
{
|
||||
b.Property<string>("AuthorIds")
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<string>("MangaIds")
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.HasKey("AuthorIds", "MangaIds");
|
||||
|
||||
b.HasIndex("MangaIds");
|
||||
|
||||
b.ToTable("AuthorToManga");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("JobJob", b =>
|
||||
{
|
||||
b.Property<string>("DependsOnJobsJobId")
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<string>("JobId")
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.HasKey("DependsOnJobsJobId", "JobId");
|
||||
|
||||
b.HasIndex("JobId");
|
||||
|
||||
b.ToTable("JobJob");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MangaTagToManga", b =>
|
||||
{
|
||||
b.Property<string>("MangaTagIds")
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<string>("MangaIds")
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.HasKey("MangaTagIds", "MangaIds");
|
||||
|
||||
b.HasIndex("MangaIds");
|
||||
|
||||
b.ToTable("MangaTagToManga");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.Jobs.DownloadAvailableChaptersJob", b =>
|
||||
{
|
||||
b.HasBaseType("API.Schema.Jobs.Job");
|
||||
|
||||
b.Property<string>("MangaId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.HasIndex("MangaId");
|
||||
|
||||
b.ToTable("Jobs", t =>
|
||||
{
|
||||
t.Property("MangaId")
|
||||
.HasColumnName("DownloadAvailableChaptersJob_MangaId");
|
||||
});
|
||||
|
||||
b.HasDiscriminator().HasValue((byte)1);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.Jobs.DownloadMangaCoverJob", b =>
|
||||
{
|
||||
b.HasBaseType("API.Schema.Jobs.Job");
|
||||
|
||||
b.Property<string>("MangaId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.HasIndex("MangaId");
|
||||
|
||||
b.HasDiscriminator().HasValue((byte)4);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.Jobs.DownloadSingleChapterJob", b =>
|
||||
{
|
||||
b.HasBaseType("API.Schema.Jobs.Job");
|
||||
|
||||
b.Property<string>("ChapterId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.HasIndex("ChapterId");
|
||||
|
||||
b.HasDiscriminator().HasValue((byte)0);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.Jobs.MoveFileOrFolderJob", b =>
|
||||
{
|
||||
b.HasBaseType("API.Schema.Jobs.Job");
|
||||
|
||||
b.Property<string>("FromLocation")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<string>("ToLocation")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.HasDiscriminator().HasValue((byte)3);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.Jobs.MoveMangaLibraryJob", b =>
|
||||
{
|
||||
b.HasBaseType("API.Schema.Jobs.Job");
|
||||
|
||||
b.Property<string>("MangaId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<string>("ToLibraryId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.HasIndex("MangaId");
|
||||
|
||||
b.HasIndex("ToLibraryId");
|
||||
|
||||
b.ToTable("Jobs", t =>
|
||||
{
|
||||
t.Property("MangaId")
|
||||
.HasColumnName("MoveMangaLibraryJob_MangaId");
|
||||
});
|
||||
|
||||
b.HasDiscriminator().HasValue((byte)7);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.Jobs.RetrieveChaptersJob", b =>
|
||||
{
|
||||
b.HasBaseType("API.Schema.Jobs.Job");
|
||||
|
||||
b.Property<string>("Language")
|
||||
.IsRequired()
|
||||
.HasMaxLength(8)
|
||||
.HasColumnType("character varying(8)");
|
||||
|
||||
b.Property<string>("MangaId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.HasIndex("MangaId");
|
||||
|
||||
b.ToTable("Jobs", t =>
|
||||
{
|
||||
t.Property("MangaId")
|
||||
.HasColumnName("RetrieveChaptersJob_MangaId");
|
||||
});
|
||||
|
||||
b.HasDiscriminator().HasValue((byte)5);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.Jobs.UpdateFilesDownloadedJob", b =>
|
||||
{
|
||||
b.HasBaseType("API.Schema.Jobs.Job");
|
||||
|
||||
b.Property<string>("MangaId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.HasIndex("MangaId");
|
||||
|
||||
b.ToTable("Jobs", t =>
|
||||
{
|
||||
t.Property("MangaId")
|
||||
.HasColumnName("UpdateFilesDownloadedJob_MangaId");
|
||||
});
|
||||
|
||||
b.HasDiscriminator().HasValue((byte)6);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.MangaConnectors.Global", b =>
|
||||
{
|
||||
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
|
||||
|
||||
b.HasDiscriminator().HasValue("Global");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.MangaConnectors.MangaDex", b =>
|
||||
{
|
||||
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
|
||||
|
||||
b.HasDiscriminator().HasValue("MangaDex");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.Chapter", b =>
|
||||
{
|
||||
b.HasOne("API.Schema.Manga", "ParentManga")
|
||||
.WithMany("Chapters")
|
||||
.HasForeignKey("ParentMangaId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("ParentManga");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.Jobs.Job", b =>
|
||||
{
|
||||
b.HasOne("API.Schema.Jobs.Job", "ParentJob")
|
||||
.WithMany()
|
||||
.HasForeignKey("ParentJobId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
b.Navigation("ParentJob");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.Manga", b =>
|
||||
{
|
||||
b.HasOne("API.Schema.LocalLibrary", "Library")
|
||||
.WithMany()
|
||||
.HasForeignKey("LibraryId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.HasOne("API.Schema.MangaConnectors.MangaConnector", "MangaConnector")
|
||||
.WithMany()
|
||||
.HasForeignKey("MangaConnectorName")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.OwnsMany("API.Schema.Link", "Links", b1 =>
|
||||
{
|
||||
b1.Property<string>("LinkId")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b1.Property<string>("LinkProvider")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b1.Property<string>("LinkUrl")
|
||||
.IsRequired()
|
||||
.HasMaxLength(2048)
|
||||
.HasColumnType("character varying(2048)");
|
||||
|
||||
b1.Property<string>("MangaId")
|
||||
.IsRequired()
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b1.HasKey("LinkId");
|
||||
|
||||
b1.HasIndex("MangaId");
|
||||
|
||||
b1.ToTable("Link");
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("MangaId");
|
||||
});
|
||||
|
||||
b.OwnsMany("API.Schema.MangaAltTitle", "AltTitles", b1 =>
|
||||
{
|
||||
b1.Property<string>("AltTitleId")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b1.Property<string>("Language")
|
||||
.IsRequired()
|
||||
.HasMaxLength(8)
|
||||
.HasColumnType("character varying(8)");
|
||||
|
||||
b1.Property<string>("MangaId")
|
||||
.IsRequired()
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b1.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b1.HasKey("AltTitleId");
|
||||
|
||||
b1.HasIndex("MangaId");
|
||||
|
||||
b1.ToTable("MangaAltTitle");
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("MangaId");
|
||||
});
|
||||
|
||||
b.Navigation("AltTitles");
|
||||
|
||||
b.Navigation("Library");
|
||||
|
||||
b.Navigation("Links");
|
||||
|
||||
b.Navigation("MangaConnector");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AuthorToManga", b =>
|
||||
{
|
||||
b.HasOne("API.Schema.Author", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("AuthorIds")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("API.Schema.Manga", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("MangaIds")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("JobJob", b =>
|
||||
{
|
||||
b.HasOne("API.Schema.Jobs.Job", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("DependsOnJobsJobId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("API.Schema.Jobs.Job", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("JobId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MangaTagToManga", b =>
|
||||
{
|
||||
b.HasOne("API.Schema.Manga", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("MangaIds")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("API.Schema.MangaTag", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("MangaTagIds")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.Jobs.DownloadAvailableChaptersJob", b =>
|
||||
{
|
||||
b.HasOne("API.Schema.Manga", "Manga")
|
||||
.WithMany()
|
||||
.HasForeignKey("MangaId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Manga");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.Jobs.DownloadMangaCoverJob", b =>
|
||||
{
|
||||
b.HasOne("API.Schema.Manga", "Manga")
|
||||
.WithMany()
|
||||
.HasForeignKey("MangaId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Manga");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.Jobs.DownloadSingleChapterJob", b =>
|
||||
{
|
||||
b.HasOne("API.Schema.Chapter", "Chapter")
|
||||
.WithMany()
|
||||
.HasForeignKey("ChapterId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Chapter");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.Jobs.MoveMangaLibraryJob", b =>
|
||||
{
|
||||
b.HasOne("API.Schema.Manga", "Manga")
|
||||
.WithMany()
|
||||
.HasForeignKey("MangaId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("API.Schema.LocalLibrary", "ToLibrary")
|
||||
.WithMany()
|
||||
.HasForeignKey("ToLibraryId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Manga");
|
||||
|
||||
b.Navigation("ToLibrary");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.Jobs.RetrieveChaptersJob", b =>
|
||||
{
|
||||
b.HasOne("API.Schema.Manga", "Manga")
|
||||
.WithMany()
|
||||
.HasForeignKey("MangaId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Manga");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.Jobs.UpdateFilesDownloadedJob", b =>
|
||||
{
|
||||
b.HasOne("API.Schema.Manga", "Manga")
|
||||
.WithMany()
|
||||
.HasForeignKey("MangaId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Manga");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.Manga", b =>
|
||||
{
|
||||
b.Navigation("Chapters");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
@ -1,9 +1,9 @@
|
||||
using System.Reflection;
|
||||
using API;
|
||||
using API.Schema;
|
||||
using API.Schema.Contexts;
|
||||
using API.Schema.Jobs;
|
||||
using API.Schema.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;
|
||||
using Asp.Versioning.Conventions;
|
||||
@ -55,17 +55,17 @@ builder.Services.AddSwaggerGen(opt =>
|
||||
});
|
||||
builder.Services.ConfigureOptions<NamedSwaggerGenOptions>();
|
||||
|
||||
string ConnectionString = $"Host={Environment.GetEnvironmentVariable("POSTGRES_HOST") ?? "localhost:5432"}; " +
|
||||
string connectionString = $"Host={Environment.GetEnvironmentVariable("POSTGRES_HOST") ?? "localhost:5432"}; " +
|
||||
$"Database={Environment.GetEnvironmentVariable("POSTGRES_DB") ?? "postgres"}; " +
|
||||
$"Username={Environment.GetEnvironmentVariable("POSTGRES_USER") ?? "postgres"}; " +
|
||||
$"Password={Environment.GetEnvironmentVariable("POSTGRES_PASSWORD") ?? "postgres"}";
|
||||
|
||||
builder.Services.AddDbContext<PgsqlContext>(options =>
|
||||
options.UseNpgsql(ConnectionString));
|
||||
builder.Services.AddDbContext<MangaContext>(options =>
|
||||
options.UseNpgsql(connectionString));
|
||||
builder.Services.AddDbContext<NotificationsContext>(options =>
|
||||
options.UseNpgsql(ConnectionString));
|
||||
options.UseNpgsql(connectionString));
|
||||
builder.Services.AddDbContext<LibraryContext>(options =>
|
||||
options.UseNpgsql(ConnectionString));
|
||||
options.UseNpgsql(connectionString));
|
||||
|
||||
builder.Services.AddControllers(options =>
|
||||
{
|
||||
@ -97,8 +97,7 @@ app.MapControllers()
|
||||
app.UseSwagger();
|
||||
app.UseSwaggerUI(options =>
|
||||
{
|
||||
options.SwaggerEndpoint(
|
||||
$"/swagger/v2/swagger.json", "v2");
|
||||
options.SwaggerEndpoint($"/swagger/v2/swagger.json", "v2");
|
||||
});
|
||||
|
||||
app.UseHttpsRedirection();
|
||||
@ -107,34 +106,21 @@ app.UseMiddleware<RequestTimeMiddleware>();
|
||||
|
||||
using (IServiceScope scope = app.Services.CreateScope())
|
||||
{
|
||||
PgsqlContext context = scope.ServiceProvider.GetRequiredService<PgsqlContext>();
|
||||
MangaContext context = scope.ServiceProvider.GetRequiredService<MangaContext>();
|
||||
context.Database.Migrate();
|
||||
|
||||
MangaConnector[] connectors =
|
||||
[
|
||||
new MangaDex(),
|
||||
new Global(scope.ServiceProvider.GetService<PgsqlContext>()!)
|
||||
new ComickIo(),
|
||||
new Global(scope.ServiceProvider.GetService<MangaContext>()!)
|
||||
];
|
||||
MangaConnector[] newConnectors = connectors.Where(c => !context.MangaConnectors.Contains(c)).ToArray();
|
||||
context.MangaConnectors.AddRange(newConnectors);
|
||||
if (!context.LocalLibraries.Any())
|
||||
context.LocalLibraries.Add(new LocalLibrary(TrangaSettings.downloadLocation, "Default Library"));
|
||||
if (!context.FileLibraries.Any())
|
||||
context.FileLibraries.Add(new FileLibrary(Tranga.Settings.DownloadLocation, "Default FileLibrary"));
|
||||
|
||||
context.Jobs.AddRange(context.Jobs.Where(j => j.JobType == JobType.DownloadAvailableChaptersJob)
|
||||
.AsEnumerable()
|
||||
.Select(dacj =>
|
||||
{
|
||||
DownloadAvailableChaptersJob? j = dacj as DownloadAvailableChaptersJob;
|
||||
return new UpdateFilesDownloadedJob(j!.Manga, 0);
|
||||
}));
|
||||
context.Jobs.RemoveRange(context.Jobs.Where(j => j.state == JobState.Completed && j.RecurrenceMs < 1));
|
||||
foreach (Job job in context.Jobs.Where(j => j.state == JobState.Running))
|
||||
{
|
||||
job.state = JobState.FirstExecution;
|
||||
job.LastExecution = DateTime.UnixEpoch;
|
||||
}
|
||||
|
||||
context.SaveChanges();
|
||||
context.Sync();
|
||||
}
|
||||
|
||||
using (IServiceScope scope = app.Services.CreateScope())
|
||||
@ -142,17 +128,24 @@ using (IServiceScope scope = app.Services.CreateScope())
|
||||
NotificationsContext context = scope.ServiceProvider.GetRequiredService<NotificationsContext>();
|
||||
context.Database.Migrate();
|
||||
|
||||
context.Notifications.RemoveRange(context.Notifications);
|
||||
string[] emojis = { "(•‿•)", "(づ \u25d5‿\u25d5 )づ", "( \u02d8\u25bd\u02d8)っ\u2668", "=\uff3e\u25cf \u22cf \u25cf\uff3e=", "(ΦωΦ)", "(\u272a\u3268\u272a)", "( ノ・o・ )ノ", "(〜^\u2207^ )〜", "~(\u2267ω\u2266)~","૮ \u00b4• ﻌ \u00b4• ა", "(\u02c3ᆺ\u02c2)", "(=\ud83d\udf66 \u0f1d \ud83d\udf66=)"};
|
||||
context.Notifications.Add(new Notification("Tranga Started", emojis[Random.Shared.Next(0, emojis.Length - 1)], NotificationUrgency.High));
|
||||
|
||||
context.SaveChanges();
|
||||
context.Sync();
|
||||
}
|
||||
|
||||
using (IServiceScope scope = app.Services.CreateScope())
|
||||
{
|
||||
LibraryContext context = scope.ServiceProvider.GetRequiredService<LibraryContext>();
|
||||
context.Database.Migrate();
|
||||
|
||||
context.Sync();
|
||||
}
|
||||
|
||||
TrangaSettings.Load();
|
||||
Tranga.StartLogger();
|
||||
Tranga.JobStarterThread.Start(app.Services);
|
||||
Tranga.NotificationSenderThread.Start(app.Services);
|
||||
|
||||
Tranga.PeriodicWorkerStarterThread.Start(app.Services);
|
||||
|
||||
app.UseCors("AllowAll");
|
||||
|
||||
|
@ -1,20 +0,0 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace API.Schema;
|
||||
|
||||
[PrimaryKey("AuthorId")]
|
||||
public class Author(string authorName)
|
||||
{
|
||||
[StringLength(64)]
|
||||
[Required]
|
||||
public string AuthorId { get; init; } = TokenGen.CreateToken(typeof(Author), authorName);
|
||||
[StringLength(128)]
|
||||
[Required]
|
||||
public string AuthorName { get; init; } = authorName;
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{AuthorId} {AuthorName}";
|
||||
}
|
||||
}
|
@ -1,189 +0,0 @@
|
||||
using API.Schema.Jobs;
|
||||
using API.Schema.LibraryConnectors;
|
||||
using API.Schema.MangaConnectors;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace API.Schema.Contexts;
|
||||
|
||||
public class PgsqlContext(DbContextOptions<PgsqlContext> options) : DbContext(options)
|
||||
{
|
||||
public DbSet<Job> Jobs { get; set; }
|
||||
public DbSet<MangaConnector> MangaConnectors { get; set; }
|
||||
public DbSet<Manga> Mangas { get; set; }
|
||||
public DbSet<LocalLibrary> LocalLibraries { get; set; }
|
||||
public DbSet<Chapter> Chapters { get; set; }
|
||||
public DbSet<Author> Authors { get; set; }
|
||||
public DbSet<MangaTag> Tags { get; set; }
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
//Job Types
|
||||
modelBuilder.Entity<Job>()
|
||||
.HasDiscriminator(j => j.JobType)
|
||||
.HasValue<MoveFileOrFolderJob>(JobType.MoveFileOrFolderJob)
|
||||
.HasValue<MoveMangaLibraryJob>(JobType.MoveMangaLibraryJob)
|
||||
.HasValue<DownloadAvailableChaptersJob>(JobType.DownloadAvailableChaptersJob)
|
||||
.HasValue<DownloadSingleChapterJob>(JobType.DownloadSingleChapterJob)
|
||||
.HasValue<DownloadMangaCoverJob>(JobType.DownloadMangaCoverJob)
|
||||
.HasValue<RetrieveChaptersJob>(JobType.RetrieveChaptersJob)
|
||||
.HasValue<UpdateFilesDownloadedJob>(JobType.UpdateFilesDownloadedJob);
|
||||
|
||||
//Job specification
|
||||
modelBuilder.Entity<DownloadAvailableChaptersJob>()
|
||||
.HasOne<Manga>(j => j.Manga)
|
||||
.WithMany()
|
||||
.HasForeignKey(j => j.MangaId)
|
||||
.IsRequired()
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
modelBuilder.Entity<DownloadAvailableChaptersJob>()
|
||||
.Navigation(j => j.Manga)
|
||||
.AutoInclude();
|
||||
modelBuilder.Entity<DownloadMangaCoverJob>()
|
||||
.HasOne<Manga>(j => j.Manga)
|
||||
.WithMany()
|
||||
.HasForeignKey(j => j.MangaId)
|
||||
.IsRequired()
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
modelBuilder.Entity<DownloadMangaCoverJob>()
|
||||
.Navigation(j => j.Manga)
|
||||
.AutoInclude();
|
||||
modelBuilder.Entity<DownloadSingleChapterJob>()
|
||||
.HasOne<Chapter>(j => j.Chapter)
|
||||
.WithMany()
|
||||
.HasForeignKey(j => j.ChapterId)
|
||||
.IsRequired()
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
modelBuilder.Entity<DownloadSingleChapterJob>()
|
||||
.Navigation(j => j.Chapter)
|
||||
.AutoInclude();
|
||||
modelBuilder.Entity<MoveMangaLibraryJob>()
|
||||
.HasOne<Manga>(j => j.Manga)
|
||||
.WithMany()
|
||||
.HasForeignKey(j => j.MangaId)
|
||||
.IsRequired()
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
modelBuilder.Entity<MoveMangaLibraryJob>()
|
||||
.Navigation(j => j.Manga)
|
||||
.AutoInclude();
|
||||
modelBuilder.Entity<MoveMangaLibraryJob>()
|
||||
.HasOne<LocalLibrary>(j => j.ToLibrary)
|
||||
.WithMany()
|
||||
.HasForeignKey(j => j.ToLibraryId)
|
||||
.IsRequired()
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
modelBuilder.Entity<MoveMangaLibraryJob>()
|
||||
.Navigation(j => j.ToLibrary)
|
||||
.AutoInclude();
|
||||
modelBuilder.Entity<RetrieveChaptersJob>()
|
||||
.HasOne<Manga>(j => j.Manga)
|
||||
.WithMany()
|
||||
.HasForeignKey(j => j.MangaId)
|
||||
.IsRequired()
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
modelBuilder.Entity<RetrieveChaptersJob>()
|
||||
.Navigation(j => j.Manga)
|
||||
.AutoInclude();
|
||||
modelBuilder.Entity<UpdateFilesDownloadedJob>()
|
||||
.HasOne<Manga>(j => j.Manga)
|
||||
.WithMany()
|
||||
.HasForeignKey(j => j.MangaId)
|
||||
.IsRequired()
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
modelBuilder.Entity<UpdateFilesDownloadedJob>()
|
||||
.Navigation(j => j.Manga)
|
||||
.AutoInclude();
|
||||
|
||||
//Job has possible ParentJob
|
||||
modelBuilder.Entity<Job>()
|
||||
.HasMany<Job>()
|
||||
.WithOne(childJob => childJob.ParentJob)
|
||||
.HasForeignKey(childjob => childjob.ParentJobId)
|
||||
.IsRequired(false)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
//Job might be dependent on other Jobs
|
||||
modelBuilder.Entity<Job>()
|
||||
.HasMany<Job>(root => root.DependsOnJobs)
|
||||
.WithMany();
|
||||
modelBuilder.Entity<Job>()
|
||||
.Navigation(j => j.DependsOnJobs)
|
||||
.AutoInclude(false);
|
||||
|
||||
//MangaConnector Types
|
||||
modelBuilder.Entity<MangaConnector>()
|
||||
.HasDiscriminator(c => c.Name)
|
||||
.HasValue<Global>("Global")
|
||||
.HasValue<MangaDex>("MangaDex");
|
||||
//MangaConnector is responsible for many Manga
|
||||
modelBuilder.Entity<MangaConnector>()
|
||||
.HasMany<Manga>()
|
||||
.WithOne(m => m.MangaConnector)
|
||||
.HasForeignKey(m => m.MangaConnectorName)
|
||||
.IsRequired()
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
modelBuilder.Entity<Manga>()
|
||||
.Navigation(m => m.MangaConnector)
|
||||
.AutoInclude();
|
||||
|
||||
//Manga has many Chapters
|
||||
modelBuilder.Entity<Manga>()
|
||||
.HasMany<Chapter>(m => m.Chapters)
|
||||
.WithOne(c => c.ParentManga)
|
||||
.HasForeignKey(c => c.ParentMangaId)
|
||||
.IsRequired()
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
modelBuilder.Entity<Chapter>()
|
||||
.Navigation(c => c.ParentManga)
|
||||
.AutoInclude();
|
||||
modelBuilder.Entity<Manga>()
|
||||
.Navigation(m => m.Chapters)
|
||||
.AutoInclude();
|
||||
//Manga owns MangaAltTitles
|
||||
modelBuilder.Entity<Manga>()
|
||||
.OwnsMany<MangaAltTitle>(m => m.AltTitles)
|
||||
.WithOwner();
|
||||
modelBuilder.Entity<Manga>()
|
||||
.Navigation(m => m.AltTitles)
|
||||
.AutoInclude();
|
||||
//Manga owns Links
|
||||
modelBuilder.Entity<Manga>()
|
||||
.OwnsMany<Link>(m => m.Links)
|
||||
.WithOwner();
|
||||
modelBuilder.Entity<Manga>()
|
||||
.Navigation(m => m.Links)
|
||||
.AutoInclude();
|
||||
//Manga has many Tags associated with many Manga
|
||||
modelBuilder.Entity<Manga>()
|
||||
.HasMany<MangaTag>(m => m.MangaTags)
|
||||
.WithMany()
|
||||
.UsingEntity("MangaTagToManga",
|
||||
l=> l.HasOne(typeof(MangaTag)).WithMany().HasForeignKey("MangaTagIds").HasPrincipalKey(nameof(MangaTag.Tag)),
|
||||
r => r.HasOne(typeof(Manga)).WithMany().HasForeignKey("MangaIds").HasPrincipalKey(nameof(Manga.MangaId)),
|
||||
j => j.HasKey("MangaTagIds", "MangaIds")
|
||||
);
|
||||
modelBuilder.Entity<Manga>()
|
||||
.Navigation(m => m.MangaTags)
|
||||
.AutoInclude();
|
||||
//Manga has many Authors associated with many Manga
|
||||
modelBuilder.Entity<Manga>()
|
||||
.HasMany<Author>(m => m.Authors)
|
||||
.WithMany()
|
||||
.UsingEntity("AuthorToManga",
|
||||
l=> l.HasOne(typeof(Author)).WithMany().HasForeignKey("AuthorIds").HasPrincipalKey(nameof(Author.AuthorId)),
|
||||
r => r.HasOne(typeof(Manga)).WithMany().HasForeignKey("MangaIds").HasPrincipalKey(nameof(Manga.MangaId)),
|
||||
j => j.HasKey("AuthorIds", "MangaIds")
|
||||
);
|
||||
modelBuilder.Entity<Manga>()
|
||||
.Navigation(m => m.Authors)
|
||||
.AutoInclude();
|
||||
|
||||
//LocalLibrary has many Mangas
|
||||
modelBuilder.Entity<LocalLibrary>()
|
||||
.HasMany<Manga>()
|
||||
.WithOne(m => m.Library)
|
||||
.HasForeignKey(m => m.LibraryId)
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
modelBuilder.Entity<Manga>()
|
||||
.Navigation(m => m.Library)
|
||||
.AutoInclude();
|
||||
}
|
||||
}
|
21
API/Schema/Identifiable.cs
Normal file
21
API/Schema/Identifiable.cs
Normal file
@ -0,0 +1,21 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace API.Schema;
|
||||
|
||||
[PrimaryKey("Key")]
|
||||
public abstract class Identifiable
|
||||
{
|
||||
public Identifiable()
|
||||
{
|
||||
this.Key = TokenGen.CreateToken(this.GetType());
|
||||
}
|
||||
|
||||
public Identifiable(string key)
|
||||
{
|
||||
this.Key = key;
|
||||
}
|
||||
|
||||
public string Key { get; init; }
|
||||
|
||||
public override string ToString() => Key;
|
||||
}
|
@ -1,33 +0,0 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using API.Schema.Contexts;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace API.Schema.Jobs;
|
||||
|
||||
public class DownloadAvailableChaptersJob : Job
|
||||
{
|
||||
[StringLength(64)] [Required] public string MangaId { get; init; }
|
||||
[JsonIgnore] public Manga Manga { get; init; } = null!;
|
||||
|
||||
public DownloadAvailableChaptersJob(Manga manga, ulong recurrenceMs, Job? parentJob = null, ICollection<Job>? dependsOnJobs = null)
|
||||
: base(TokenGen.CreateToken(typeof(DownloadAvailableChaptersJob)), JobType.DownloadAvailableChaptersJob, recurrenceMs, parentJob, dependsOnJobs)
|
||||
{
|
||||
this.MangaId = manga.MangaId;
|
||||
this.Manga = manga;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EF ONLY!!!
|
||||
/// </summary>
|
||||
internal DownloadAvailableChaptersJob(string mangaId, ulong recurrenceMs, string? parentJobId)
|
||||
: base(TokenGen.CreateToken(typeof(DownloadAvailableChaptersJob)), JobType.DownloadAvailableChaptersJob, recurrenceMs, parentJobId)
|
||||
{
|
||||
this.MangaId = mangaId;
|
||||
}
|
||||
|
||||
protected override IEnumerable<Job> RunInternal(PgsqlContext context)
|
||||
{
|
||||
context.Attach(Manga);
|
||||
return Manga.Chapters.Select(chapter => new DownloadSingleChapterJob(chapter, this));
|
||||
}
|
||||
}
|
@ -1,43 +0,0 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using API.Schema.Contexts;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace API.Schema.Jobs;
|
||||
|
||||
public class DownloadMangaCoverJob : Job
|
||||
{
|
||||
[StringLength(64)] [Required] public string MangaId { get; init; }
|
||||
[JsonIgnore] public Manga Manga { get; init; } = null!;
|
||||
|
||||
public DownloadMangaCoverJob(Manga manga, Job? parentJob = null, ICollection<Job>? dependsOnJobs = null)
|
||||
: base(TokenGen.CreateToken(typeof(DownloadMangaCoverJob)), JobType.DownloadMangaCoverJob, 0, parentJob, dependsOnJobs)
|
||||
{
|
||||
this.MangaId = manga.MangaId;
|
||||
this.Manga = manga;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EF ONLY!!!
|
||||
/// </summary>
|
||||
internal DownloadMangaCoverJob(string mangaId, string? parentJobId)
|
||||
: base(TokenGen.CreateToken(typeof(DownloadMangaCoverJob)), JobType.DownloadMangaCoverJob, 0, parentJobId)
|
||||
{
|
||||
this.MangaId = mangaId;
|
||||
}
|
||||
|
||||
protected override IEnumerable<Job> RunInternal(PgsqlContext context)
|
||||
{
|
||||
context.Attach(Manga);
|
||||
try
|
||||
{
|
||||
Manga.CoverFileNameInCache = Manga.MangaConnector.SaveCoverImageToCache(Manga);
|
||||
context.SaveChanges();
|
||||
}
|
||||
catch (DbUpdateException e)
|
||||
{
|
||||
Log.Error(e);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
}
|
@ -1,95 +0,0 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using API.Schema.Contexts;
|
||||
using log4net;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace API.Schema.Jobs;
|
||||
|
||||
[PrimaryKey("JobId")]
|
||||
public abstract class Job
|
||||
{
|
||||
[StringLength(64)]
|
||||
[Required]
|
||||
public string JobId { get; init; }
|
||||
|
||||
[StringLength(64)] public string? ParentJobId { get; init; }
|
||||
[JsonIgnore] public Job? ParentJob { get; init; }
|
||||
[JsonIgnore] public ICollection<Job> DependsOnJobs { get; init; }
|
||||
|
||||
[Required] public JobType JobType { get; init; }
|
||||
|
||||
[Required] public ulong RecurrenceMs { get; set; }
|
||||
|
||||
[Required] public DateTime LastExecution { get; internal set; } = DateTime.UnixEpoch;
|
||||
|
||||
[NotMapped] [Required] public DateTime NextExecution => LastExecution.AddMilliseconds(RecurrenceMs);
|
||||
[Required] public JobState state { get; internal set; } = JobState.FirstExecution;
|
||||
[Required] public bool Enabled { get; internal set; } = true;
|
||||
|
||||
[JsonIgnore] [NotMapped] internal bool IsCompleted => state is >= (JobState)128 and < (JobState)192;
|
||||
[JsonIgnore] [NotMapped] internal bool DependenciesFulfilled => DependsOnJobs.All(j => j.IsCompleted);
|
||||
|
||||
[NotMapped] [JsonIgnore] protected ILog Log { get; init; }
|
||||
|
||||
protected Job(string jobId, JobType jobType, ulong recurrenceMs, Job? parentJob = null, ICollection<Job>? dependsOnJobs = null)
|
||||
{
|
||||
this.JobId = jobId;
|
||||
this.JobType = jobType;
|
||||
this.RecurrenceMs = recurrenceMs;
|
||||
this.ParentJobId = parentJob?.JobId;
|
||||
this.ParentJob = parentJob;
|
||||
this.DependsOnJobs = dependsOnJobs ?? [];
|
||||
|
||||
this.Log = LogManager.GetLogger(this.GetType());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EF ONLY!!!
|
||||
/// </summary>
|
||||
protected internal Job(string jobId, JobType jobType, ulong recurrenceMs, string? parentJobId)
|
||||
{
|
||||
this.JobId = jobId;
|
||||
this.JobType = jobType;
|
||||
this.RecurrenceMs = recurrenceMs;
|
||||
this.ParentJobId = parentJobId;
|
||||
this.DependsOnJobs = [];
|
||||
|
||||
this.Log = LogManager.GetLogger(this.GetType());
|
||||
}
|
||||
|
||||
public IEnumerable<Job> Run(IServiceProvider serviceProvider)
|
||||
{
|
||||
Log.Debug($"Running job {JobId}");
|
||||
using IServiceScope scope = serviceProvider.CreateScope();
|
||||
|
||||
try
|
||||
{
|
||||
PgsqlContext context = scope.ServiceProvider.GetRequiredService<PgsqlContext>();
|
||||
context.Attach(this);
|
||||
this.state = JobState.Running;
|
||||
context.SaveChanges();
|
||||
Job[] newJobs = RunInternal(context).ToArray();
|
||||
this.state = JobState.Completed;
|
||||
context.SaveChanges();
|
||||
context.Jobs.AddRange(newJobs);
|
||||
context.SaveChanges();
|
||||
Log.Info($"Job {JobId} completed. Generated {newJobs.Length} new jobs.");
|
||||
return newJobs;
|
||||
}
|
||||
catch (DbUpdateException e)
|
||||
{
|
||||
this.state = JobState.Failed;
|
||||
Log.Error($"Failed to run job {JobId}", e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract IEnumerable<Job> RunInternal(PgsqlContext context);
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{JobId}";
|
||||
}
|
||||
}
|
@ -1,14 +0,0 @@
|
||||
namespace API.Schema.Jobs;
|
||||
|
||||
public enum JobState : byte
|
||||
{
|
||||
//Values 0-63 Preparation Stages
|
||||
FirstExecution = 0,
|
||||
//64-127 Running Stages
|
||||
Running = 64,
|
||||
//128-191 Completion Stages
|
||||
Completed = 128,
|
||||
CompletedWaiting = 159,
|
||||
//192-255 Error stages
|
||||
Failed = 192
|
||||
}
|
@ -1,14 +0,0 @@
|
||||
namespace API.Schema.Jobs;
|
||||
|
||||
|
||||
public enum JobType : byte
|
||||
{
|
||||
DownloadSingleChapterJob = 0,
|
||||
DownloadAvailableChaptersJob = 1,
|
||||
UpdateMetaDataJob = 2,
|
||||
MoveFileOrFolderJob = 3,
|
||||
DownloadMangaCoverJob = 4,
|
||||
RetrieveChaptersJob = 5,
|
||||
UpdateFilesDownloadedJob = 6,
|
||||
MoveMangaLibraryJob = 7
|
||||
}
|
@ -1,70 +0,0 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using API.Schema.Contexts;
|
||||
|
||||
namespace API.Schema.Jobs;
|
||||
|
||||
public class MoveFileOrFolderJob : Job
|
||||
{
|
||||
[StringLength(256)]
|
||||
[Required]
|
||||
public string FromLocation { get; init; }
|
||||
[StringLength(256)]
|
||||
[Required]
|
||||
public string ToLocation { get; init; }
|
||||
|
||||
public MoveFileOrFolderJob(string fromLocation, string toLocation, Job? parentJob = null, ICollection<Job>? dependsOnJobs = null)
|
||||
: base(TokenGen.CreateToken(typeof(MoveFileOrFolderJob)), JobType.MoveFileOrFolderJob, 0, parentJob, dependsOnJobs)
|
||||
{
|
||||
this.FromLocation = fromLocation;
|
||||
this.ToLocation = toLocation;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EF ONLY!!!
|
||||
/// </summary>
|
||||
internal MoveFileOrFolderJob(string jobId, string fromLocation, string toLocation, string? parentJobId)
|
||||
: base(jobId, JobType.MoveFileOrFolderJob, 0, parentJobId)
|
||||
{
|
||||
this.FromLocation = fromLocation;
|
||||
this.ToLocation = toLocation;
|
||||
}
|
||||
|
||||
protected override IEnumerable<Job> RunInternal(PgsqlContext context)
|
||||
{
|
||||
try
|
||||
{
|
||||
FileInfo fi = new (FromLocation);
|
||||
if (!fi.Exists)
|
||||
{
|
||||
Log.Error($"File does not exist at {FromLocation}");
|
||||
return [];
|
||||
}
|
||||
|
||||
if (File.Exists(ToLocation))//Do not override existing
|
||||
{
|
||||
Log.Error($"File already exists at {ToLocation}");
|
||||
return [];
|
||||
}
|
||||
if(fi.Attributes.HasFlag(FileAttributes.Directory))
|
||||
MoveDirectory(fi, ToLocation);
|
||||
else
|
||||
MoveFile(fi, ToLocation);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e);
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
private void MoveDirectory(FileInfo from, string toLocation)
|
||||
{
|
||||
Directory.Move(from.FullName, toLocation);
|
||||
}
|
||||
|
||||
private void MoveFile(FileInfo from, string toLocation)
|
||||
{
|
||||
File.Move(from.FullName, toLocation);
|
||||
}
|
||||
}
|
@ -1,51 +0,0 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using API.Schema.Contexts;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace API.Schema.Jobs;
|
||||
|
||||
public class MoveMangaLibraryJob : Job
|
||||
{
|
||||
[StringLength(64)] [Required] public string MangaId { get; init; }
|
||||
[JsonIgnore] public Manga Manga { get; init; } = null!;
|
||||
[StringLength(64)] [Required] public string ToLibraryId { get; init; }
|
||||
public LocalLibrary ToLibrary { get; init; } = null!;
|
||||
|
||||
public MoveMangaLibraryJob(Manga manga, LocalLibrary toLibrary, Job? parentJob = null, ICollection<Job>? dependsOnJobs = null)
|
||||
: base(TokenGen.CreateToken(typeof(MoveMangaLibraryJob)), JobType.MoveMangaLibraryJob, 0, parentJob, dependsOnJobs)
|
||||
{
|
||||
this.MangaId = manga.MangaId;
|
||||
this.Manga = manga;
|
||||
this.ToLibraryId = toLibrary.LocalLibraryId;
|
||||
this.ToLibrary = toLibrary;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EF ONLY!!!
|
||||
/// </summary>
|
||||
internal MoveMangaLibraryJob(string mangaId, string toLibraryId, string? parentJobId)
|
||||
: base(TokenGen.CreateToken(typeof(MoveMangaLibraryJob)), JobType.MoveMangaLibraryJob, 0, parentJobId)
|
||||
{
|
||||
this.MangaId = mangaId;
|
||||
this.ToLibraryId = toLibraryId;
|
||||
}
|
||||
|
||||
protected override IEnumerable<Job> RunInternal(PgsqlContext context)
|
||||
{
|
||||
context.Attach(Manga);
|
||||
Dictionary<Chapter, string> oldPath = Manga.Chapters.ToDictionary(c => c, c => c.FullArchiveFilePath);
|
||||
Manga.Library = ToLibrary;
|
||||
try
|
||||
{
|
||||
context.SaveChanges();
|
||||
}
|
||||
catch (DbUpdateException e)
|
||||
{
|
||||
Log.Error(e);
|
||||
return [];
|
||||
}
|
||||
|
||||
return Manga.Chapters.Select(c => new MoveFileOrFolderJob(oldPath[c], c.FullArchiveFilePath));
|
||||
}
|
||||
}
|
@ -1,52 +0,0 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using API.Schema.Contexts;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace API.Schema.Jobs;
|
||||
|
||||
public class RetrieveChaptersJob : Job
|
||||
{
|
||||
[StringLength(64)] [Required] public string MangaId { get; init; }
|
||||
[JsonIgnore] public Manga Manga { get; init; } = null!;
|
||||
[StringLength(8)] [Required] public string Language { get; private set; }
|
||||
|
||||
public RetrieveChaptersJob(Manga manga, string language, ulong recurrenceMs, Job? parentJob = null, ICollection<Job>? dependsOnJobs = null)
|
||||
: base(TokenGen.CreateToken(typeof(RetrieveChaptersJob)), JobType.RetrieveChaptersJob, recurrenceMs, parentJob, dependsOnJobs)
|
||||
{
|
||||
this.MangaId = manga.MangaId;
|
||||
this.Manga = manga;
|
||||
this.Language = language;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EF ONLY!!!
|
||||
/// </summary>
|
||||
internal RetrieveChaptersJob(string mangaId, string language, ulong recurrenceMs, string? parentJobId)
|
||||
: base(TokenGen.CreateToken(typeof(RetrieveChaptersJob)), JobType.RetrieveChaptersJob, recurrenceMs, parentJobId)
|
||||
{
|
||||
this.MangaId = mangaId;
|
||||
this.Language = language;
|
||||
}
|
||||
|
||||
protected override IEnumerable<Job> RunInternal(PgsqlContext context)
|
||||
{
|
||||
context.Attach(Manga);
|
||||
// This gets all chapters that are not downloaded
|
||||
Chapter[] allChapters = Manga.MangaConnector.GetChapters(Manga, Language);
|
||||
Chapter[] newChapters = allChapters.Where(chapter => context.Chapters.Contains(chapter) == false).ToArray();
|
||||
Log.Info($"{newChapters.Length} new chapters.");
|
||||
|
||||
try
|
||||
{
|
||||
context.Chapters.AddRange(newChapters);
|
||||
context.SaveChanges();
|
||||
}
|
||||
catch (DbUpdateException e)
|
||||
{
|
||||
Log.Error(e);
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
}
|
@ -1,45 +0,0 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using API.Schema.Contexts;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace API.Schema.Jobs;
|
||||
|
||||
public class UpdateFilesDownloadedJob : Job
|
||||
{
|
||||
[StringLength(64)] [Required] public string MangaId { get; init; }
|
||||
[JsonIgnore] public Manga Manga { get; init; } = null!;
|
||||
|
||||
public UpdateFilesDownloadedJob(Manga manga, ulong recurrenceMs, Job? parentJob = null, ICollection<Job>? dependsOnJobs = null)
|
||||
: base(TokenGen.CreateToken(typeof(UpdateFilesDownloadedJob)), JobType.UpdateFilesDownloadedJob, recurrenceMs, parentJob, dependsOnJobs)
|
||||
{
|
||||
this.MangaId = manga.MangaId;
|
||||
this.Manga = manga;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EF ONLY!!!
|
||||
/// </summary>
|
||||
internal UpdateFilesDownloadedJob(string mangaId, ulong recurrenceMs, string? parentJobId)
|
||||
: base(TokenGen.CreateToken(typeof(UpdateFilesDownloadedJob)), JobType.UpdateFilesDownloadedJob, recurrenceMs, parentJobId)
|
||||
{
|
||||
this.MangaId = mangaId;
|
||||
}
|
||||
|
||||
protected override IEnumerable<Job> RunInternal(PgsqlContext context)
|
||||
{
|
||||
context.Attach(Manga);
|
||||
foreach (Chapter chapter in Manga.Chapters)
|
||||
chapter.Downloaded = chapter.CheckDownloaded();
|
||||
|
||||
try
|
||||
{
|
||||
context.SaveChanges();
|
||||
}
|
||||
catch (DbUpdateException e)
|
||||
{
|
||||
Log.Error(e);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
}
|
@ -1,32 +0,0 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using log4net;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace API.Schema.LibraryConnectors;
|
||||
|
||||
[PrimaryKey("LibraryConnectorId")]
|
||||
public abstract class LibraryConnector(string libraryConnectorId, LibraryType libraryType, string baseUrl, string auth)
|
||||
{
|
||||
[StringLength(64)]
|
||||
[Required]
|
||||
public string LibraryConnectorId { get; } = libraryConnectorId;
|
||||
|
||||
[Required]
|
||||
public LibraryType LibraryType { get; init; } = libraryType;
|
||||
[StringLength(256)]
|
||||
[Required]
|
||||
[Url]
|
||||
public string BaseUrl { get; init; } = baseUrl;
|
||||
[StringLength(256)]
|
||||
[Required]
|
||||
public string Auth { get; init; } = auth;
|
||||
|
||||
[JsonIgnore]
|
||||
[NotMapped]
|
||||
protected ILog Log { get; init; } = LogManager.GetLogger($"{libraryType.ToString()} {baseUrl}");
|
||||
|
||||
protected abstract void UpdateLibraryInternal();
|
||||
internal abstract bool Test();
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
namespace API.Schema.LibraryConnectors;
|
||||
|
||||
public enum LibraryType : byte
|
||||
{
|
||||
Komga = 0,
|
||||
Kavita = 1
|
||||
}
|
@ -1,12 +1,12 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
|
||||
namespace API.Schema.LibraryConnectors;
|
||||
namespace API.Schema.LibraryContext.LibraryConnectors;
|
||||
|
||||
public class Kavita : LibraryConnector
|
||||
{
|
||||
|
||||
public Kavita(string baseUrl, string auth) : base(TokenGen.CreateToken(typeof(Kavita), baseUrl), LibraryType.Kavita, baseUrl, auth)
|
||||
public Kavita(string baseUrl, string auth) : base(LibraryType.Kavita, baseUrl, auth)
|
||||
{
|
||||
}
|
||||
|
||||
@ -44,8 +44,9 @@ public class Kavita : LibraryConnector
|
||||
{
|
||||
}
|
||||
}
|
||||
catch (HttpRequestException e)
|
||||
catch (HttpRequestException)
|
||||
{
|
||||
|
||||
}
|
||||
return "";
|
||||
}
|
||||
@ -53,13 +54,13 @@ public class Kavita : LibraryConnector
|
||||
protected override void UpdateLibraryInternal()
|
||||
{
|
||||
foreach (KavitaLibrary lib in GetLibraries())
|
||||
NetClient.MakePost($"{BaseUrl}/api/ToLibrary/scan?libraryId={lib.id}", "Bearer", Auth);
|
||||
NetClient.MakePost($"{BaseUrl}/api/ToFileLibrary/scan?libraryId={lib.id}", "Bearer", Auth);
|
||||
}
|
||||
|
||||
internal override bool Test()
|
||||
{
|
||||
foreach (KavitaLibrary lib in GetLibraries())
|
||||
if (NetClient.MakePost($"{BaseUrl}/api/ToLibrary/scan?libraryId={lib.id}", "Bearer", Auth))
|
||||
if (NetClient.MakePost($"{BaseUrl}/api/ToFileLibrary/scan?libraryId={lib.id}", "Bearer", Auth))
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
@ -70,7 +71,7 @@ public class Kavita : LibraryConnector
|
||||
/// <returns>Array of KavitaLibrary</returns>
|
||||
private IEnumerable<KavitaLibrary> GetLibraries()
|
||||
{
|
||||
Stream data = NetClient.MakeRequest($"{BaseUrl}/api/ToLibrary/libraries", "Bearer", Auth);
|
||||
Stream data = NetClient.MakeRequest($"{BaseUrl}/api/ToFileLibrary/libraries", "Bearer", Auth);
|
||||
if (data == Stream.Null)
|
||||
{
|
||||
Log.Info("No libraries found");
|
||||
@ -90,7 +91,7 @@ public class Kavita : LibraryConnector
|
||||
JsonObject? jObject = (JsonObject?)jsonNode;
|
||||
if(jObject is null)
|
||||
continue;
|
||||
int libraryId = jObject!["id"]!.GetValue<int>();
|
||||
int libraryId = jObject["id"]!.GetValue<int>();
|
||||
string libraryName = jObject["name"]!.GetValue<string>();
|
||||
ret.Add(new KavitaLibrary(libraryId, libraryName));
|
||||
}
|
@ -1,12 +1,11 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
|
||||
namespace API.Schema.LibraryConnectors;
|
||||
namespace API.Schema.LibraryContext.LibraryConnectors;
|
||||
|
||||
public class Komga : LibraryConnector
|
||||
{
|
||||
public Komga(string baseUrl, string auth) : base(TokenGen.CreateToken(typeof(Komga), baseUrl), LibraryType.Komga,
|
||||
baseUrl, auth)
|
||||
public Komga(string baseUrl, string auth) : base(LibraryType.Komga, baseUrl, auth)
|
||||
{
|
||||
}
|
||||
|
@ -0,0 +1,57 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using log4net;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace API.Schema.LibraryContext.LibraryConnectors;
|
||||
|
||||
[PrimaryKey("Key")]
|
||||
public abstract class LibraryConnector : Identifiable
|
||||
{
|
||||
[Required]
|
||||
public LibraryType LibraryType { get; init; }
|
||||
[StringLength(256)]
|
||||
[Required]
|
||||
[Url]
|
||||
public string BaseUrl { get; init; }
|
||||
[StringLength(256)]
|
||||
[Required]
|
||||
public string Auth { get; init; }
|
||||
|
||||
[JsonIgnore]
|
||||
[NotMapped]
|
||||
protected ILog Log { get; init; }
|
||||
|
||||
protected LibraryConnector(LibraryType libraryType, string baseUrl, string auth)
|
||||
: base()
|
||||
{
|
||||
this.LibraryType = libraryType;
|
||||
this.BaseUrl = baseUrl;
|
||||
this.Auth = auth;
|
||||
this.Log = LogManager.GetLogger(GetType());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EF CORE ONLY!!!!
|
||||
/// </summary>
|
||||
internal LibraryConnector(string key, LibraryType libraryType, string baseUrl, string auth)
|
||||
: base(key)
|
||||
{
|
||||
this.LibraryType = libraryType;
|
||||
this.BaseUrl = baseUrl;
|
||||
this.Auth = auth;
|
||||
this.Log = LogManager.GetLogger(GetType());
|
||||
}
|
||||
|
||||
public override string ToString() => $"{base.ToString()} {this.LibraryType} {this.BaseUrl}";
|
||||
|
||||
protected abstract void UpdateLibraryInternal();
|
||||
internal abstract bool Test();
|
||||
}
|
||||
|
||||
public enum LibraryType : byte
|
||||
{
|
||||
Komga = 0,
|
||||
Kavita = 1
|
||||
}
|
@ -2,7 +2,7 @@
|
||||
using System.Net.Http.Headers;
|
||||
using log4net;
|
||||
|
||||
namespace API.Schema.LibraryConnectors;
|
||||
namespace API.Schema.LibraryContext.LibraryConnectors;
|
||||
|
||||
public class NetClient
|
||||
{
|
@ -1,9 +1,9 @@
|
||||
using API.Schema.LibraryConnectors;
|
||||
using API.Schema.LibraryContext.LibraryConnectors;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace API.Schema.Contexts;
|
||||
namespace API.Schema.LibraryContext;
|
||||
|
||||
public class LibraryContext(DbContextOptions<LibraryContext> options) : DbContext(options)
|
||||
public class LibraryContext(DbContextOptions<LibraryContext> options) : TrangaBaseContext<LibraryContext>(options)
|
||||
{
|
||||
public DbSet<LibraryConnector> LibraryConnectors { get; set; }
|
||||
|
@ -1,24 +0,0 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace API.Schema;
|
||||
|
||||
[PrimaryKey("LinkId")]
|
||||
public class Link(string linkProvider, string linkUrl)
|
||||
{
|
||||
[StringLength(64)]
|
||||
[Required]
|
||||
public string LinkId { get; init; } = TokenGen.CreateToken(typeof(Link), linkProvider, linkUrl);
|
||||
[StringLength(64)]
|
||||
[Required]
|
||||
public string LinkProvider { get; init; } = linkProvider;
|
||||
[StringLength(2048)]
|
||||
[Required]
|
||||
[Url]
|
||||
public string LinkUrl { get; init; } = linkUrl;
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{LinkId} {LinkProvider} {LinkUrl}";
|
||||
}
|
||||
}
|
@ -1,22 +0,0 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace API.Schema;
|
||||
|
||||
public class LocalLibrary(string basePath, string libraryName)
|
||||
{
|
||||
[StringLength(64)]
|
||||
[Required]
|
||||
public string LocalLibraryId { get; init; } = TokenGen.CreateToken(typeof(LocalLibrary), basePath);
|
||||
[StringLength(256)]
|
||||
[Required]
|
||||
public string BasePath { get; internal set; } = basePath;
|
||||
|
||||
[StringLength(512)]
|
||||
[Required]
|
||||
public string LibraryName { get; internal set; } = libraryName;
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{LocalLibraryId} {LibraryName} - {BasePath}";
|
||||
}
|
||||
}
|
@ -1,148 +0,0 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using API.Schema.MangaConnectors;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Newtonsoft.Json;
|
||||
using static System.IO.UnixFileMode;
|
||||
|
||||
namespace API.Schema;
|
||||
|
||||
[PrimaryKey("MangaId")]
|
||||
public class Manga
|
||||
{
|
||||
[StringLength(64)]
|
||||
[Required]
|
||||
public string MangaId { get; init; }
|
||||
[StringLength(256)] [Required] public string IdOnConnectorSite { get; init; }
|
||||
[StringLength(512)] [Required] public string Name { get; internal set; }
|
||||
[Required] public string Description { get; internal set; }
|
||||
[Url] [StringLength(512)] [Required] public string WebsiteUrl { get; internal init; }
|
||||
[JsonIgnore] [Url] [StringLength(512)] public string CoverUrl { get; internal set; }
|
||||
[Required] public MangaReleaseStatus ReleaseStatus { get; internal set; }
|
||||
|
||||
[StringLength(64)]
|
||||
public string? LibraryId { get; init; }
|
||||
[JsonIgnore] public LocalLibrary? Library { get; internal set; }
|
||||
|
||||
[StringLength(32)]
|
||||
[Required]
|
||||
public string MangaConnectorName { get; init; }
|
||||
[JsonIgnore] public MangaConnector MangaConnector { get; init; } = null!;
|
||||
|
||||
public ICollection<Author> Authors { get; internal set; }= null!;
|
||||
public ICollection<MangaTag> MangaTags { get; internal set; }= null!;
|
||||
public ICollection<Link> Links { get; internal set; }= null!;
|
||||
public ICollection<MangaAltTitle> AltTitles { get; internal set; } = null!;
|
||||
[Required] public float IgnoreChaptersBefore { get; internal set; }
|
||||
[StringLength(1024)] [Required] public string DirectoryName { get; private set; }
|
||||
|
||||
[JsonIgnore] [StringLength(512)] public string? CoverFileNameInCache { get; internal set; } = null;
|
||||
[Required] public uint? Year { get; internal init; }
|
||||
[StringLength(8)] public string? OriginalLanguage { get; internal init; }
|
||||
|
||||
[JsonIgnore]
|
||||
[NotMapped]
|
||||
public string? FullDirectoryPath => Library is not null ? Path.Join(Library.BasePath, DirectoryName) : null;
|
||||
|
||||
[JsonIgnore] public ICollection<Chapter> Chapters { get; internal set; } = [];
|
||||
|
||||
public Manga(string idOnConnector, string name, string description, string websiteUrl, string coverUrl, MangaReleaseStatus releaseStatus,
|
||||
MangaConnector mangaConnector, ICollection<Author> authors, ICollection<MangaTag> mangaTags, ICollection<Link> links, ICollection<MangaAltTitle> altTitles,
|
||||
LocalLibrary? library = null, float ignoreChaptersBefore = 0f, uint? year = null, string? originalLanguage = null)
|
||||
{
|
||||
this.MangaId = TokenGen.CreateToken(typeof(Manga), mangaConnector.Name, idOnConnector);
|
||||
this.IdOnConnectorSite = idOnConnector;
|
||||
this.Name = name;
|
||||
this.Description = description;
|
||||
this.WebsiteUrl = websiteUrl;
|
||||
this.CoverUrl = coverUrl;
|
||||
this.ReleaseStatus = releaseStatus;
|
||||
this.LibraryId = library?.LocalLibraryId;
|
||||
this.Library = library;
|
||||
this.MangaConnectorName = mangaConnector.Name;
|
||||
this.MangaConnector = mangaConnector;
|
||||
this.Authors = authors;
|
||||
this.MangaTags = mangaTags;
|
||||
this.Links = links;
|
||||
this.AltTitles = altTitles;
|
||||
this.IgnoreChaptersBefore = ignoreChaptersBefore;
|
||||
this.DirectoryName = CleanDirectoryName(name);
|
||||
this.Year = year;
|
||||
this.OriginalLanguage = originalLanguage;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EF ONLY!!!
|
||||
/// </summary>
|
||||
public Manga(string mangaId, string idOnConnectorSite, string name, string description, string websiteUrl, string coverUrl, MangaReleaseStatus releaseStatus,
|
||||
string mangaConnectorName, string directoryName, float ignoreChaptersBefore, string? libraryId, uint? year, string? originalLanguage)
|
||||
{
|
||||
this.MangaId = mangaId;
|
||||
this.IdOnConnectorSite = idOnConnectorSite;
|
||||
this.Name = name;
|
||||
this.Description = description;
|
||||
this.WebsiteUrl = websiteUrl;
|
||||
this.CoverUrl = coverUrl;
|
||||
this.ReleaseStatus = releaseStatus;
|
||||
this.MangaConnectorName = mangaConnectorName;
|
||||
this.DirectoryName = directoryName;
|
||||
this.LibraryId = libraryId;
|
||||
this.IgnoreChaptersBefore = ignoreChaptersBefore;
|
||||
this.Year = year;
|
||||
this.OriginalLanguage = originalLanguage;
|
||||
}
|
||||
|
||||
|
||||
public string CreatePublicationFolder()
|
||||
{
|
||||
string publicationFolder = FullDirectoryPath;
|
||||
if(!Directory.Exists(publicationFolder))
|
||||
Directory.CreateDirectory(publicationFolder);
|
||||
if(RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
||||
File.SetUnixFileMode(publicationFolder, GroupRead | GroupWrite | GroupExecute | OtherRead | OtherWrite | OtherExecute | UserRead | UserWrite | UserExecute);
|
||||
return publicationFolder;
|
||||
}
|
||||
|
||||
//https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file
|
||||
//less than 32 is control *forbidden*
|
||||
//34 is " *forbidden*
|
||||
//42 is * *forbidden*
|
||||
//47 is / *forbidden*
|
||||
//58 is : *forbidden*
|
||||
//60 is < *forbidden*
|
||||
//62 is > *forbidden*
|
||||
//63 is ? *forbidden*
|
||||
//92 is \ *forbidden*
|
||||
//124 is | *forbidden*
|
||||
//127 is delete *forbidden*
|
||||
//Below 127 all except *******
|
||||
private static readonly int[] ForbiddenCharsBelow127 = [34, 42, 47, 58, 60, 62, 63, 92, 124, 127];
|
||||
//Above 127 none except *******
|
||||
private static readonly int[] IncludeCharsAbove127 = [128, 138, 142];
|
||||
//128 is € include
|
||||
//138 is Š include
|
||||
//142 is Ž include
|
||||
//152 through 255 looks fine except 157, 172, 173, 175 *******
|
||||
private static readonly int[] ForbiddenCharsAbove152 = [157, 172, 173, 175];
|
||||
private static string CleanDirectoryName(string name)
|
||||
{
|
||||
StringBuilder sb = new ();
|
||||
foreach (char c in name)
|
||||
{
|
||||
if (c > 32 && c < 127 && ForbiddenCharsBelow127.Contains(c) == false)
|
||||
sb.Append(c);
|
||||
else if (c > 127 && c < 152 && IncludeCharsAbove127.Contains(c))
|
||||
sb.Append(c);
|
||||
else if(c >= 152 && c <= 255 && ForbiddenCharsAbove152.Contains(c) == false)
|
||||
sb.Append(c);
|
||||
}
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{MangaId} {Name}";
|
||||
}
|
||||
}
|
@ -1,23 +0,0 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace API.Schema;
|
||||
|
||||
[PrimaryKey("AltTitleId")]
|
||||
public class MangaAltTitle(string language, string title)
|
||||
{
|
||||
[StringLength(64)]
|
||||
[Required]
|
||||
public string AltTitleId { get; init; } = TokenGen.CreateToken("AltTitle", language, title);
|
||||
[StringLength(8)]
|
||||
[Required]
|
||||
public string Language { get; init; } = language;
|
||||
[StringLength(256)]
|
||||
[Required]
|
||||
public string Title { get; set; } = title;
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{AltTitleId} {Language} {Title}";
|
||||
}
|
||||
}
|
@ -1,55 +0,0 @@
|
||||
using API.Schema.Contexts;
|
||||
|
||||
namespace API.Schema.MangaConnectors;
|
||||
|
||||
public class Global : MangaConnector
|
||||
{
|
||||
private PgsqlContext context { get; init; }
|
||||
public Global(PgsqlContext context) : base("Global", ["all"], [""], "")
|
||||
{
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
public override Manga[] SearchManga(string mangaSearchName)
|
||||
{
|
||||
//Get all enabled Connectors
|
||||
MangaConnector[] enabledConnectors = context.MangaConnectors.Where(c => c.Enabled && c.Name != "Global").ToArray();
|
||||
|
||||
//Create Task for each MangaConnector to search simulatneously
|
||||
Task<Manga[]>[] tasks =
|
||||
enabledConnectors.Select(c => new Task<Manga[]>(() => c.SearchManga(mangaSearchName))).ToArray();
|
||||
foreach (var task in tasks)
|
||||
task.Start();
|
||||
|
||||
//Wait for all tasks to finish
|
||||
do
|
||||
{
|
||||
Thread.Sleep(50);
|
||||
}while(tasks.Any(t => t.Status < TaskStatus.RanToCompletion));
|
||||
|
||||
//Concatenate all results into one
|
||||
Manga[] ret = tasks.Select(t => t.IsCompletedSuccessfully ? t.Result : []).ToArray().SelectMany(i => i).ToArray();
|
||||
return ret;
|
||||
}
|
||||
|
||||
public override Manga? GetMangaFromUrl(string url)
|
||||
{
|
||||
MangaConnector? mc = context.MangaConnectors.ToArray().FirstOrDefault(c => c.UrlMatchesConnector(url));
|
||||
return mc?.GetMangaFromUrl(url) ?? null;
|
||||
}
|
||||
|
||||
public override Manga? GetMangaFromId(string mangaIdOnSite)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
public override Chapter[] GetChapters(Manga manga, string? language = null)
|
||||
{
|
||||
return manga.MangaConnector.GetChapters(manga, language);
|
||||
}
|
||||
|
||||
internal override string[] GetChapterImageUrls(Chapter chapter)
|
||||
{
|
||||
return chapter.ParentManga.MangaConnector.GetChapterImageUrls(chapter);
|
||||
}
|
||||
}
|
17
API/Schema/MangaContext/AltTitle.cs
Normal file
17
API/Schema/MangaContext/AltTitle.cs
Normal file
@ -0,0 +1,17 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace API.Schema.MangaContext;
|
||||
|
||||
[PrimaryKey("Key")]
|
||||
public class AltTitle(string language, string title) : Identifiable(TokenGen.CreateToken("AltTitle"))
|
||||
{
|
||||
[StringLength(8)]
|
||||
[Required]
|
||||
public string Language { get; init; } = language;
|
||||
[StringLength(256)]
|
||||
[Required]
|
||||
public string Title { get; init; } = title;
|
||||
|
||||
public override string ToString() => $"{base.ToString()} {Language} {Title}";
|
||||
}
|
14
API/Schema/MangaContext/Author.cs
Normal file
14
API/Schema/MangaContext/Author.cs
Normal file
@ -0,0 +1,14 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace API.Schema.MangaContext;
|
||||
|
||||
[PrimaryKey("Key")]
|
||||
public class Author(string authorName) : Identifiable(TokenGen.CreateToken(typeof(Author), authorName))
|
||||
{
|
||||
[StringLength(128)]
|
||||
[Required]
|
||||
public string AuthorName { get; init; } = authorName;
|
||||
|
||||
public override string ToString() => $"{base.ToString()} {AuthorName}";
|
||||
}
|
@ -4,38 +4,60 @@ using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Xml.Linq;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace API.Schema;
|
||||
namespace API.Schema.MangaContext;
|
||||
|
||||
[PrimaryKey("ChapterId")]
|
||||
public class Chapter : IComparable<Chapter>
|
||||
[PrimaryKey("Key")]
|
||||
public class Chapter : Identifiable, IComparable<Chapter>
|
||||
{
|
||||
[StringLength(64)] [Required] public string ChapterId { get; init; }
|
||||
[StringLength(64)] [Required] public string ParentMangaId { get; init; } = null!;
|
||||
private Manga? _parentManga;
|
||||
|
||||
public string ParentMangaId { get; init; }
|
||||
[JsonIgnore] public Manga ParentManga { get; init; } = null!;
|
||||
[JsonIgnore]
|
||||
public Manga ParentManga
|
||||
{
|
||||
get => _lazyLoader.Load(this, ref _parentManga) ?? throw new InvalidOperationException();
|
||||
init
|
||||
{
|
||||
ParentMangaId = value.Key;
|
||||
_parentManga = value;
|
||||
}
|
||||
}
|
||||
|
||||
[NotMapped]
|
||||
public Dictionary<string, string> IdsOnMangaConnectors =>
|
||||
MangaConnectorIds.ToDictionary(id => id.MangaConnectorName, id => id.IdOnConnectorSite);
|
||||
|
||||
private ICollection<MangaConnectorId<Chapter>>? _mangaConnectorIds;
|
||||
[JsonIgnore]
|
||||
public ICollection<MangaConnectorId<Chapter>> MangaConnectorIds
|
||||
{
|
||||
get => _lazyLoader.Load(this, ref _mangaConnectorIds) ?? throw new InvalidOperationException();
|
||||
init => _mangaConnectorIds = value;
|
||||
}
|
||||
|
||||
public int? VolumeNumber { get; private set; }
|
||||
[StringLength(10)] [Required] public string ChapterNumber { get; private set; }
|
||||
|
||||
[StringLength(2048)] [Required] [Url] public string Url { get; internal set; }
|
||||
|
||||
[StringLength(256)] public string? Title { get; private set; }
|
||||
|
||||
[StringLength(256)] [Required] public string FileName { get; private set; }
|
||||
|
||||
[Required] public bool Downloaded { get; internal set; }
|
||||
[JsonIgnore] [NotMapped] public string FullArchiveFilePath => Path.Join(ParentManga.FullDirectoryPath, FileName);
|
||||
[NotMapped] public string FullArchiveFilePath => Path.Join(ParentManga.FullDirectoryPath, FileName);
|
||||
|
||||
public Chapter(Manga parentManga, string url, string chapterNumber, int? volumeNumber = null, string? title = null)
|
||||
private readonly ILazyLoader _lazyLoader = null!;
|
||||
|
||||
public Chapter(Manga parentManga, string chapterNumber,
|
||||
int? volumeNumber, string? title = null)
|
||||
: base(TokenGen.CreateToken(typeof(Chapter), parentManga.Key, chapterNumber))
|
||||
{
|
||||
this.ChapterId = TokenGen.CreateToken(typeof(Chapter), parentManga.MangaId, chapterNumber);
|
||||
this.ParentMangaId = parentManga.MangaId;
|
||||
this.ParentManga = parentManga;
|
||||
this.MangaConnectorIds = [];
|
||||
this.VolumeNumber = volumeNumber;
|
||||
this.ChapterNumber = chapterNumber;
|
||||
this.Url = url;
|
||||
this.Title = title;
|
||||
this.FileName = GetArchiveFilePath();
|
||||
this.Downloaded = false;
|
||||
@ -44,13 +66,12 @@ public class Chapter : IComparable<Chapter>
|
||||
/// <summary>
|
||||
/// EF ONLY!!!
|
||||
/// </summary>
|
||||
internal Chapter(string chapterId, string parentMangaId, int? volumeNumber, string chapterNumber, string url, string? title, string fileName, bool downloaded)
|
||||
internal Chapter(ILazyLoader lazyLoader, string key, int? volumeNumber, string chapterNumber, string? title, string fileName, bool downloaded)
|
||||
: base(key)
|
||||
{
|
||||
this.ChapterId = chapterId;
|
||||
this.ParentMangaId = parentMangaId;
|
||||
this._lazyLoader = lazyLoader;
|
||||
this.VolumeNumber = volumeNumber;
|
||||
this.ChapterNumber = chapterNumber;
|
||||
this.Url = url;
|
||||
this.Title = title;
|
||||
this.FileName = fileName;
|
||||
this.Downloaded = downloaded;
|
||||
@ -75,19 +96,19 @@ public class Chapter : IComparable<Chapter>
|
||||
public bool CheckDownloaded() => File.Exists(FullArchiveFilePath);
|
||||
|
||||
/// Placeholders:
|
||||
/// %M Manga Name
|
||||
/// %M Obj Name
|
||||
/// %V Volume
|
||||
/// %C Chapter
|
||||
/// %T Title
|
||||
/// %A Author (first in list)
|
||||
/// %I Chapter Internal ID
|
||||
/// %i Manga Internal ID
|
||||
/// %Y Year (Manga)
|
||||
/// %i Obj Internal ID
|
||||
/// %Y Year (Obj)
|
||||
private static readonly Regex NullableRex = new(@"\?([a-zA-Z])\(([^\)]*)\)|(.+?)");
|
||||
private static readonly Regex ReplaceRexx = new(@"%([a-zA-Z])|(.+?)");
|
||||
private string GetArchiveFilePath()
|
||||
{
|
||||
string archiveNamingScheme = TrangaSettings.chapterNamingScheme;
|
||||
string archiveNamingScheme = Tranga.Settings.ChapterNamingScheme;
|
||||
StringBuilder stringBuilder = new();
|
||||
foreach (Match nullable in NullableRex.Matches(archiveNamingScheme))
|
||||
{
|
||||
@ -105,8 +126,6 @@ public class Chapter : IComparable<Chapter>
|
||||
'C' => ChapterNumber is null,
|
||||
'T' => Title is null,
|
||||
'A' => ParentManga?.Authors?.FirstOrDefault()?.AuthorName is null,
|
||||
'I' => ChapterId is null,
|
||||
'i' => ParentManga?.MangaId is null,
|
||||
'Y' => ParentManga?.Year is null,
|
||||
_ => true
|
||||
};
|
||||
@ -133,8 +152,6 @@ public class Chapter : IComparable<Chapter>
|
||||
'C' => ChapterNumber,
|
||||
'T' => Title,
|
||||
'A' => ParentManga?.Authors?.FirstOrDefault()?.AuthorName,
|
||||
'I' => ChapterId,
|
||||
'i' => ParentManga?.MangaId,
|
||||
'Y' => ParentManga?.Year.ToString(),
|
||||
_ => null
|
||||
};
|
||||
@ -172,18 +189,22 @@ public class Chapter : IComparable<Chapter>
|
||||
internal string GetComicInfoXmlString()
|
||||
{
|
||||
XElement comicInfo = new("ComicInfo",
|
||||
new XElement("Tags", string.Join(',', ParentManga.MangaTags.Select(tag => tag.Tag))),
|
||||
new XElement("LanguageISO", ParentManga.OriginalLanguage),
|
||||
new XElement("Title", Title),
|
||||
new XElement("Writer", string.Join(',', ParentManga.Authors.Select(author => author.AuthorName))),
|
||||
new XElement("Volume", VolumeNumber),
|
||||
new XElement("Number", ChapterNumber)
|
||||
);
|
||||
if(Title is not null)
|
||||
comicInfo.Add(new XElement("Title", Title));
|
||||
if(ParentManga.MangaTags.Count > 0)
|
||||
comicInfo.Add(new XElement("Tags", string.Join(',', ParentManga.MangaTags.Select(tag => tag.Tag))));
|
||||
if(VolumeNumber is not null)
|
||||
comicInfo.Add(new XElement("Volume", VolumeNumber));
|
||||
if(ParentManga.Authors.Count > 0)
|
||||
comicInfo.Add(new XElement("Writer", string.Join(',', ParentManga.Authors.Select(author => author.AuthorName))));
|
||||
if(ParentManga.OriginalLanguage is not null)
|
||||
comicInfo.Add(new XElement("LanguageISO", ParentManga.OriginalLanguage));
|
||||
if(ParentManga.Description != string.Empty)
|
||||
comicInfo.Add(new XElement("Summary", ParentManga.Description));
|
||||
return comicInfo.ToString();
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{ChapterId} Vol.{VolumeNumber} Ch.{ChapterNumber} - {Title}";
|
||||
}
|
||||
public override string ToString() => $"{base.ToString()} Vol.{VolumeNumber} Ch.{ChapterNumber} - {Title}";
|
||||
}
|
19
API/Schema/MangaContext/FileLibrary.cs
Normal file
19
API/Schema/MangaContext/FileLibrary.cs
Normal file
@ -0,0 +1,19 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace API.Schema.MangaContext;
|
||||
|
||||
[PrimaryKey("Key")]
|
||||
public class FileLibrary(string basePath, string libraryName)
|
||||
: Identifiable(TokenGen.CreateToken(typeof(FileLibrary), basePath))
|
||||
{
|
||||
[StringLength(256)]
|
||||
[Required]
|
||||
public string BasePath { get; internal set; } = basePath;
|
||||
|
||||
[StringLength(512)]
|
||||
[Required]
|
||||
public string LibraryName { get; internal set; } = libraryName;
|
||||
|
||||
public override string ToString() => $"{base.ToString()} {LibraryName} - {BasePath}";
|
||||
}
|
18
API/Schema/MangaContext/Link.cs
Normal file
18
API/Schema/MangaContext/Link.cs
Normal file
@ -0,0 +1,18 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace API.Schema.MangaContext;
|
||||
|
||||
[PrimaryKey("Key")]
|
||||
public class Link(string linkProvider, string linkUrl) : Identifiable(TokenGen.CreateToken(typeof(Link), linkProvider, linkUrl))
|
||||
{
|
||||
[StringLength(64)]
|
||||
[Required]
|
||||
public string LinkProvider { get; init; } = linkProvider;
|
||||
[StringLength(2048)]
|
||||
[Required]
|
||||
[Url]
|
||||
public string LinkUrl { get; init; } = linkUrl;
|
||||
|
||||
public override string ToString() => $"{base.ToString()} {LinkProvider} {LinkUrl}";
|
||||
}
|
196
API/Schema/MangaContext/Manga.cs
Normal file
196
API/Schema/MangaContext/Manga.cs
Normal file
@ -0,0 +1,196 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using API.Workers;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Newtonsoft.Json;
|
||||
using static System.IO.UnixFileMode;
|
||||
|
||||
namespace API.Schema.MangaContext;
|
||||
|
||||
[PrimaryKey("Key")]
|
||||
public class Manga : Identifiable
|
||||
{
|
||||
[StringLength(512)] [Required] public string Name { get; internal set; }
|
||||
[Required] public string Description { get; internal set; }
|
||||
[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;
|
||||
}
|
||||
}
|
||||
|
||||
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; }
|
||||
[JsonIgnore] [StringLength(512)] public string? CoverFileNameInCache { get; internal set; }
|
||||
public uint? Year { get; internal init; }
|
||||
[StringLength(8)] public string? OriginalLanguage { get; internal init; }
|
||||
|
||||
[JsonIgnore]
|
||||
[NotMapped]
|
||||
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;
|
||||
}
|
||||
|
||||
[NotMapped] public Dictionary<string, string> IdsOnMangaConnectors =>
|
||||
MangaConnectorIds.ToDictionary(id => id.MangaConnectorName, id => id.IdOnConnectorSite);
|
||||
private ICollection<MangaConnectorId<Manga>>? _mangaConnectorIds;
|
||||
[JsonIgnore]
|
||||
public ICollection<MangaConnectorId<Manga>> MangaConnectorIds
|
||||
{
|
||||
get => _lazyLoader.Load(this, ref _mangaConnectorIds) ?? throw new InvalidOperationException();
|
||||
private set => _mangaConnectorIds = value;
|
||||
}
|
||||
|
||||
private readonly ILazyLoader _lazyLoader = null!;
|
||||
|
||||
public Manga(string name, string description, string coverUrl, MangaReleaseStatus releaseStatus,
|
||||
ICollection<Author> authors, ICollection<MangaTag> mangaTags, ICollection<Link> links, ICollection<AltTitle> altTitles,
|
||||
FileLibrary? library = null, float ignoreChaptersBefore = 0f, uint? year = null, string? originalLanguage = null)
|
||||
:base(TokenGen.CreateToken(typeof(Manga), name))
|
||||
{
|
||||
this.Name = name;
|
||||
this.Description = description;
|
||||
this.CoverUrl = coverUrl;
|
||||
this.ReleaseStatus = releaseStatus;
|
||||
this.Library = library;
|
||||
this.Authors = authors;
|
||||
this.MangaTags = mangaTags;
|
||||
this.Links = links;
|
||||
this.AltTitles = altTitles;
|
||||
this.IgnoreChaptersBefore = ignoreChaptersBefore;
|
||||
this.DirectoryName = CleanDirectoryName(name);
|
||||
this.Year = year;
|
||||
this.OriginalLanguage = originalLanguage;
|
||||
this.Chapters = [];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EF ONLY!!!
|
||||
/// </summary>
|
||||
public Manga(ILazyLoader lazyLoader, string key, string name, string description, string coverUrl,
|
||||
MangaReleaseStatus releaseStatus,
|
||||
string directoryName, float ignoreChaptersBefore, string? libraryId, uint? year, string? originalLanguage)
|
||||
: base(key)
|
||||
{
|
||||
this._lazyLoader = lazyLoader;
|
||||
this.Name = name;
|
||||
this.Description = description;
|
||||
this.CoverUrl = coverUrl;
|
||||
this.ReleaseStatus = releaseStatus;
|
||||
this.DirectoryName = directoryName;
|
||||
this.LibraryId = libraryId;
|
||||
this.IgnoreChaptersBefore = ignoreChaptersBefore;
|
||||
this.Year = year;
|
||||
this.OriginalLanguage = originalLanguage;
|
||||
}
|
||||
|
||||
|
||||
public string CreatePublicationFolder()
|
||||
{
|
||||
string? publicationFolder = FullDirectoryPath;
|
||||
if (publicationFolder is null)
|
||||
throw new DirectoryNotFoundException("Publication folder not found");
|
||||
if(!Directory.Exists(publicationFolder))
|
||||
Directory.CreateDirectory(publicationFolder);
|
||||
if(RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
||||
File.SetUnixFileMode(publicationFolder, GroupRead | GroupWrite | GroupExecute | OtherRead | OtherWrite | OtherExecute | UserRead | UserWrite | UserExecute);
|
||||
return publicationFolder;
|
||||
}
|
||||
|
||||
//https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file
|
||||
//less than 32 is control *forbidden*
|
||||
//34 is " *forbidden*
|
||||
//42 is * *forbidden*
|
||||
//47 is / *forbidden*
|
||||
//58 is : *forbidden*
|
||||
//60 is < *forbidden*
|
||||
//62 is > *forbidden*
|
||||
//63 is ? *forbidden*
|
||||
//92 is \ *forbidden*
|
||||
//124 is | *forbidden*
|
||||
//127 is delete *forbidden*
|
||||
//Below 127 all except *******
|
||||
private static readonly int[] ForbiddenCharsBelow127 = [34, 42, 47, 58, 60, 62, 63, 92, 124, 127];
|
||||
//Above 127 none except *******
|
||||
private static readonly int[] IncludeCharsAbove127 = [128, 138, 142];
|
||||
//128 is € include
|
||||
//138 is Š include
|
||||
//142 is Ž include
|
||||
//152 through 255 looks fine except 157, 172, 173, 175 *******
|
||||
private static readonly int[] ForbiddenCharsAbove152 = [157, 172, 173, 175];
|
||||
private static string CleanDirectoryName(string name)
|
||||
{
|
||||
StringBuilder sb = new ();
|
||||
foreach (char c in name)
|
||||
{
|
||||
if (c >= 32 && c < 127 && ForbiddenCharsBelow127.Contains(c) == false)
|
||||
sb.Append(c);
|
||||
else if (c > 127 && c < 152 && IncludeCharsAbove127.Contains(c))
|
||||
sb.Append(c);
|
||||
else if(c >= 152 && c <= 255 && ForbiddenCharsAbove152.Contains(c) == false)
|
||||
sb.Append(c);
|
||||
}
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Merges another Manga (MangaConnectorIds and Chapters)
|
||||
/// </summary>
|
||||
/// <param name="other">The other <see cref="Manga" /> to merge</param>
|
||||
/// <param name="context"><see cref="MangaContext"/> to use for Database operations</param>
|
||||
/// <returns>An array of <see cref="MoveFileOrFolderWorker"/> for moving <see cref="Chapter"/> to new Directory</returns>
|
||||
public BaseWorker[] MergeFrom(Manga other, MangaContext context)
|
||||
{
|
||||
context.Mangas.Remove(other);
|
||||
List<BaseWorker> newJobs = new();
|
||||
|
||||
this.MangaConnectorIds = this.MangaConnectorIds
|
||||
.UnionBy(other.MangaConnectorIds, id => id.MangaConnectorName)
|
||||
.ToList();
|
||||
|
||||
foreach (Chapter otherChapter in other.Chapters)
|
||||
{
|
||||
string oldPath = otherChapter.FullArchiveFilePath;
|
||||
Chapter newChapter = new(this, otherChapter.ChapterNumber, otherChapter.VolumeNumber,
|
||||
otherChapter.Title);
|
||||
this.Chapters.Add(newChapter);
|
||||
string newPath = newChapter.FullArchiveFilePath;
|
||||
newJobs.Add(new MoveFileOrFolderWorker(newPath, oldPath));
|
||||
}
|
||||
|
||||
return newJobs.ToArray();
|
||||
}
|
||||
|
||||
public override string ToString() => $"{base.ToString()} {Name}";
|
||||
}
|
||||
|
||||
public enum MangaReleaseStatus : byte
|
||||
{
|
||||
Continuing = 0,
|
||||
Completed = 1,
|
||||
OnHiatus = 2,
|
||||
Cancelled = 3,
|
||||
Unreleased = 4
|
||||
}
|
70
API/Schema/MangaContext/MangaConnectorId.cs
Normal file
70
API/Schema/MangaContext/MangaConnectorId.cs
Normal file
@ -0,0 +1,70 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using API.Schema.MangaContext.MangaConnectors;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
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;
|
||||
|
||||
[JsonIgnore]
|
||||
public T Obj
|
||||
{
|
||||
get => _lazyLoader.Load(this, ref _obj) ?? throw new InvalidOperationException();
|
||||
internal set
|
||||
{
|
||||
ObjId = value.Key;
|
||||
_obj = value;
|
||||
}
|
||||
}
|
||||
|
||||
[StringLength(32)] [Required] public string MangaConnectorName { get; private set; } = null!;
|
||||
[JsonIgnore] private MangaConnector? _mangaConnector;
|
||||
[JsonIgnore]
|
||||
public MangaConnector MangaConnector
|
||||
{
|
||||
get => _lazyLoader.Load(this, ref _mangaConnector) ?? throw new InvalidOperationException();
|
||||
init
|
||||
{
|
||||
MangaConnectorName = value.Name;
|
||||
_mangaConnector = value;
|
||||
}
|
||||
}
|
||||
|
||||
[StringLength(256)] [Required] public string IdOnConnectorSite { get; init; }
|
||||
[Url] [StringLength(512)] public string? WebsiteUrl { get; internal init; }
|
||||
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.IdOnConnectorSite = idOnConnectorSite;
|
||||
this.WebsiteUrl = websiteUrl;
|
||||
this.UseForDownload = useForDownload;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EF CORE ONLY!!!
|
||||
/// </summary>
|
||||
public MangaConnectorId(ILazyLoader lazyLoader, 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;
|
||||
this.WebsiteUrl = websiteUrl;
|
||||
this.UseForDownload = useForDownload;
|
||||
}
|
||||
|
||||
public override string ToString() => $"{base.ToString()} {_obj}";
|
||||
}
|
258
API/Schema/MangaContext/MangaConnectors/ComickIo.cs
Normal file
258
API/Schema/MangaContext/MangaConnectors/ComickIo.cs
Normal file
@ -0,0 +1,258 @@
|
||||
using System.Text.RegularExpressions;
|
||||
using API.MangaDownloadClients;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace API.Schema.MangaContext.MangaConnectors;
|
||||
|
||||
public class ComickIo : MangaConnector
|
||||
{
|
||||
//https://api.comick.io/docs/
|
||||
//https://en.wikipedia.org/wiki/List_of_ISO_639_language_codes
|
||||
|
||||
public ComickIo() : base("ComickIo",
|
||||
["en","pt","pt-br","it","de","ru","aa","ab","ae","af","ak","am","an","ar-ae","ar-bh","ar-dz","ar-eg","ar-iq","ar-jo","ar-kw","ar-lb","ar-ly","ar-ma","ar-om","ar-qa","ar-sa","ar-sy","ar-tn","ar-ye","ar","as","av","ay","az","ba","be","bg","bh","bi","bm","bn","bo","br","bs","ca","ce","ch","co","cr","cs","cu","cv","cy","da","de-at","de-ch","de-de","de-li","de-lu","div","dv","dz","ee","el","en-au","en-bz","en-ca","en-cb","en-gb","en-ie","en-jm","en-nz","en-ph","en-tt","en-us","en-za","en-zw","eo","es-ar","es-bo","es-cl","es-co","es-cr","es-do","es-ec","es-es","es-gt","es-hn","es-la","es-mx","es-ni","es-pa","es-pe","es-pr","es-py","es-sv","es-us","es-uy","es-ve","es","et","eu","fa","ff","fi","fj","fo","fr-be","fr-ca","fr-ch","fr-fr","fr-lu","fr-mc","fr","fy","ga","gd","gl","gn","gu","gv","ha","he","hi","ho","hr-ba","hr-hr","hr","ht","hu","hy","hz","ia","id","ie","ig","ii","ik","in","io","is","it-ch","it-it","iu","iw","ja","ja-ro","ji","jv","jw","ka","kg","ki","kj","kk","kl","km","kn","ko","ko-ro","kr","ks","ku","kv","kw","ky","kz","la","lb","lg","li","ln","lo","ls","lt","lu","lv","mg","mh","mi","mk","ml","mn","mo","mr","ms-bn","ms-my","ms","mt","my","na","nb","nd","ne","ng","nl-be","nl-nl","nl","nn","no","nr","ns","nv","ny","oc","oj","om","or","os","pa","pi","pl","ps","pt-pt","qu-bo","qu-ec","qu-pe","qu","rm","rn","ro","rw","sa","sb","sc","sd","se-fi","se-no","se-se","se","sg","sh","si","sk","sl","sm","sn","so","sq","sr-ba","sr-sp","sr","ss","st","su","sv-fi","sv-se","sv","sw","sx","syr","ta","te","tg","th","ti","tk","tl","tn","to","tr","ts","tt","tw","ty","ug","uk","ur","us","uz","ve","vi","vo","wa","wo","xh","yi","yo","za","zh-cn","zh-hk","zh-mo","zh-ro","zh-sg","zh-tw","zh","zu"],
|
||||
["comick.io"],
|
||||
"https://comick.io/static/icons/unicorn-64.png")
|
||||
{
|
||||
this.downloadClient = new HttpDownloadClient();
|
||||
}
|
||||
|
||||
public override (Manga, MangaConnectorId<Manga>)[] SearchManga(string mangaSearchName)
|
||||
{
|
||||
Log.Info($"Searching Obj: {mangaSearchName}");
|
||||
|
||||
List<string> slugs = new();
|
||||
int page = 1;
|
||||
while(page < 50)
|
||||
{
|
||||
string requestUrl = $"https://api.comick.fun/v1.0/search/?type=comic&t=false&limit=100&showall=true&" +
|
||||
$"page={page}&q={mangaSearchName}";
|
||||
|
||||
RequestResult result = downloadClient.MakeRequest(requestUrl, RequestType.Default);
|
||||
if ((int)result.statusCode < 200 || (int)result.statusCode >= 300)
|
||||
{
|
||||
Log.Error("Request failed");
|
||||
return [];
|
||||
}
|
||||
|
||||
using StreamReader sr = new (result.result);
|
||||
JArray data = JArray.Parse(sr.ReadToEnd());
|
||||
|
||||
if (data.Count < 1)
|
||||
break;
|
||||
|
||||
slugs.AddRange(data.Select(token => token.Value<string>("slug")!));
|
||||
page++;
|
||||
}
|
||||
Log.Debug($"Search {mangaSearchName} yielded {slugs.Count} slugs. Requesting mangas now...");
|
||||
|
||||
|
||||
List<(Manga, MangaConnectorId<Manga>)> mangas = new ();
|
||||
foreach (string slug in slugs)
|
||||
{
|
||||
if(GetMangaFromId(slug) is { } entry)
|
||||
mangas.Add(entry);
|
||||
}
|
||||
|
||||
Log.Info($"Search {mangaSearchName} yielded {mangas.Count} results.");
|
||||
return mangas.ToArray();
|
||||
}
|
||||
|
||||
private readonly Regex _getSlugFromTitleRex = new(@"https?:\/\/comick\.io\/comic\/(.+)(?:\/.*)*");
|
||||
public override (Manga, MangaConnectorId<Manga>)? GetMangaFromUrl(string url)
|
||||
{
|
||||
Match m = _getSlugFromTitleRex.Match(url);
|
||||
return m.Groups[1].Success ? GetMangaFromId(m.Groups[1].Value) : null;
|
||||
}
|
||||
|
||||
public override (Manga, MangaConnectorId<Manga>)? GetMangaFromId(string mangaIdOnSite)
|
||||
{
|
||||
string requestUrl = $"https://api.comick.fun/comic/{mangaIdOnSite}";
|
||||
|
||||
RequestResult result = downloadClient.MakeRequest(requestUrl, RequestType.MangaInfo);
|
||||
if ((int)result.statusCode < 200 || (int)result.statusCode >= 300)
|
||||
{
|
||||
Log.Error("Request failed");
|
||||
return null;
|
||||
}
|
||||
using StreamReader sr = new (result.result);
|
||||
JToken data = JToken.Parse(sr.ReadToEnd());
|
||||
|
||||
return ParseMangaFromJToken(data);
|
||||
}
|
||||
|
||||
public override (Chapter, MangaConnectorId<Chapter>)[] GetChapters(MangaConnectorId<Manga> mangaConnectorId,
|
||||
string? language = null)
|
||||
{
|
||||
Log.Info($"Getting Chapters: {mangaConnectorId.IdOnConnectorSite}");
|
||||
List<(Chapter, MangaConnectorId<Chapter>)> chapters = new();
|
||||
int page = 1;
|
||||
while(page < 50)
|
||||
{
|
||||
string requestUrl = $"https://api.comick.fun/comic/{mangaConnectorId.IdOnConnectorSite}/chapters?limit=100&page={page}&lang={language}";
|
||||
|
||||
RequestResult result = downloadClient.MakeRequest(requestUrl, RequestType.MangaInfo);
|
||||
if ((int)result.statusCode < 200 || (int)result.statusCode >= 300)
|
||||
{
|
||||
Log.Error("Request failed");
|
||||
return [];
|
||||
}
|
||||
|
||||
using StreamReader sr = new (result.result);
|
||||
JToken data = JToken.Parse(sr.ReadToEnd());
|
||||
JArray? chaptersArray = data["chapters"] as JArray;
|
||||
|
||||
if (chaptersArray is null || chaptersArray.Count < 1)
|
||||
break;
|
||||
|
||||
chapters.AddRange(ParseChapters(mangaConnectorId, chaptersArray));
|
||||
|
||||
page++;
|
||||
}
|
||||
|
||||
return chapters.ToArray();
|
||||
}
|
||||
|
||||
private readonly Regex _hidFromUrl = new(@"https?:\/\/comick\.io\/comic\/.+\/([^-]+).*");
|
||||
internal override string[] GetChapterImageUrls(MangaConnectorId<Chapter> chapterId)
|
||||
{
|
||||
|
||||
Log.Info($"Getting Chapter Image-Urls: {chapterId.Obj}");
|
||||
if (chapterId.WebsiteUrl is null || !UrlMatchesConnector(chapterId.WebsiteUrl))
|
||||
{
|
||||
Log.Debug($"Url is not for Connector. {chapterId.WebsiteUrl}");
|
||||
return [];
|
||||
}
|
||||
|
||||
Match m = _hidFromUrl.Match(chapterId.WebsiteUrl);
|
||||
if (!m.Groups[1].Success)
|
||||
{
|
||||
Log.Debug($"Could not parse hid from url. {chapterId.WebsiteUrl}");
|
||||
return [];
|
||||
}
|
||||
|
||||
string hid = m.Groups[1].Value;
|
||||
|
||||
string requestUrl = $"https://api.comick.fun/chapter/{hid}/get_images";
|
||||
RequestResult result = downloadClient.MakeRequest(requestUrl, RequestType.MangaInfo);
|
||||
if ((int)result.statusCode < 200 || (int)result.statusCode >= 300)
|
||||
{
|
||||
Log.Error("Request failed");
|
||||
return [];
|
||||
}
|
||||
|
||||
using StreamReader sr = new (result.result);
|
||||
JArray data = JArray.Parse(sr.ReadToEnd());
|
||||
|
||||
return data.Select(token =>
|
||||
{
|
||||
string url = $"https://meo.comick.pictures/{token.Value<string>("b2key")}";
|
||||
return url;
|
||||
}).ToArray();
|
||||
}
|
||||
|
||||
private (Manga manga, MangaConnectorId<Manga> id) ParseMangaFromJToken(JToken json)
|
||||
{
|
||||
string? hid = json["comic"]?.Value<string>("hid");
|
||||
string? slug = json["comic"]?.Value<string>("slug");
|
||||
string? name = json["comic"]?.Value<string>("title");
|
||||
string? description = json["comic"]?.Value<string>("desc");
|
||||
string? originalLanguage = json["comic"]?.Value<string>("country");
|
||||
string url = $"https://comick.io/comic/{slug}";
|
||||
string? coverName = json["comic"]?["md_covers"]?.First?.Value<string>("b2key");
|
||||
string coverUrl = $"https://meo.comick.pictures/{coverName}";
|
||||
int? releaseStatusStr = json["comic"]?.Value<int>("status");
|
||||
MangaReleaseStatus status = releaseStatusStr switch
|
||||
{
|
||||
1 => MangaReleaseStatus.Continuing,
|
||||
2 => MangaReleaseStatus.Completed,
|
||||
3 => MangaReleaseStatus.Cancelled,
|
||||
4 => MangaReleaseStatus.OnHiatus,
|
||||
_ => MangaReleaseStatus.Unreleased
|
||||
};
|
||||
uint? year = json["comic"]?.Value<uint?>("year");
|
||||
JArray? altTitlesArray = json["comic"]?["md_titles"] as JArray;
|
||||
//Cant let language be null, so fill with whatever.
|
||||
byte whatever = 0;
|
||||
List<AltTitle> altTitles = altTitlesArray?
|
||||
.Select(token => new AltTitle(token.Value<string>("lang")??whatever++.ToString(), token.Value<string>("title")!))
|
||||
.ToList()!;
|
||||
|
||||
JArray? authorsArray = json["authors"] as JArray;
|
||||
JArray? artistsArray = json["artists"] as JArray;
|
||||
List<Author> authors = authorsArray?.Concat(artistsArray!)
|
||||
.Select(token => new Author(token.Value<string>("name")!))
|
||||
.DistinctBy(a => a.Key)
|
||||
.ToList()!;
|
||||
|
||||
JArray? genreArray = json["comic"]?["md_comic_md_genres"] as JArray;
|
||||
List<MangaTag> tags = genreArray?
|
||||
.Select(token => new MangaTag(token["md_genres"]?.Value<string>("name")!))
|
||||
.ToList()!;
|
||||
|
||||
JArray? linksArray = json["comic"]?["links"] as JArray;
|
||||
List<Link> links = linksArray?
|
||||
.ToObject<Dictionary<string,string>>()?
|
||||
.Select(kv =>
|
||||
{
|
||||
string fullUrl = kv.Key switch
|
||||
{
|
||||
"al" => $"https://anilist.co/manga/{kv.Value}",
|
||||
"ap" => $"https://www.anime-planet.com/manga/{kv.Value}",
|
||||
"bw" => $"https://bookwalker.jp/{kv.Value}",
|
||||
"mu" => $"https://www.mangaupdates.com/series.html?id={kv.Value}",
|
||||
"nu" => $"https://www.novelupdates.com/series/{kv.Value}",
|
||||
"mal" => $"https://myanimelist.net/manga/{kv.Value}",
|
||||
_ => kv.Value
|
||||
};
|
||||
string key = kv.Key switch
|
||||
{
|
||||
"al" => "AniList",
|
||||
"ap" => "Anime Planet",
|
||||
"bw" => "BookWalker",
|
||||
"mu" => "Obj Updates",
|
||||
"nu" => "Novel Updates",
|
||||
"kt" => "Kitsu.io",
|
||||
"amz" => "Amazon",
|
||||
"ebj" => "eBookJapan",
|
||||
"mal" => "MyAnimeList",
|
||||
"cdj" => "CDJapan",
|
||||
_ => kv.Key
|
||||
};
|
||||
return new Link(key, fullUrl);
|
||||
}).ToList()!;
|
||||
|
||||
if(hid is null)
|
||||
throw new Exception("hid is null");
|
||||
if(slug is null)
|
||||
throw new Exception("slug is null");
|
||||
if(name is null)
|
||||
throw new Exception("name is null");
|
||||
|
||||
Manga manga = new (name, description??"", coverUrl, status, authors, tags, links, altTitles,
|
||||
year: year, originalLanguage: originalLanguage);
|
||||
return (manga, new MangaConnectorId<Manga>(manga, this, hid, url));
|
||||
}
|
||||
|
||||
private List<(Chapter, MangaConnectorId<Chapter>)> ParseChapters(MangaConnectorId<Manga> mcIdManga, JArray chaptersArray)
|
||||
{
|
||||
List<(Chapter, MangaConnectorId<Chapter>)> chapters = new ();
|
||||
foreach (JToken chapter in chaptersArray)
|
||||
{
|
||||
string? chapterNum = chapter.Value<string>("chap");
|
||||
string? volumeNumStr = chapter.Value<string>("vol");
|
||||
int? volumeNum = volumeNumStr is null ? null : int.Parse(volumeNumStr);
|
||||
string? title = chapter.Value<string>("title");
|
||||
string? hid = chapter.Value<string>("hid");
|
||||
string url = $"https://comick.io/comic/{mcIdManga.IdOnConnectorSite}/{hid}";
|
||||
|
||||
if(chapterNum is null || hid is null)
|
||||
continue;
|
||||
|
||||
Chapter ch = new (mcIdManga.Obj, chapterNum, volumeNum, title);
|
||||
|
||||
chapters.Add((ch, new (ch, this, hid, url)));
|
||||
}
|
||||
return chapters;
|
||||
}
|
||||
}
|
54
API/Schema/MangaContext/MangaConnectors/Global.cs
Normal file
54
API/Schema/MangaContext/MangaConnectors/Global.cs
Normal file
@ -0,0 +1,54 @@
|
||||
namespace API.Schema.MangaContext.MangaConnectors;
|
||||
|
||||
public class Global : MangaConnector
|
||||
{
|
||||
private MangaContext context { get; init; }
|
||||
public Global(MangaContext context) : 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();
|
||||
|
||||
//Create Task for each MangaConnector to search simultaneously
|
||||
Task<(Manga, MangaConnectorId<Manga>)[]>[] tasks =
|
||||
enabledConnectors.Select(c => new Task<(Manga, MangaConnectorId<Manga>)[]>(() => c.SearchManga(mangaSearchName))).ToArray();
|
||||
foreach (var task in tasks)
|
||||
task.Start();
|
||||
|
||||
//Wait for all tasks to finish
|
||||
do
|
||||
{
|
||||
Thread.Sleep(50);
|
||||
}while(tasks.Any(t => t.Status < TaskStatus.RanToCompletion));
|
||||
|
||||
//Concatenate all results into one
|
||||
(Manga, MangaConnectorId<Manga>)[] ret = tasks.Select(t => t.IsCompletedSuccessfully ? t.Result : []).ToArray().SelectMany(i => i).ToArray();
|
||||
return ret;
|
||||
}
|
||||
|
||||
public override (Manga, MangaConnectorId<Manga>)? GetMangaFromUrl(string url)
|
||||
{
|
||||
MangaConnector? mc = context.MangaConnectors.ToArray().FirstOrDefault(c => c.UrlMatchesConnector(url));
|
||||
return mc?.GetMangaFromUrl(url) ?? null;
|
||||
}
|
||||
|
||||
public override (Manga, MangaConnectorId<Manga>)? GetMangaFromId(string mangaIdOnSite)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
public override (Chapter, MangaConnectorId<Chapter>)[] GetChapters(MangaConnectorId<Manga> manga,
|
||||
string? language = null)
|
||||
{
|
||||
return manga.MangaConnector.GetChapters(manga, language);
|
||||
}
|
||||
|
||||
internal override string[] GetChapterImageUrls(MangaConnectorId<Chapter> chapterId)
|
||||
{
|
||||
return chapterId.MangaConnector.GetChapterImageUrls(chapterId);
|
||||
}
|
||||
}
|
@ -6,7 +6,7 @@ using log4net;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace API.Schema.MangaConnectors;
|
||||
namespace API.Schema.MangaContext.MangaConnectors;
|
||||
|
||||
[PrimaryKey("Name")]
|
||||
public abstract class MangaConnector(string name, string[] supportedLanguages, string[] baseUris, string iconUrl)
|
||||
@ -34,35 +34,36 @@ public abstract class MangaConnector(string name, string[] supportedLanguages, s
|
||||
[Required]
|
||||
public bool Enabled { get; internal set; } = true;
|
||||
|
||||
public abstract Manga[] SearchManga(string mangaSearchName);
|
||||
public abstract (Manga, MangaConnectorId<Manga>)[] SearchManga(string mangaSearchName);
|
||||
|
||||
public abstract Manga? GetMangaFromUrl(string url);
|
||||
public abstract (Manga, MangaConnectorId<Manga>)? GetMangaFromUrl(string url);
|
||||
|
||||
public abstract Manga? GetMangaFromId(string mangaIdOnSite);
|
||||
public abstract (Manga, MangaConnectorId<Manga>)? GetMangaFromId(string mangaIdOnSite);
|
||||
|
||||
public abstract Chapter[] GetChapters(Manga manga, string? language = null);
|
||||
public abstract (Chapter, MangaConnectorId<Chapter>)[] GetChapters(MangaConnectorId<Manga> mangaId,
|
||||
string? language = null);
|
||||
|
||||
internal abstract string[] GetChapterImageUrls(Chapter chapter);
|
||||
internal abstract string[] GetChapterImageUrls(MangaConnectorId<Chapter> chapterId);
|
||||
|
||||
public bool UrlMatchesConnector(string url) => BaseUris.Any(baseUri => Regex.IsMatch(url, "https?://" + baseUri + "/.*"));
|
||||
|
||||
internal string? SaveCoverImageToCache(Manga manga, int retries = 3)
|
||||
internal string? SaveCoverImageToCache(MangaConnectorId<Manga> mangaId, int retries = 3)
|
||||
{
|
||||
if(retries < 0)
|
||||
return null;
|
||||
|
||||
Regex urlRex = new (@"https?:\/\/((?:[a-zA-Z0-9-]+\.)+[a-zA-Z0-9]+)\/(?:.+\/)*(.+\.([a-zA-Z]+))");
|
||||
//https?:\/\/[a-zA-Z0-9-]+\.([a-zA-Z0-9-]+\.[a-zA-Z0-9]+)\/(?:.+\/)*(.+\.([a-zA-Z]+)) for only second level domains
|
||||
Match match = urlRex.Match(manga.CoverUrl);
|
||||
string filename = $"{match.Groups[1].Value}-{manga.MangaId}.{match.Groups[3].Value}";
|
||||
Match match = urlRex.Match(mangaId.Obj.CoverUrl);
|
||||
string filename = $"{match.Groups[1].Value}-{mangaId.Key}.{match.Groups[3].Value}";
|
||||
string saveImagePath = Path.Join(TrangaSettings.coverImageCache, filename);
|
||||
|
||||
if (File.Exists(saveImagePath))
|
||||
return saveImagePath;
|
||||
|
||||
RequestResult coverResult = downloadClient.MakeRequest(manga.CoverUrl, RequestType.MangaCover, $"https://{match.Groups[1].Value}");
|
||||
RequestResult coverResult = downloadClient.MakeRequest(mangaId.Obj.CoverUrl, RequestType.MangaCover, $"https://{match.Groups[1].Value}");
|
||||
if ((int)coverResult.statusCode < 200 || (int)coverResult.statusCode >= 300)
|
||||
return SaveCoverImageToCache(manga, --retries);
|
||||
return SaveCoverImageToCache(mangaId, --retries);
|
||||
|
||||
using MemoryStream ms = new();
|
||||
coverResult.result.CopyTo(ms);
|
@ -2,7 +2,7 @@
|
||||
using API.MangaDownloadClients;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace API.Schema.MangaConnectors;
|
||||
namespace API.Schema.MangaContext.MangaConnectors;
|
||||
|
||||
public class MangaDex : MangaConnector
|
||||
{
|
||||
@ -18,10 +18,10 @@ public class MangaDex : MangaConnector
|
||||
}
|
||||
|
||||
private const int Limit = 100;
|
||||
public override Manga[] SearchManga(string mangaSearchName)
|
||||
public override (Manga, MangaConnectorId<Manga>)[] SearchManga(string mangaSearchName)
|
||||
{
|
||||
Log.Info($"Searching Manga: {mangaSearchName}");
|
||||
List<Manga> mangas = new ();
|
||||
Log.Info($"Searching Obj: {mangaSearchName}");
|
||||
List<(Manga, MangaConnectorId<Manga>)> mangas = new ();
|
||||
|
||||
int offset = 0;
|
||||
int total = int.MaxValue;
|
||||
@ -67,9 +67,9 @@ public class MangaDex : MangaConnector
|
||||
}
|
||||
|
||||
private static readonly Regex GetMangaIdFromUrl = new(@"https?:\/\/mangadex\.org\/title\/([a-z0-9-]+)\/?.*");
|
||||
public override Manga? GetMangaFromUrl(string url)
|
||||
public override (Manga, MangaConnectorId<Manga>)? GetMangaFromUrl(string url)
|
||||
{
|
||||
Log.Info($"Getting Manga: {url}");
|
||||
Log.Info($"Getting Obj: {url}");
|
||||
if (!UrlMatchesConnector(url))
|
||||
{
|
||||
Log.Debug($"Url is not for Connector. {url}");
|
||||
@ -87,9 +87,9 @@ public class MangaDex : MangaConnector
|
||||
return GetMangaFromId(id);
|
||||
}
|
||||
|
||||
public override Manga? GetMangaFromId(string mangaIdOnSite)
|
||||
public override (Manga, MangaConnectorId<Manga>)? GetMangaFromId(string mangaIdOnSite)
|
||||
{
|
||||
Log.Info($"Getting Manga: {mangaIdOnSite}");
|
||||
Log.Info($"Getting Obj: {mangaIdOnSite}");
|
||||
string requestUrl =
|
||||
$"https://api.mangadex.org/manga/{mangaIdOnSite}" +
|
||||
$"?includes%5B%5D=manga&includes%5B%5D=cover_art&includes%5B%5D=author&includes%5B%5D=artist&includes%5B%5D=tag'";
|
||||
@ -118,14 +118,13 @@ public class MangaDex : MangaConnector
|
||||
return null;
|
||||
}
|
||||
|
||||
Manga manga = ParseMangaFromJToken(data);
|
||||
return manga;
|
||||
return ParseMangaFromJToken(data);
|
||||
}
|
||||
|
||||
public override Chapter[] GetChapters(Manga manga, string? language = null)
|
||||
public override (Chapter, MangaConnectorId<Chapter>)[] GetChapters(MangaConnectorId<Manga> manga, string? language = null)
|
||||
{
|
||||
Log.Info($"Getting Chapters: {manga.IdOnConnectorSite}");
|
||||
List<Chapter> chapters = new ();
|
||||
List<(Chapter, MangaConnectorId<Chapter>)> chapters = new ();
|
||||
|
||||
int offset = 0;
|
||||
int total = int.MaxValue;
|
||||
@ -166,24 +165,24 @@ public class MangaDex : MangaConnector
|
||||
chapters.AddRange(data.Select(d => ParseChapterFromJToken(manga, d)));
|
||||
}
|
||||
|
||||
Log.Info($"Request for chapters for {manga.Name} yielded {chapters.Count} results.");
|
||||
Log.Info($"Request for chapters for {manga.Obj.Name} yielded {chapters.Count} results.");
|
||||
return chapters.ToArray();
|
||||
}
|
||||
|
||||
private static readonly Regex GetChapterIdFromUrl = new(@"https?:\/\/mangadex\.org\/chapter\/([a-z0-9-]+)\/?.*");
|
||||
internal override string[] GetChapterImageUrls(Chapter chapter)
|
||||
internal override string[] GetChapterImageUrls(MangaConnectorId<Chapter> chapterId)
|
||||
{
|
||||
Log.Info($"Getting Chapter Image-Urls: {chapter.Url}");
|
||||
if (!UrlMatchesConnector(chapter.Url))
|
||||
Log.Info($"Getting Chapter Image-Urls: {chapterId.Obj}");
|
||||
if (chapterId.WebsiteUrl is null || !UrlMatchesConnector(chapterId.WebsiteUrl))
|
||||
{
|
||||
Log.Debug($"Url is not for Connector. {chapter.Url}");
|
||||
Log.Debug($"Url is not for Connector. {chapterId.WebsiteUrl}");
|
||||
return [];
|
||||
}
|
||||
|
||||
Match match = GetChapterIdFromUrl.Match(chapter.Url);
|
||||
Match match = GetChapterIdFromUrl.Match(chapterId.WebsiteUrl);
|
||||
if (!match.Success || !match.Groups[1].Success)
|
||||
{
|
||||
Log.Debug($"Url is not for Connector (Could not retrieve id). {chapter.Url}");
|
||||
Log.Debug($"Url is not for Connector (Could not retrieve id). {chapterId.WebsiteUrl}");
|
||||
return [];
|
||||
}
|
||||
|
||||
@ -223,25 +222,28 @@ public class MangaDex : MangaConnector
|
||||
return urls.ToArray();
|
||||
}
|
||||
|
||||
private Manga ParseMangaFromJToken(JToken jToken)
|
||||
private (Manga manga, MangaConnectorId<Manga> id) ParseMangaFromJToken(JToken jToken)
|
||||
{
|
||||
string? id = jToken.Value<string>("id");
|
||||
if(id is null)
|
||||
throw new Exception("jToken was not in expected format");
|
||||
|
||||
JObject? attributes = jToken["attributes"] as JObject;
|
||||
string? name = attributes?["title"]?.Value<string>("en");
|
||||
string? description = attributes?["description"]?.Value<string>("en");
|
||||
string? status = attributes?["status"]?.Value<string>();
|
||||
uint? year = attributes?["year"]?.Value<uint>();
|
||||
string? originalLanguage = attributes?["originalLanguage"]?.Value<string>();
|
||||
JArray? altTitlesJArray = attributes?["altTitles"] as JArray;
|
||||
JArray? tagsJArray = attributes?["tags"] as JArray;
|
||||
|
||||
if(attributes is null)
|
||||
throw new Exception("jToken was not in expected format");
|
||||
string? name = attributes["title"]?.Value<string>("en") ?? attributes["title"]?.First?.First?.Value<string>();
|
||||
string description = attributes["description"]?.Value<string>("en")??attributes["description"]?.First?.First?.Value<string>()??"";
|
||||
string? status = attributes["status"]?.Value<string>();
|
||||
uint? year = attributes["year"]?.Value<uint?>();
|
||||
string? originalLanguage = attributes["originalLanguage"]?.Value<string>();
|
||||
JArray? altTitlesJArray = attributes.TryGetValue("altTitles", out JToken? altTitlesArray) ? altTitlesArray as JArray : null;
|
||||
JArray? tagsJArray = attributes.TryGetValue("tags", out JToken? tagsArray) ? tagsArray as JArray : null;
|
||||
JArray? relationships = jToken["relationships"] as JArray;
|
||||
string? coverFileName =
|
||||
relationships?.FirstOrDefault(r => r["type"]?.Value<string>() == "cover_art")?["attributes"]?.Value<string>("fileName");
|
||||
if (name is null || status is null || relationships is null)
|
||||
throw new Exception("jToken was not in expected format");
|
||||
|
||||
if (id is null || attributes is null || name is null || description is null || status is null ||
|
||||
altTitlesJArray is null || tagsJArray is null || relationships is null || coverFileName is null)
|
||||
string? coverFileName = relationships.FirstOrDefault(r => r["type"]?.Value<string>() == "cover_art")?["attributes"]?.Value<string>("fileName");
|
||||
if(coverFileName is null)
|
||||
throw new Exception("jToken was not in expected format");
|
||||
|
||||
List<Link> links = attributes["links"]?
|
||||
@ -264,7 +266,7 @@ public class MangaDex : MangaConnector
|
||||
"al" => "AniList",
|
||||
"ap" => "Anime Planet",
|
||||
"bw" => "BookWalker",
|
||||
"mu" => "Manga Updates",
|
||||
"mu" => "Obj Updates",
|
||||
"nu" => "Novel Updates",
|
||||
"kt" => "Kitsu.io",
|
||||
"amz" => "Amazon",
|
||||
@ -276,19 +278,19 @@ public class MangaDex : MangaConnector
|
||||
return new Link(key, url);
|
||||
}).ToList()!;
|
||||
|
||||
List<MangaAltTitle> altTitles = altTitlesJArray
|
||||
List<AltTitle> altTitles = (altTitlesJArray??[])
|
||||
.Select(t =>
|
||||
{
|
||||
JObject? j = t as JObject;
|
||||
JProperty? p = j?.Properties().First();
|
||||
if (p is null)
|
||||
return null;
|
||||
return new MangaAltTitle(p.Name, p.Value.ToString());
|
||||
return new AltTitle(p.Name, p.Value.ToString());
|
||||
}).Where(x => x is not null).ToList()!;
|
||||
|
||||
List<MangaTag> tags = tagsJArray
|
||||
List<MangaTag> tags = (tagsJArray??[])
|
||||
.Where(t => t.Value<string>("type") == "tag")
|
||||
.Select(t => t["attributes"]?["name"]?.Value<string>("en"))
|
||||
.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()!;
|
||||
|
||||
@ -310,26 +312,27 @@ public class MangaDex : MangaConnector
|
||||
string websiteUrl = $"https://mangadex.org/title/{id}";
|
||||
string coverUrl = $"https://uploads.mangadex.org/covers/{id}/{coverFileName}";
|
||||
|
||||
return new Manga(id, name, description, websiteUrl, coverUrl, releaseStatus, this,
|
||||
authors, tags, links,altTitles,
|
||||
Manga manga = new Manga(name, description, coverUrl, releaseStatus, authors, tags, links,altTitles,
|
||||
null, 0f, year, originalLanguage);
|
||||
return (manga, new MangaConnectorId<Manga>(manga, this, id, websiteUrl));
|
||||
}
|
||||
|
||||
private Chapter ParseChapterFromJToken(Manga parentManga, JToken jToken)
|
||||
private (Chapter chapter, MangaConnectorId<Chapter> id) ParseChapterFromJToken(MangaConnectorId<Manga> mcIdManga, JToken jToken)
|
||||
{
|
||||
string? id = jToken.Value<string>("id");
|
||||
JToken? attributes = jToken["attributes"];
|
||||
string? chapter = attributes?.Value<string>("chapter");
|
||||
string? chapterStr = attributes?.Value<string>("chapter");
|
||||
string? volumeStr = attributes?.Value<string>("volume");
|
||||
int? volume = null;
|
||||
int? volumeNumber = null;
|
||||
string? title = attributes?.Value<string>("title");
|
||||
|
||||
if(id is null || chapter is null)
|
||||
if(id is null || chapterStr is null)
|
||||
throw new Exception("jToken was not in expected format");
|
||||
if(volumeStr is not null)
|
||||
volume = int.Parse(volumeStr);
|
||||
volumeNumber = int.Parse(volumeStr);
|
||||
|
||||
string url = $"https://mangadex.org/chapter/{id}";
|
||||
return new Chapter(parentManga, url, chapter, volume, title);
|
||||
string websiteUrl = $"https://mangadex.org/chapter/{id}";
|
||||
Chapter chapter = new (mcIdManga.Obj, chapterStr, volumeNumber, title);
|
||||
return (chapter, new MangaConnectorId<Chapter>(chapter, this, id, websiteUrl));
|
||||
}
|
||||
}
|
134
API/Schema/MangaContext/MangaContext.cs
Normal file
134
API/Schema/MangaContext/MangaContext.cs
Normal file
@ -0,0 +1,134 @@
|
||||
using API.Schema.MangaContext.MangaConnectors;
|
||||
using API.Schema.MangaContext.MetadataFetchers;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
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; }
|
||||
public DbSet<Author> Authors { get; set; }
|
||||
public DbSet<MangaTag> Tags { get; set; }
|
||||
public DbSet<MangaConnectorId<Manga>> MangaConnectorToManga { get; set; }
|
||||
public DbSet<MangaConnectorId<Chapter>> MangaConnectorToChapter { get; set; }
|
||||
public DbSet<MetadataEntry> MetadataEntries { get; set; }
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
//MangaConnector Types
|
||||
modelBuilder.Entity<MangaConnector>()
|
||||
.HasDiscriminator(c => c.Name)
|
||||
.HasValue<Global>("Global")
|
||||
.HasValue<MangaDex>("MangaDex")
|
||||
.HasValue<ComickIo>("ComickIo");
|
||||
|
||||
//Manga has many Chapters
|
||||
modelBuilder.Entity<Manga>()
|
||||
.HasMany<Chapter>(m => m.Chapters)
|
||||
.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)
|
||||
.WithOwner();
|
||||
modelBuilder.Entity<Manga>()
|
||||
.Navigation(m => m.AltTitles)
|
||||
.AutoInclude();
|
||||
//Manga owns Links
|
||||
modelBuilder.Entity<Manga>()
|
||||
.OwnsMany<Link>(m => m.Links)
|
||||
.WithOwner();
|
||||
modelBuilder.Entity<Manga>()
|
||||
.Navigation(m => m.Links)
|
||||
.AutoInclude();
|
||||
//Manga has many Tags associated with many Obj
|
||||
modelBuilder.Entity<Manga>()
|
||||
.HasMany<MangaTag>(m => m.MangaTags)
|
||||
.WithMany()
|
||||
.UsingEntity("MangaTagToManga",
|
||||
l=> l.HasOne(typeof(MangaTag)).WithMany().HasForeignKey("MangaTagIds").HasPrincipalKey(nameof(MangaTag.Tag)),
|
||||
r => r.HasOne(typeof(Manga)).WithMany().HasForeignKey("MangaIds").HasPrincipalKey(nameof(Manga.Key)),
|
||||
j => j.HasKey("MangaTagIds", "MangaIds")
|
||||
);
|
||||
modelBuilder.Entity<Manga>()
|
||||
.Navigation(m => m.MangaTags)
|
||||
.AutoInclude();
|
||||
//Manga has many Authors associated with many Obj
|
||||
modelBuilder.Entity<Manga>()
|
||||
.HasMany<Author>(m => m.Authors)
|
||||
.WithMany()
|
||||
.UsingEntity("AuthorToManga",
|
||||
l=> l.HasOne(typeof(Author)).WithMany().HasForeignKey("AuthorIds").HasPrincipalKey(nameof(Author.Key)),
|
||||
r => r.HasOne(typeof(Manga)).WithMany().HasForeignKey("MangaIds").HasPrincipalKey(nameof(Manga.Key)),
|
||||
j => j.HasKey("AuthorIds", "MangaIds")
|
||||
);
|
||||
modelBuilder.Entity<Manga>()
|
||||
.Navigation(m => m.Authors)
|
||||
.AutoInclude();
|
||||
//Manga has many MangaIds
|
||||
modelBuilder.Entity<Manga>()
|
||||
.HasMany<MangaConnectorId<Manga>>(m => m.MangaConnectorIds)
|
||||
.WithOne(id => id.Obj)
|
||||
.HasForeignKey(id => id.ObjId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
modelBuilder.Entity<Manga>()
|
||||
.Navigation(m => m.MangaConnectorIds)
|
||||
.EnableLazyLoading();
|
||||
modelBuilder.Entity<MangaConnectorId<Manga>>()
|
||||
.HasOne<MangaConnector>(id => id.MangaConnector)
|
||||
.WithMany()
|
||||
.HasForeignKey(id => id.MangaConnectorName)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
modelBuilder.Entity<MangaConnectorId<Manga>>()
|
||||
.Navigation(entry => entry.MangaConnector)
|
||||
.EnableLazyLoading();
|
||||
|
||||
|
||||
//FileLibrary has many Mangas
|
||||
modelBuilder.Entity<FileLibrary>()
|
||||
.HasMany<Manga>()
|
||||
.WithOne(m => m.Library)
|
||||
.HasForeignKey(m => m.LibraryId)
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
modelBuilder.Entity<Manga>()
|
||||
.Navigation(m => m.Library)
|
||||
.EnableLazyLoading();
|
||||
|
||||
modelBuilder.Entity<MetadataFetcher>()
|
||||
.HasDiscriminator<string>(nameof(MetadataEntry))
|
||||
.HasValue<MyAnimeList>(nameof(MyAnimeList));
|
||||
//MetadataEntry
|
||||
modelBuilder.Entity<MetadataEntry>()
|
||||
.HasOne<Manga>(entry => entry.Manga)
|
||||
.WithMany()
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
modelBuilder.Entity<MetadataEntry>()
|
||||
.HasOne<MetadataFetcher>(entry => entry.MetadataFetcher)
|
||||
.WithMany()
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
}
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace API.Schema;
|
||||
namespace API.Schema.MangaContext;
|
||||
|
||||
[PrimaryKey("Tag")]
|
||||
public class MangaTag(string tag)
|
||||
@ -10,8 +10,5 @@ public class MangaTag(string tag)
|
||||
[Required]
|
||||
public string Tag { get; init; } = tag;
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{Tag}";
|
||||
}
|
||||
public override string ToString() => Tag;
|
||||
}
|
35
API/Schema/MangaContext/MetadataFetchers/MetadataEntry.cs
Normal file
35
API/Schema/MangaContext/MetadataFetchers/MetadataEntry.cs
Normal file
@ -0,0 +1,35 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace API.Schema.MangaContext.MetadataFetchers;
|
||||
|
||||
[PrimaryKey("MetadataFetcherName", "Identifier")]
|
||||
public class MetadataEntry
|
||||
{
|
||||
[JsonIgnore]
|
||||
public Manga Manga { get; init; } = null!;
|
||||
public string MangaId { get; init; }
|
||||
[JsonIgnore]
|
||||
public MetadataFetcher MetadataFetcher { get; init; } = null!;
|
||||
public string MetadataFetcherName { get; init; }
|
||||
public string Identifier { get; init; }
|
||||
|
||||
public MetadataEntry(MetadataFetcher fetcher, Manga manga, string identifier)
|
||||
{
|
||||
this.Manga = manga;
|
||||
this.MangaId = manga.Key;
|
||||
this.MetadataFetcher = fetcher;
|
||||
this.MetadataFetcherName = fetcher.Name;
|
||||
this.Identifier = identifier;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EFCORE only!!!!
|
||||
/// </summary>
|
||||
internal MetadataEntry(string mangaId, string identifier, string metadataFetcherName)
|
||||
{
|
||||
this.MangaId = mangaId;
|
||||
this.Identifier = identifier;
|
||||
this.MetadataFetcherName = metadataFetcherName;
|
||||
}
|
||||
}
|
35
API/Schema/MangaContext/MetadataFetchers/MetadataFetcher.cs
Normal file
35
API/Schema/MangaContext/MetadataFetchers/MetadataFetcher.cs
Normal file
@ -0,0 +1,35 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace API.Schema.MangaContext.MetadataFetchers;
|
||||
|
||||
[PrimaryKey("Name")]
|
||||
public abstract class MetadataFetcher
|
||||
{
|
||||
// ReSharper disable once EntityFramework.ModelValidation.UnlimitedStringLength
|
||||
public string Name { get; init; }
|
||||
|
||||
protected MetadataFetcher()
|
||||
{
|
||||
this.Name = this.GetType().Name;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EFCORE ONLY!!!
|
||||
/// </summary>
|
||||
internal MetadataFetcher(string name)
|
||||
{
|
||||
this.Name = name;
|
||||
}
|
||||
|
||||
internal MetadataEntry CreateMetadataEntry(Manga manga, string identifier) =>
|
||||
new (this, manga, identifier);
|
||||
|
||||
public abstract MetadataSearchResult[] SearchMetadataEntry(Manga manga);
|
||||
|
||||
public abstract MetadataSearchResult[] SearchMetadataEntry(string searchTerm);
|
||||
|
||||
/// <summary>
|
||||
/// Updates the Manga linked in the MetadataEntry
|
||||
/// </summary>
|
||||
public abstract void UpdateMetadata(MetadataEntry metadataEntry, MangaContext dbContext);
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
namespace API.Schema.MangaContext.MetadataFetchers;
|
||||
|
||||
public record MetadataSearchResult(string Identifier, string Name, string Url, string? Description = null, string? CoverUrl = null);
|
78
API/Schema/MangaContext/MetadataFetchers/MyAnimeList.cs
Normal file
78
API/Schema/MangaContext/MetadataFetchers/MyAnimeList.cs
Normal file
@ -0,0 +1,78 @@
|
||||
using System.Text.RegularExpressions;
|
||||
using JikanDotNet;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace API.Schema.MangaContext.MetadataFetchers;
|
||||
|
||||
public class MyAnimeList : MetadataFetcher
|
||||
{
|
||||
private static readonly Jikan Jikan = new ();
|
||||
private static readonly Regex GetIdFromUrl = new(@"https?:\/\/myanimelist\.net\/manga\/([0-9]+)\/?.*");
|
||||
|
||||
public override MetadataSearchResult[] SearchMetadataEntry(Manga manga)
|
||||
{
|
||||
if (manga.Links.Any(link => link.LinkProvider.Equals("MyAnimeList", StringComparison.InvariantCultureIgnoreCase)))
|
||||
{
|
||||
string url = manga.Links.First(link => link.LinkProvider.Equals("MyAnimeList", StringComparison.InvariantCultureIgnoreCase)).LinkUrl;
|
||||
Match m = GetIdFromUrl.Match(url);
|
||||
if (m.Success && m.Groups[1].Success)
|
||||
{
|
||||
long id = long.Parse(m.Groups[1].Value);
|
||||
JikanDotNet.Manga data = Jikan.GetMangaAsync(id).Result.Data;
|
||||
return [new MetadataSearchResult(id.ToString(), data.Titles.First().Title, data.Url, data.Synopsis)];
|
||||
}
|
||||
}
|
||||
|
||||
return SearchMetadataEntry(manga.Name);
|
||||
}
|
||||
|
||||
public override MetadataSearchResult[] SearchMetadataEntry(string searchTerm)
|
||||
{
|
||||
|
||||
ICollection<JikanDotNet.Manga> resultData = Jikan.SearchMangaAsync(searchTerm).Result.Data;
|
||||
if (resultData.Count < 1)
|
||||
return [];
|
||||
return resultData.Select(data =>
|
||||
new MetadataSearchResult(data.MalId.ToString(), data.Titles.First().Title, data.Url, data.Synopsis))
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the Manga linked in the MetadataEntry
|
||||
/// </summary>
|
||||
/// <param name="metadataEntry"></param>
|
||||
/// <param name="dbContext"></param>
|
||||
/// <exception cref="FormatException"></exception>
|
||||
/// <exception cref="DbUpdateException"></exception>
|
||||
public override void UpdateMetadata(MetadataEntry metadataEntry, MangaContext dbContext)
|
||||
{
|
||||
Manga dbManga = dbContext.Mangas.Find(metadataEntry.MangaId)!;
|
||||
MangaFull resultData;
|
||||
try
|
||||
{
|
||||
long id = long.Parse(metadataEntry.Identifier);
|
||||
resultData = Jikan.GetMangaFullDataAsync(id).Result.Data;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
throw new FormatException("ID was not in correct format");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
dbManga.Name = resultData.Titles.First().Title;
|
||||
dbManga.Description = resultData.Synopsis;
|
||||
dbManga.AltTitles.Clear();
|
||||
dbManga.AltTitles = resultData.Titles.Select(t => new AltTitle(t.Type, t.Title)).ToList();
|
||||
dbManga.Authors.Clear();
|
||||
dbManga.Authors = resultData.Authors.Select(a => new Author(a.Name)).ToList();
|
||||
|
||||
dbContext.Sync();
|
||||
}
|
||||
catch (DbUpdateException e)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -1,10 +0,0 @@
|
||||
namespace API.Schema;
|
||||
|
||||
public enum MangaReleaseStatus : byte
|
||||
{
|
||||
Continuing = 0,
|
||||
Completed = 1,
|
||||
OnHiatus = 2,
|
||||
Cancelled = 3,
|
||||
Unreleased = 4
|
||||
}
|
@ -1,8 +0,0 @@
|
||||
namespace API.Schema;
|
||||
|
||||
public enum NotificationUrgency : byte
|
||||
{
|
||||
Low = 1,
|
||||
Normal = 3,
|
||||
High = 5
|
||||
}
|
@ -1,15 +1,11 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace API.Schema;
|
||||
namespace API.Schema.NotificationsContext;
|
||||
|
||||
[PrimaryKey("NotificationId")]
|
||||
public class Notification
|
||||
[PrimaryKey(nameof(Key))]
|
||||
public class Notification : Identifiable
|
||||
{
|
||||
[StringLength(64)]
|
||||
[Required]
|
||||
public string NotificationId { get; init; }
|
||||
|
||||
[Required]
|
||||
public NotificationUrgency Urgency { get; init; }
|
||||
|
||||
@ -24,29 +20,37 @@ public class Notification
|
||||
[Required]
|
||||
public DateTime Date { get; init; }
|
||||
|
||||
public bool IsSent { get; internal set; }
|
||||
|
||||
public Notification(string title, string message = "", NotificationUrgency urgency = NotificationUrgency.Normal, DateTime? date = null)
|
||||
: base(TokenGen.CreateToken("Notification"))
|
||||
{
|
||||
this.NotificationId = TokenGen.CreateToken("Notification");
|
||||
this.Title = title;
|
||||
this.Message = message;
|
||||
this.Urgency = urgency;
|
||||
this.Date = date ?? DateTime.UtcNow;
|
||||
this.IsSent = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EF ONLY!!!
|
||||
/// </summary>
|
||||
public Notification(string notificationId, string title, string message, NotificationUrgency urgency, DateTime date)
|
||||
public Notification(string key, string title, string message, NotificationUrgency urgency, DateTime date, bool isSent)
|
||||
: base(key)
|
||||
{
|
||||
this.NotificationId = notificationId;
|
||||
this.Title = title;
|
||||
this.Message = message;
|
||||
this.Urgency = urgency;
|
||||
this.Date = date;
|
||||
this.IsSent = isSent;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{NotificationId} {Urgency} {Title}";
|
||||
}
|
||||
public override string ToString() => $"{base.ToString()} {Urgency} {Title} {Message}";
|
||||
}
|
||||
|
||||
public enum NotificationUrgency : byte
|
||||
{
|
||||
Low = 1,
|
||||
Normal = 3,
|
||||
High = 5
|
||||
}
|
@ -5,7 +5,7 @@ using log4net;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace API.Schema.NotificationConnectors;
|
||||
namespace API.Schema.NotificationsContext.NotificationConnectors;
|
||||
|
||||
[PrimaryKey("Name")]
|
||||
public class NotificationConnector(string name, string url, Dictionary<string, string> headers, string httpMethod, string body)
|
||||
@ -34,7 +34,7 @@ public class NotificationConnector(string name, string url, Dictionary<string, s
|
||||
[NotMapped]
|
||||
private readonly HttpClient Client = new()
|
||||
{
|
||||
DefaultRequestHeaders = { { "User-Agent", TrangaSettings.userAgent } }
|
||||
DefaultRequestHeaders = { { "User-Agent", Tranga.Settings.UserAgent } }
|
||||
};
|
||||
|
||||
[JsonIgnore]
|
||||
@ -79,4 +79,6 @@ public class NotificationConnector(string name, string url, Dictionary<string, s
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
public override string ToString() => $"{GetType().Name} {Name}";
|
||||
}
|
@ -1,9 +1,9 @@
|
||||
using API.Schema.NotificationConnectors;
|
||||
using API.Schema.NotificationsContext.NotificationConnectors;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace API.Schema.Contexts;
|
||||
namespace API.Schema.NotificationsContext;
|
||||
|
||||
public class NotificationsContext(DbContextOptions<NotificationsContext> options) : DbContext(options)
|
||||
public class NotificationsContext(DbContextOptions<NotificationsContext> options) : TrangaBaseContext<NotificationsContext>(options)
|
||||
{
|
||||
public DbSet<NotificationConnector> NotificationConnectors { get; set; }
|
||||
public DbSet<Notification> Notifications { get; set; }
|
40
API/Schema/TrangaBaseContext.cs
Normal file
40
API/Schema/TrangaBaseContext.cs
Normal file
@ -0,0 +1,40 @@
|
||||
using log4net;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Diagnostics;
|
||||
|
||||
namespace API.Schema;
|
||||
|
||||
public abstract class TrangaBaseContext<T> : DbContext where T : DbContext
|
||||
{
|
||||
private ILog Log { get; init; }
|
||||
|
||||
protected TrangaBaseContext(DbContextOptions<T> options) : base(options)
|
||||
{
|
||||
this.Log = LogManager.GetLogger(GetType());
|
||||
}
|
||||
|
||||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
||||
{
|
||||
base.OnConfiguring(optionsBuilder);
|
||||
optionsBuilder.LogTo(s =>
|
||||
{
|
||||
Log.Debug(s);
|
||||
}, Array.Empty<string>(), LogLevel.Warning, DbContextLoggerOptions.Level | DbContextLoggerOptions.Category | DbContextLoggerOptions.UtcTime);
|
||||
}
|
||||
|
||||
internal (bool success, string? exceptionMessage) Sync()
|
||||
{
|
||||
try
|
||||
{
|
||||
this.SaveChanges();
|
||||
return (true, null);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(null, e);
|
||||
return (false, e.Message);
|
||||
}
|
||||
}
|
||||
|
||||
public override string ToString() => $"{GetType().Name} {typeof(T).Name}";
|
||||
}
|
@ -5,7 +5,7 @@ namespace API;
|
||||
|
||||
public static class TokenGen
|
||||
{
|
||||
private const int MinimumLength = 32;
|
||||
private const int MinimumLength = 16;
|
||||
private const int MaximumLength = 64;
|
||||
private const string Chars = "abcdefghijklmnopqrstuvwxyz0123456789";
|
||||
|
||||
|
310
API/Tranga.cs
310
API/Tranga.cs
@ -1,218 +1,202 @@
|
||||
using API.Schema;
|
||||
using API.Schema.Contexts;
|
||||
using API.Schema.Jobs;
|
||||
using API.Schema.MangaConnectors;
|
||||
using API.Schema.NotificationConnectors;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using API.Schema.LibraryContext;
|
||||
using API.Schema.MangaContext;
|
||||
using API.Schema.MangaContext.MetadataFetchers;
|
||||
using API.Schema.NotificationsContext;
|
||||
using API.Workers;
|
||||
using API.Workers.MaintenanceWorkers;
|
||||
using log4net;
|
||||
using log4net.Config;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.ChangeTracking;
|
||||
|
||||
namespace API;
|
||||
|
||||
public static class Tranga
|
||||
{
|
||||
public static Thread NotificationSenderThread { get; } = new (NotificationSender);
|
||||
public static Thread JobStarterThread { get; } = new (JobStarter);
|
||||
|
||||
// ReSharper disable once InconsistentNaming
|
||||
private const string TRANGA =
|
||||
"\n\n" +
|
||||
" _______ v2\n" +
|
||||
"|_ _|.----..---.-..-----..-----..---.-.\n" +
|
||||
" | | | _|| _ || || _ || _ |\n" +
|
||||
" |___| |__| |___._||__|__||___ ||___._|\n" +
|
||||
" |_____| \n\n";
|
||||
|
||||
public static Thread PeriodicWorkerStarterThread { get; } = new (WorkerStarter);
|
||||
private static readonly ILog Log = LogManager.GetLogger(typeof(Tranga));
|
||||
internal static readonly MetadataFetcher[] MetadataFetchers = [new MyAnimeList()];
|
||||
internal static TrangaSettings Settings = TrangaSettings.Load();
|
||||
|
||||
internal static readonly UpdateMetadataWorker UpdateMetadataWorker = new ();
|
||||
internal static readonly SendNotificationsWorker SendNotificationsWorker = new();
|
||||
internal static readonly UpdateChaptersDownloadedWorker UpdateChaptersDownloadedWorker = new();
|
||||
internal static readonly CheckForNewChaptersWorker CheckForNewChaptersWorker = new();
|
||||
internal static readonly CleanupMangaCoversWorker CleanupMangaCoversWorker = new();
|
||||
internal static readonly StartNewChapterDownloadsWorker StartNewChapterDownloadsWorker = new();
|
||||
internal static readonly RemoveOldNotificationsWorker RemoveOldNotificationsWorker = new();
|
||||
|
||||
internal static void StartLogger()
|
||||
{
|
||||
BasicConfigurator.Configure();
|
||||
Log.Info("Logger Configured.");
|
||||
Log.Info(TRANGA);
|
||||
|
||||
AddWorker(UpdateMetadataWorker);
|
||||
AddWorker(SendNotificationsWorker);
|
||||
AddWorker(UpdateChaptersDownloadedWorker);
|
||||
AddWorker(CheckForNewChaptersWorker);
|
||||
AddWorker(CleanupMangaCoversWorker);
|
||||
AddWorker(StartNewChapterDownloadsWorker);
|
||||
AddWorker(RemoveOldNotificationsWorker);
|
||||
}
|
||||
|
||||
private static void NotificationSender(object? serviceProviderObj)
|
||||
internal static HashSet<BaseWorker> AllWorkers { get; private set; } = new ();
|
||||
public static void AddWorker(BaseWorker worker) => AllWorkers.Add(worker);
|
||||
public static void AddWorkers(IEnumerable<BaseWorker> workers)
|
||||
{
|
||||
if (serviceProviderObj is null)
|
||||
foreach (BaseWorker baseWorker in workers)
|
||||
{
|
||||
Log.Error("serviceProviderObj is null");
|
||||
return;
|
||||
}
|
||||
IServiceProvider serviceProvider = (IServiceProvider)serviceProviderObj!;
|
||||
using IServiceScope scope = serviceProvider.CreateScope();
|
||||
NotificationsContext context = scope.ServiceProvider.GetRequiredService<NotificationsContext>();
|
||||
|
||||
try
|
||||
{
|
||||
//Removing Notifications from previous runs
|
||||
IQueryable<Notification> staleNotifications =
|
||||
context.Notifications.Where(n => n.Urgency < NotificationUrgency.Normal);
|
||||
context.Notifications.RemoveRange(staleNotifications);
|
||||
context.SaveChanges();
|
||||
}
|
||||
catch (DbUpdateException e)
|
||||
{
|
||||
Log.Error("Error removing stale notifications.", e);
|
||||
}
|
||||
|
||||
while (true)
|
||||
{
|
||||
SendNotifications(serviceProvider, NotificationUrgency.High);
|
||||
SendNotifications(serviceProvider, NotificationUrgency.Normal);
|
||||
SendNotifications(serviceProvider, NotificationUrgency.Low);
|
||||
|
||||
Thread.Sleep(2000);
|
||||
AddWorker(baseWorker);
|
||||
}
|
||||
}
|
||||
|
||||
private static void SendNotifications(IServiceProvider serviceProvider, NotificationUrgency urgency)
|
||||
public static void RemoveWorker(BaseWorker removeWorker)
|
||||
{
|
||||
Log.Info($"Sending notifications for {urgency}");
|
||||
using IServiceScope scope = serviceProvider.CreateScope();
|
||||
NotificationsContext context = scope.ServiceProvider.GetRequiredService<NotificationsContext>();
|
||||
IEnumerable<BaseWorker> baseWorkers = AllWorkers.Where(w => w.DependenciesAndSelf.Any(worker => worker == removeWorker));
|
||||
|
||||
List<Notification> notifications = context.Notifications.Where(n => n.Urgency == urgency).ToList();
|
||||
if (!notifications.Any())
|
||||
return;
|
||||
|
||||
try
|
||||
foreach (BaseWorker worker in baseWorkers)
|
||||
{
|
||||
foreach (NotificationConnector notificationConnector in context.NotificationConnectors)
|
||||
{
|
||||
foreach (Notification notification in notifications)
|
||||
notificationConnector.SendNotification(notification.Title, notification.Message);
|
||||
}
|
||||
|
||||
context.Notifications.RemoveRange(notifications);
|
||||
context.SaveChangesAsync();
|
||||
}
|
||||
catch (DbUpdateException e)
|
||||
{
|
||||
Log.Error("Error sending notifications.", e);
|
||||
StopWorker(worker);
|
||||
AllWorkers.Remove(worker);
|
||||
}
|
||||
}
|
||||
|
||||
private const string TRANGA =
|
||||
"\n\n" +
|
||||
" _______ \n" +
|
||||
"|_ _|.----..---.-..-----..-----..---.-.\n" +
|
||||
" | | | _|| _ || || _ || _ |\n" +
|
||||
" |___| |__| |___._||__|__||___ ||___._|\n" +
|
||||
" |_____| \n\n";
|
||||
private static readonly Dictionary<Thread, Job> RunningJobs = new();
|
||||
private static void JobStarter(object? serviceProviderObj)
|
||||
private static readonly Dictionary<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)
|
||||
{
|
||||
Log.Info("WorkerStarter Thread running.");
|
||||
if (serviceProviderObj is null)
|
||||
{
|
||||
Log.Error("serviceProviderObj is null");
|
||||
return;
|
||||
}
|
||||
IServiceProvider serviceProvider = (IServiceProvider)serviceProviderObj;
|
||||
using IServiceScope scope = serviceProvider.CreateScope();
|
||||
PgsqlContext? context = scope.ServiceProvider.GetService<PgsqlContext>();
|
||||
if (context is null)
|
||||
{
|
||||
Log.Error("PgsqlContext is null");
|
||||
return;
|
||||
}
|
||||
|
||||
Log.Info(TRANGA);
|
||||
Log.Info("Loading Jobs");
|
||||
context.Jobs.Load();
|
||||
Log.Info("JobStarter Thread running.");
|
||||
while (true)
|
||||
{
|
||||
foreach (EntityEntry entityEntry in context.ChangeTracker.Entries().ToArray())
|
||||
entityEntry.Reload();
|
||||
//Update finished Jobs to new states
|
||||
List<Job> completedJobs = context.Jobs.Where(j => j.state == JobState.Completed).ToList();
|
||||
foreach (Job completedJob in completedJobs)
|
||||
if (completedJob.RecurrenceMs <= 0)
|
||||
context.Jobs.Remove(completedJob);
|
||||
else
|
||||
{
|
||||
completedJob.state = JobState.CompletedWaiting;
|
||||
completedJob.LastExecution = DateTime.UtcNow;
|
||||
}
|
||||
List<Job> failedJobs = context.Jobs.Where(j => j.state == JobState.Failed).ToList();
|
||||
foreach (Job failedJob in failedJobs)
|
||||
{
|
||||
failedJob.Enabled = false;
|
||||
failedJob.LastExecution = DateTime.UtcNow;
|
||||
}
|
||||
CheckRunningWorkers();
|
||||
|
||||
//Retrieve waiting and due Jobs
|
||||
List<Job> waitingJobs = context.Jobs.Where(j =>
|
||||
j.Enabled && (j.state == JobState.FirstExecution || j.state == JobState.CompletedWaiting)).ToList();
|
||||
List<Job> runningJobs = context.Jobs.Where(j => j.state == JobState.Running).ToList();
|
||||
List<Job> dueJobs = waitingJobs.Where(j => j.NextExecution < DateTime.UtcNow).ToList();
|
||||
foreach (BaseWorker baseWorker in AllWorkers.DueWorkers())
|
||||
StartWorkers.Add(baseWorker);
|
||||
|
||||
List<MangaConnector> busyConnectors = GetBusyConnectors(runningJobs);
|
||||
List<Job> startJobs = FilterJobPreconditions(context, dueJobs, busyConnectors);
|
||||
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());
|
||||
|
||||
//Start Jobs that are allowed to run (preconditions match)
|
||||
foreach (Job job in startJobs)
|
||||
{
|
||||
Thread t = new(() =>
|
||||
{
|
||||
job.Run(serviceProvider);
|
||||
});
|
||||
RunningJobs.Add(t, job);
|
||||
t.Start();
|
||||
StartWorkers.Remove(worker);
|
||||
}
|
||||
Log.Debug($"Jobs Completed: {completedJobs.Count} Failed: {failedJobs.Count} Running: {runningJobs.Count}\n" +
|
||||
$"Waiting: {waitingJobs.Count}\n" +
|
||||
$"\tof which Due: {dueJobs.Count}\n" +
|
||||
$"\t\tof which Started: {startJobs.Count}");
|
||||
|
||||
(Thread, Job)[] removeFromThreadsList = RunningJobs.Where(t => !t.Key.IsAlive)
|
||||
.Select(t => (t.Key, t.Value)).ToArray();
|
||||
Log.Debug($"Remove from Threads List: {removeFromThreadsList.Length}");
|
||||
foreach ((Thread thread, Job job) thread in removeFromThreadsList)
|
||||
{
|
||||
RunningJobs.Remove(thread.thread);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
context.SaveChanges();
|
||||
}
|
||||
catch (DbUpdateException e)
|
||||
{
|
||||
Log.Error("Failed saving Job changes.", e);
|
||||
}
|
||||
Thread.Sleep(TrangaSettings.startNewJobTimeoutMs);
|
||||
Thread.Sleep(Settings.WorkCycleTimeoutMs);
|
||||
}
|
||||
}
|
||||
|
||||
private static List<MangaConnector> GetBusyConnectors(List<Job> runningJobs)
|
||||
private static void CheckRunningWorkers()
|
||||
{
|
||||
HashSet<MangaConnector> busyConnectors = new();
|
||||
foreach (Job runningJob in runningJobs)
|
||||
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)
|
||||
{
|
||||
if(GetJobConnector(runningJob) is { } mangaConnector)
|
||||
busyConnectors.Add(mangaConnector);
|
||||
RunningWorkers.Remove(worker);
|
||||
foreach (BaseWorker newWorker in task.Result)
|
||||
AllWorkers.Add(newWorker);
|
||||
task.Dispose();
|
||||
}
|
||||
return busyConnectors.ToList();
|
||||
}
|
||||
|
||||
private static List<Job> FilterJobPreconditions(PgsqlContext context, List<Job> dueJobs, List<MangaConnector> busyConnectors) =>
|
||||
dueJobs
|
||||
.Where(j =>
|
||||
private static IEnumerable<BaseWorker> DueWorkers(this IEnumerable<BaseWorker> workers)
|
||||
{
|
||||
context.Entry(j).Collection(j => j.DependsOnJobs).Load(LoadOptions.ForceIdentityResolution);
|
||||
return j.DependenciesFulfilled;
|
||||
})
|
||||
.Where(j =>
|
||||
return workers.Where(w =>
|
||||
{
|
||||
//Filter jobs with busy connectors
|
||||
if (GetJobConnector(j) is { } mangaConnector)
|
||||
return busyConnectors.Contains(mangaConnector) == false;
|
||||
if (w.State is >= WorkerExecutionState.Running and < WorkerExecutionState.Completed)
|
||||
return false;
|
||||
if (w is IPeriodic periodicWorker)
|
||||
return periodicWorker.IsDue;
|
||||
return true;
|
||||
})
|
||||
.ToList();
|
||||
});
|
||||
}
|
||||
|
||||
private static MangaConnector? GetJobConnector(Job job)
|
||||
internal static void MarkWorkerForStart(BaseWorker worker) => StartWorkers.Add(worker);
|
||||
|
||||
internal static void StopWorker(BaseWorker worker)
|
||||
{
|
||||
if (job is DownloadAvailableChaptersJob dacj)
|
||||
return dacj.Manga.MangaConnector;
|
||||
if (job is DownloadMangaCoverJob dmcj)
|
||||
return dmcj.Manga.MangaConnector;
|
||||
if (job is DownloadSingleChapterJob dscj)
|
||||
return dscj.Chapter.ParentManga.MangaConnector;
|
||||
if (job is RetrieveChaptersJob rcj)
|
||||
return rcj.Manga.MangaConnector;
|
||||
return null;
|
||||
StartWorkers.Remove(worker);
|
||||
worker.Cancel();
|
||||
RunningWorkers.Remove(worker);
|
||||
}
|
||||
|
||||
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 =>
|
||||
{
|
||||
MangaTag? inDb = context.Tags.Find(mt.Tag);
|
||||
return inDb ?? mt;
|
||||
});
|
||||
manga.MangaTags = mergedTags.ToList();
|
||||
|
||||
IEnumerable<Author> mergedAuthors = manga.Authors.Select(ma =>
|
||||
{
|
||||
Author? inDb = context.Authors.Find(ma.Key);
|
||||
return inDb ?? ma;
|
||||
});
|
||||
manga.Authors = mergedAuthors.ToList();
|
||||
|
||||
if(context.MangaConnectorToManga.Find(addMcId.Key) is null)
|
||||
context.MangaConnectorToManga.Add(mcId);
|
||||
|
||||
if (context.Sync() is { success: false })
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
internal static bool AddChapterToContext((Chapter, MangaConnectorId<Chapter>) addChapter, MangaContext context,
|
||||
[NotNullWhen(true)] out Chapter? chapter) => AddChapterToContext(addChapter.Item1, addChapter.Item2, context, out chapter);
|
||||
|
||||
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);
|
||||
|
||||
if (context.Sync() is { success: false })
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
}
|
@ -1,178 +1,112 @@
|
||||
using System.Runtime.InteropServices;
|
||||
using API.MangaDownloadClients;
|
||||
using API.Schema;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace API;
|
||||
|
||||
public static class TrangaSettings
|
||||
public struct TrangaSettings()
|
||||
{
|
||||
public static string downloadLocation { get; private set; } = (RuntimeInformation.IsOSPlatform(OSPlatform.Linux) ? "/Manga" : Path.Join(Directory.GetCurrentDirectory(), "Downloads"));
|
||||
public static string workingDirectory { get; private set; } = Path.Join(RuntimeInformation.IsOSPlatform(OSPlatform.Linux) ? "/usr/share" : Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "tranga-api");
|
||||
|
||||
[JsonIgnore]
|
||||
internal static readonly string DefaultUserAgent = $"Tranga ({Enum.GetName(Environment.OSVersion.Platform)}; {(Environment.Is64BitOperatingSystem ? "x64" : "")}) / 1.0";
|
||||
public static string userAgent { get; private set; } = DefaultUserAgent;
|
||||
public static int compression{ get; private set; } = 40;
|
||||
public static bool bwImages { get; private set; } = false;
|
||||
public static string workingDirectory => Path.Join(RuntimeInformation.IsOSPlatform(OSPlatform.Linux) ? "/usr/share" : Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "tranga-api");
|
||||
[JsonIgnore]
|
||||
public static string settingsFilePath => Path.Join(workingDirectory, "settings.json");
|
||||
[JsonIgnore]
|
||||
public static string coverImageCache => Path.Join(workingDirectory, "imageCache");
|
||||
public string DownloadLocation => RuntimeInformation.IsOSPlatform(OSPlatform.Linux) ? "/Manga" : Path.Join(Directory.GetCurrentDirectory(), "Manga");
|
||||
[JsonIgnore]
|
||||
internal static readonly string DefaultUserAgent = $"Tranga/2.0 ({Enum.GetName(Environment.OSVersion.Platform)}; {(Environment.Is64BitOperatingSystem ? "x64" : "")})";
|
||||
public string UserAgent { get; set; } = DefaultUserAgent;
|
||||
public int ImageCompression{ get; set; } = 40;
|
||||
public bool BlackWhiteImages { get; set; } = false;
|
||||
public string FlareSolverrUrl { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// Placeholders:
|
||||
/// %M Manga Name
|
||||
/// %M Obj Name
|
||||
/// %V Volume
|
||||
/// %C Chapter
|
||||
/// %T Title
|
||||
/// %A Author (first in list)
|
||||
/// %I Chapter Internal ID
|
||||
/// %i Manga Internal ID
|
||||
/// %Y Year (Manga)
|
||||
/// %i Obj Internal ID
|
||||
/// %Y Year (Obj)
|
||||
///
|
||||
/// ?_(...) replace _ with a value from above:
|
||||
/// Everything inside the braces will only be added if the value of %_ is not null
|
||||
/// </summary>
|
||||
public static string chapterNamingScheme { get; private set; } = "%M - ?V(Vol.%V )Ch.%C?T( - %T)";
|
||||
[JsonIgnore]
|
||||
public static string settingsFilePath => Path.Join(workingDirectory, "settings.json");
|
||||
[JsonIgnore]
|
||||
public static string coverImageCache => Path.Join(workingDirectory, "imageCache");
|
||||
public static bool aprilFoolsMode { get; private set; } = true;
|
||||
public static int startNewJobTimeoutMs { get; private set; } = 1000;
|
||||
public string ChapterNamingScheme { get; set; } = "%M - ?V(Vol.%V )Ch.%C?T( - %T)";
|
||||
public int WorkCycleTimeoutMs { get; set; } = 20000;
|
||||
[JsonIgnore]
|
||||
internal static readonly Dictionary<RequestType, int> DefaultRequestLimits = new ()
|
||||
{
|
||||
{RequestType.MangaInfo, 60},
|
||||
{RequestType.MangaDexFeed, 60},
|
||||
{RequestType.MangaDexImage, 40},
|
||||
{RequestType.MangaImage, 60},
|
||||
{RequestType.MangaDexImage, 60},
|
||||
{RequestType.MangaImage, 240},
|
||||
{RequestType.MangaCover, 60},
|
||||
{RequestType.Default, 60}
|
||||
};
|
||||
public static Dictionary<RequestType, int> requestLimits { get; private set; } = DefaultRequestLimits;
|
||||
public Dictionary<RequestType, int> RequestLimits { get; set; } = DefaultRequestLimits;
|
||||
|
||||
public static TimeSpan NotificationUrgencyDelay(NotificationUrgency urgency) => urgency switch
|
||||
public string DownloadLanguage { get; set; } = "en";
|
||||
|
||||
public static TrangaSettings Load()
|
||||
{
|
||||
NotificationUrgency.High => TimeSpan.Zero,
|
||||
NotificationUrgency.Normal => TimeSpan.FromMinutes(5),
|
||||
NotificationUrgency.Low => TimeSpan.FromMinutes(10),
|
||||
_ => TimeSpan.FromHours(1)
|
||||
}; //TODO make this a setting?
|
||||
|
||||
public static void Load()
|
||||
{
|
||||
if(File.Exists(settingsFilePath))
|
||||
Deserialize(File.ReadAllText(settingsFilePath));
|
||||
else return;
|
||||
|
||||
Directory.CreateDirectory(downloadLocation);
|
||||
ExportSettings();
|
||||
if (!File.Exists(settingsFilePath))
|
||||
new TrangaSettings().Save();
|
||||
return JsonConvert.DeserializeObject<TrangaSettings>(File.ReadAllText(settingsFilePath));
|
||||
}
|
||||
|
||||
public static void UpdateAprilFoolsMode(bool enabled)
|
||||
public void Save()
|
||||
{
|
||||
aprilFoolsMode = enabled;
|
||||
ExportSettings();
|
||||
File.WriteAllText(settingsFilePath, JsonConvert.SerializeObject(this));
|
||||
}
|
||||
|
||||
public static void UpdateCompressImages(int value)
|
||||
public void SetUserAgent(string value)
|
||||
{
|
||||
compression = int.Clamp(value, 1, 100);
|
||||
ExportSettings();
|
||||
this.UserAgent = value;
|
||||
Save();
|
||||
}
|
||||
|
||||
public static void UpdateBwImages(bool enabled)
|
||||
public void SetRequestLimit(RequestType type, int value)
|
||||
{
|
||||
bwImages = enabled;
|
||||
ExportSettings();
|
||||
this.RequestLimits[type] = value;
|
||||
Save();
|
||||
}
|
||||
|
||||
public static void UpdateUserAgent(string? customUserAgent)
|
||||
public void ResetRequestLimits()
|
||||
{
|
||||
userAgent = customUserAgent ?? DefaultUserAgent;
|
||||
ExportSettings();
|
||||
this.RequestLimits = DefaultRequestLimits;
|
||||
Save();
|
||||
}
|
||||
|
||||
public static void UpdateRequestLimit(RequestType requestType, int newLimit)
|
||||
public void UpdateImageCompression(int value)
|
||||
{
|
||||
requestLimits[requestType] = newLimit;
|
||||
ExportSettings();
|
||||
this.ImageCompression = value;
|
||||
Save();
|
||||
}
|
||||
|
||||
public static void UpdateChapterNamingScheme(string namingScheme)
|
||||
public void SetBlackWhiteImageEnabled(bool enabled)
|
||||
{
|
||||
chapterNamingScheme = namingScheme;
|
||||
ExportSettings();
|
||||
this.BlackWhiteImages = enabled;
|
||||
Save();
|
||||
}
|
||||
|
||||
public static void ResetRequestLimits()
|
||||
public void SetChapterNamingScheme(string scheme)
|
||||
{
|
||||
requestLimits = DefaultRequestLimits;
|
||||
ExportSettings();
|
||||
this.ChapterNamingScheme = scheme;
|
||||
Save();
|
||||
}
|
||||
|
||||
public static void ExportSettings()
|
||||
public void SetFlareSolverrUrl(string url)
|
||||
{
|
||||
if (File.Exists(settingsFilePath))
|
||||
{
|
||||
while(IsFileInUse(settingsFilePath))
|
||||
Thread.Sleep(100);
|
||||
}
|
||||
else
|
||||
Directory.CreateDirectory(new FileInfo(settingsFilePath).DirectoryName!);
|
||||
File.WriteAllText(settingsFilePath, Serialize());
|
||||
this.FlareSolverrUrl = url;
|
||||
Save();
|
||||
}
|
||||
|
||||
internal static bool IsFileInUse(string filePath)
|
||||
public void SetDownloadLanguage(string language)
|
||||
{
|
||||
if (!File.Exists(filePath))
|
||||
return false;
|
||||
try
|
||||
{
|
||||
using FileStream stream = new (filePath, FileMode.Open, FileAccess.Read, FileShare.None);
|
||||
stream.Close();
|
||||
return false;
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public static JObject AsJObject()
|
||||
{
|
||||
JObject jobj = new ();
|
||||
jobj.Add("downloadLocation", JToken.FromObject(downloadLocation));
|
||||
jobj.Add("workingDirectory", JToken.FromObject(workingDirectory));
|
||||
jobj.Add("userAgent", JToken.FromObject(userAgent));
|
||||
jobj.Add("aprilFoolsMode", JToken.FromObject(aprilFoolsMode));
|
||||
jobj.Add("requestLimits", JToken.FromObject(requestLimits));
|
||||
jobj.Add("compression", JToken.FromObject(compression));
|
||||
jobj.Add("bwImages", JToken.FromObject(bwImages));
|
||||
jobj.Add("startNewJobTimeoutMs", JToken.FromObject(startNewJobTimeoutMs));
|
||||
jobj.Add("chapterNamingScheme", JToken.FromObject(chapterNamingScheme));
|
||||
return jobj;
|
||||
}
|
||||
|
||||
public static string Serialize() => AsJObject().ToString();
|
||||
|
||||
public static void Deserialize(string serialized)
|
||||
{
|
||||
JObject jobj = JObject.Parse(serialized);
|
||||
if (jobj.TryGetValue("downloadLocation", out JToken? dl))
|
||||
downloadLocation = dl.Value<string>()!;
|
||||
if (jobj.TryGetValue("workingDirectory", out JToken? wd))
|
||||
workingDirectory = wd.Value<string>()!;
|
||||
if (jobj.TryGetValue("userAgent", out JToken? ua))
|
||||
userAgent = ua.Value<string>()!;
|
||||
if (jobj.TryGetValue("aprilFoolsMode", out JToken? afm))
|
||||
aprilFoolsMode = afm.Value<bool>()!;
|
||||
if (jobj.TryGetValue("requestLimits", out JToken? rl))
|
||||
requestLimits = rl.ToObject<Dictionary<RequestType, int>>()!;
|
||||
if (jobj.TryGetValue("compression", out JToken? ci))
|
||||
compression = ci.Value<int>()!;
|
||||
if (jobj.TryGetValue("bwImages", out JToken? bwi))
|
||||
bwImages = bwi.Value<bool>()!;
|
||||
if (jobj.TryGetValue("startNewJobTimeoutMs", out JToken? snjt))
|
||||
startNewJobTimeoutMs = snjt.Value<int>()!;
|
||||
if (jobj.TryGetValue("chapterNamingScheme", out JToken? cns))
|
||||
chapterNamingScheme = cns.Value<string>()!;
|
||||
this.DownloadLanguage = language;
|
||||
Save();
|
||||
}
|
||||
}
|
120
API/Workers/BaseWorker.cs
Normal file
120
API/Workers/BaseWorker.cs
Normal file
@ -0,0 +1,120 @@
|
||||
using API.Schema;
|
||||
using log4net;
|
||||
|
||||
namespace API.Workers;
|
||||
|
||||
public abstract class BaseWorker : Identifiable
|
||||
{
|
||||
/// <summary>
|
||||
/// Workers this Worker depends on being completed before running.
|
||||
/// </summary>
|
||||
public BaseWorker[] DependsOn { get; init; }
|
||||
/// <summary>
|
||||
/// Dependencies and dependencies of dependencies. See also <see cref="DependsOn"/>.
|
||||
/// </summary>
|
||||
public IEnumerable<BaseWorker> AllDependencies => DependsOn.Select(d => d.AllDependencies).SelectMany(x => x);
|
||||
/// <summary>
|
||||
/// <see cref="AllDependencies"/> and Self.
|
||||
/// </summary>
|
||||
public IEnumerable<BaseWorker> DependenciesAndSelf => AllDependencies.Append(this);
|
||||
/// <summary>
|
||||
/// <see cref="DependsOn"/> where <see cref="WorkerExecutionState"/> is less than Completed.
|
||||
/// </summary>
|
||||
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));
|
||||
protected ILog Log { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Stops worker, and marks as <see cref="WorkerExecutionState"/>.Cancelled
|
||||
/// </summary>
|
||||
public void Cancel()
|
||||
{
|
||||
Log.Debug($"Cancelled {this}");
|
||||
this.State = WorkerExecutionState.Cancelled;
|
||||
CancellationTokenSource.Cancel();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stops worker, and marks as <see cref="WorkerExecutionState"/>.Failed
|
||||
/// </summary>
|
||||
protected void Fail()
|
||||
{
|
||||
Log.Debug($"Failed {this}");
|
||||
this.State = WorkerExecutionState.Failed;
|
||||
CancellationTokenSource.Cancel();
|
||||
}
|
||||
|
||||
public BaseWorker(IEnumerable<BaseWorker>? dependsOn = null)
|
||||
{
|
||||
this.DependsOn = dependsOn?.ToArray() ?? [];
|
||||
this.Log = LogManager.GetLogger(GetType());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets States during worker-run.
|
||||
/// States:
|
||||
/// <list type="bullet">
|
||||
/// <item><see cref="WorkerExecutionState"/>.Waiting when waiting for <see cref="MissingDependencies"/></item>
|
||||
/// <item><see cref="WorkerExecutionState"/>.Running when running</item>
|
||||
/// <item><see cref="WorkerExecutionState"/>.Completed after finished</item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// <list type="bullet">
|
||||
/// <item>If <see cref="BaseWorker"/> has <see cref="MissingDependencies"/>, missing dependencies.</item>
|
||||
/// <item>If <see cref="MissingDependencies"/> are <see cref="WorkerExecutionState"/>.Running, itself after waiting for dependencies.</item>
|
||||
/// <item>If <see cref="BaseWorker"/> has run, additional <see cref="BaseWorker"/>.</item>
|
||||
/// </list>
|
||||
/// </returns>
|
||||
public Task<BaseWorker[]> DoWork()
|
||||
{
|
||||
Log.Debug($"Checking {this}");
|
||||
this.State = WorkerExecutionState.Waiting;
|
||||
|
||||
BaseWorker[] missingDependenciesThatNeedStarting = MissingDependencies.Where(d => d.State < WorkerExecutionState.Waiting).ToArray();
|
||||
if(missingDependenciesThatNeedStarting.Any())
|
||||
return new Task<BaseWorker[]>(() => missingDependenciesThatNeedStarting);
|
||||
|
||||
if (MissingDependencies.Any())
|
||||
return new Task<BaseWorker[]>(WaitForDependencies);
|
||||
|
||||
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();
|
||||
this.State = WorkerExecutionState.Running;
|
||||
return task;
|
||||
}
|
||||
|
||||
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())
|
||||
{
|
||||
Thread.Sleep(Tranga.Settings.WorkCycleTimeoutMs);
|
||||
}
|
||||
return [this];
|
||||
}
|
||||
}
|
||||
|
||||
public enum WorkerExecutionState
|
||||
{
|
||||
Failed = 0,
|
||||
Cancelled = 32,
|
||||
Created = 64,
|
||||
Waiting = 96,
|
||||
Running = 128,
|
||||
Completed = 192
|
||||
}
|
24
API/Workers/BaseWorkerWithContext.cs
Normal file
24
API/Workers/BaseWorkerWithContext.cs
Normal file
@ -0,0 +1,24 @@
|
||||
using System.Configuration;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace API.Workers;
|
||||
|
||||
public abstract class BaseWorkerWithContext<T>(IEnumerable<BaseWorker>? dependsOn = null) : BaseWorker(dependsOn) where T : DbContext
|
||||
{
|
||||
protected T DbContext = null!;
|
||||
private IServiceScope? _scope;
|
||||
|
||||
public void SetScope(IServiceScope scope)
|
||||
{
|
||||
this._scope = scope;
|
||||
this.DbContext = scope.ServiceProvider.GetRequiredService<T>();
|
||||
}
|
||||
|
||||
/// <exception cref="ConfigurationErrorsException">Scope has not been set. <see cref="SetScope"/></exception>
|
||||
public new Task<BaseWorker[]> DoWork()
|
||||
{
|
||||
if (DbContext is null)
|
||||
throw new ConfigurationErrorsException("Scope has not been set.");
|
||||
return base.DoWork();
|
||||
}
|
||||
}
|
9
API/Workers/IPeriodic.cs
Normal file
9
API/Workers/IPeriodic.cs
Normal file
@ -0,0 +1,9 @@
|
||||
namespace API.Workers;
|
||||
|
||||
public interface IPeriodic
|
||||
{
|
||||
internal DateTime LastExecution { get; set; }
|
||||
public TimeSpan Interval { get; set; }
|
||||
public DateTime NextExecution => LastExecution.Add(Interval);
|
||||
public bool IsDue => NextExecution <= DateTime.UtcNow;
|
||||
}
|
@ -1,52 +1,49 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.IO.Compression;
|
||||
using System.Runtime.InteropServices;
|
||||
using API.MangaDownloadClients;
|
||||
using API.Schema.Contexts;
|
||||
using Newtonsoft.Json;
|
||||
using API.Schema.MangaContext;
|
||||
using API.Schema.MangaContext.MangaConnectors;
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.Formats.Jpeg;
|
||||
using SixLabors.ImageSharp.Processing;
|
||||
using SixLabors.ImageSharp.Processing.Processors.Binarization;
|
||||
using static System.IO.UnixFileMode;
|
||||
|
||||
namespace API.Schema.Jobs;
|
||||
namespace API.Workers;
|
||||
|
||||
public class DownloadSingleChapterJob : Job
|
||||
public class DownloadChapterFromMangaconnectorWorker(MangaConnectorId<Chapter> chId, IEnumerable<BaseWorker>? dependsOn = null)
|
||||
: BaseWorkerWithContext<MangaContext>(dependsOn)
|
||||
{
|
||||
[StringLength(64)] [Required] public string ChapterId { get; init; }
|
||||
|
||||
[JsonIgnore] public Chapter Chapter { get; init; } = null!;
|
||||
|
||||
public DownloadSingleChapterJob(Chapter chapter, Job? parentJob = null, ICollection<Job>? dependsOnJobs = null)
|
||||
: base(TokenGen.CreateToken(typeof(DownloadSingleChapterJob)), JobType.DownloadSingleChapterJob, 0, parentJob, dependsOnJobs)
|
||||
internal readonly string MangaConnectorIdId = chId.Key;
|
||||
protected override BaseWorker[] DoWorkInternal()
|
||||
{
|
||||
this.ChapterId = chapter.ChapterId;
|
||||
this.Chapter = chapter;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EF ONLY!!!
|
||||
/// </summary>
|
||||
internal DownloadSingleChapterJob(string chapterId, string? parentJobId)
|
||||
: base(TokenGen.CreateToken(typeof(DownloadSingleChapterJob)), JobType.DownloadSingleChapterJob, 0, parentJobId)
|
||||
if (DbContext.MangaConnectorToChapter.Find(MangaConnectorIdId) is not { } MangaConnectorId)
|
||||
return []; //TODO Exception?
|
||||
MangaConnector mangaConnector = MangaConnectorId.MangaConnector;
|
||||
Chapter chapter = MangaConnectorId.Obj;
|
||||
if (chapter.Downloaded)
|
||||
{
|
||||
this.ChapterId = chapterId;
|
||||
}
|
||||
|
||||
protected override IEnumerable<Job> RunInternal(PgsqlContext context)
|
||||
{
|
||||
context.Attach(Chapter);
|
||||
string[] imageUrls = Chapter.ParentManga.MangaConnector.GetChapterImageUrls(Chapter);
|
||||
if (imageUrls.Length < 1)
|
||||
{
|
||||
Log.Info($"No imageUrls for chapter {ChapterId}");
|
||||
Log.Info("Chapter was already downloaded.");
|
||||
return [];
|
||||
}
|
||||
string saveArchiveFilePath = Chapter.FullArchiveFilePath;
|
||||
|
||||
string[] imageUrls = mangaConnector.GetChapterImageUrls(MangaConnectorId);
|
||||
if (imageUrls.Length < 1)
|
||||
{
|
||||
Log.Info($"No imageUrls for chapter {chapter}");
|
||||
return [];
|
||||
}
|
||||
string saveArchiveFilePath = chapter.FullArchiveFilePath;
|
||||
Log.Debug($"Chapter path: {saveArchiveFilePath}");
|
||||
|
||||
//Check if Publication Directory already exists
|
||||
string directoryPath = Path.GetDirectoryName(saveArchiveFilePath)!;
|
||||
string? directoryPath = Path.GetDirectoryName(saveArchiveFilePath);
|
||||
if (directoryPath is null)
|
||||
{
|
||||
Log.Error($"Directory path could not be found: {saveArchiveFilePath}");
|
||||
this.Fail();
|
||||
return [];
|
||||
}
|
||||
if (!Directory.Exists(directoryPath))
|
||||
{
|
||||
Log.Info($"Creating publication Directory: {directoryPath}");
|
||||
@ -67,7 +64,7 @@ public class DownloadSingleChapterJob : Job
|
||||
string tempFolder = Directory.CreateTempSubdirectory("trangatemp").FullName;
|
||||
Log.Debug($"Created temp folder: {tempFolder}");
|
||||
|
||||
Log.Info($"Downloading images: {ChapterId}");
|
||||
Log.Info($"Downloading images: {chapter}");
|
||||
int chapterNum = 0;
|
||||
//Download all Images to temporary Folder
|
||||
foreach (string imageUrl in imageUrls)
|
||||
@ -82,43 +79,61 @@ public class DownloadSingleChapterJob : Job
|
||||
}
|
||||
}
|
||||
|
||||
CopyCoverFromCacheToDownloadLocation(Chapter.ParentManga);
|
||||
CopyCoverFromCacheToDownloadLocation(chapter.ParentManga);
|
||||
|
||||
Log.Debug($"Creating ComicInfo.xml {ChapterId}");
|
||||
File.WriteAllText(Path.Join(tempFolder, "ComicInfo.xml"), Chapter.GetComicInfoXmlString());
|
||||
Log.Debug($"Creating ComicInfo.xml {chapter}");
|
||||
File.WriteAllText(Path.Join(tempFolder, "ComicInfo.xml"), chapter.GetComicInfoXmlString());
|
||||
|
||||
Log.Debug($"Packaging images to archive {ChapterId}");
|
||||
Log.Debug($"Packaging images to archive {chapter}");
|
||||
//ZIP-it and ship-it
|
||||
ZipFile.CreateFromDirectory(tempFolder, saveArchiveFilePath);
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
||||
File.SetUnixFileMode(saveArchiveFilePath, UserRead | UserWrite | UserExecute | GroupRead | GroupWrite | GroupExecute | OtherRead | OtherExecute);
|
||||
Directory.Delete(tempFolder, true); //Cleanup
|
||||
|
||||
Chapter.Downloaded = true;
|
||||
context.SaveChanges();
|
||||
chapter.Downloaded = true;
|
||||
DbContext.Sync();
|
||||
|
||||
return [new UpdateFilesDownloadedJob(Chapter.ParentManga, 0, this)];
|
||||
return [];
|
||||
}
|
||||
|
||||
private void ProcessImage(string imagePath)
|
||||
{
|
||||
if (!TrangaSettings.bwImages && TrangaSettings.compression == 100)
|
||||
if (!Tranga.Settings.BlackWhiteImages && Tranga.Settings.ImageCompression == 100)
|
||||
{
|
||||
Log.Debug($"No processing requested for image");
|
||||
Log.Debug("No processing requested for image");
|
||||
return;
|
||||
}
|
||||
|
||||
Log.Debug($"Processing image: {imagePath}");
|
||||
|
||||
try
|
||||
{
|
||||
using Image image = Image.Load(imagePath);
|
||||
File.Delete(imagePath);
|
||||
if(TrangaSettings.bwImages)
|
||||
if (Tranga.Settings.BlackWhiteImages)
|
||||
image.Mutate(i => i.ApplyProcessor(new AdaptiveThresholdProcessor()));
|
||||
File.Delete(imagePath);
|
||||
image.SaveAsJpeg(imagePath, new JpegEncoder()
|
||||
{
|
||||
Quality = TrangaSettings.compression
|
||||
Quality = Tranga.Settings.ImageCompression
|
||||
});
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
if (e is UnknownImageFormatException or NotSupportedException)
|
||||
{
|
||||
//If the Image-Format is not processable by ImageSharp, we can't modify it.
|
||||
Log.Debug($"Unable to process {imagePath}: Not supported image format");
|
||||
}else if (e is InvalidImageContentException)
|
||||
{
|
||||
Log.Debug($"Unable to process {imagePath}: Invalid Content");
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Error(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void CopyCoverFromCacheToDownloadLocation(Manga manga)
|
||||
{
|
||||
@ -131,8 +146,11 @@ public class DownloadSingleChapterJob : Job
|
||||
return;
|
||||
}
|
||||
|
||||
//TODO MangaConnector Selection
|
||||
MangaConnectorId<Manga> mcId = manga.MangaConnectorIds.First();
|
||||
|
||||
Log.Info($"Copying cover to {publicationFolder}");
|
||||
string? fileInCache = manga.CoverFileNameInCache ?? manga.MangaConnector.SaveCoverImageToCache(manga);
|
||||
string? fileInCache = manga.CoverFileNameInCache ?? mcId.MangaConnector.SaveCoverImageToCache(mcId);
|
||||
if (fileInCache is null)
|
||||
{
|
||||
Log.Error($"File {fileInCache} does not exist");
|
||||
@ -142,7 +160,7 @@ public class DownloadSingleChapterJob : Job
|
||||
string newFilePath = Path.Join(publicationFolder, $"cover.{Path.GetFileName(fileInCache).Split('.')[^1]}" );
|
||||
File.Copy(fileInCache, newFilePath, true);
|
||||
if(RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
||||
File.SetUnixFileMode(newFilePath, GroupRead | GroupWrite | UserRead | UserWrite);
|
||||
File.SetUnixFileMode(newFilePath, GroupRead | GroupWrite | UserRead | UserWrite | OtherRead | OtherWrite);
|
||||
Log.Debug($"Copied cover from {fileInCache} to {newFilePath}");
|
||||
}
|
||||
|
||||
@ -156,10 +174,12 @@ public class DownloadSingleChapterJob : Job
|
||||
if (requestResult.result == Stream.Null)
|
||||
return false;
|
||||
|
||||
FileStream fs = new (savePath, FileMode.Create, FileAccess.Write, FileShare.None);
|
||||
FileStream fs = new(savePath, FileMode.Create, FileAccess.Write, FileShare.None);
|
||||
requestResult.result.CopyTo(fs);
|
||||
fs.Close();
|
||||
ProcessImage(savePath);
|
||||
return true;
|
||||
}
|
||||
|
||||
public override string ToString() => $"{base.ToString()} {MangaConnectorIdId}";
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
using API.Schema.MangaContext;
|
||||
using API.Schema.MangaContext.MangaConnectors;
|
||||
|
||||
namespace API.Workers;
|
||||
|
||||
public class DownloadCoverFromMangaconnectorWorker(MangaConnectorId<Manga> mcId, IEnumerable<BaseWorker>? dependsOn = null)
|
||||
: BaseWorkerWithContext<MangaContext>(dependsOn)
|
||||
{
|
||||
internal readonly string MangaConnectorIdId = mcId.Key;
|
||||
protected override BaseWorker[] DoWorkInternal()
|
||||
{
|
||||
if (DbContext.MangaConnectorToManga.Find(MangaConnectorIdId) is not { } MangaConnectorId)
|
||||
return []; //TODO Exception?
|
||||
MangaConnector mangaConnector = MangaConnectorId.MangaConnector;
|
||||
Manga manga = MangaConnectorId.Obj;
|
||||
|
||||
manga.CoverFileNameInCache = mangaConnector.SaveCoverImageToCache(MangaConnectorId);
|
||||
|
||||
DbContext.Sync();
|
||||
return [];
|
||||
}
|
||||
|
||||
public override string ToString() => $"{base.ToString()} {MangaConnectorIdId}";
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
using API.Schema.MangaContext;
|
||||
using API.Schema.MangaContext.MangaConnectors;
|
||||
|
||||
namespace API.Workers;
|
||||
|
||||
public class RetrieveMangaChaptersFromMangaconnectorWorker(MangaConnectorId<Manga> mcId, string language, IEnumerable<BaseWorker>? dependsOn = null)
|
||||
: BaseWorkerWithContext<MangaContext>(dependsOn)
|
||||
{
|
||||
internal readonly string MangaConnectorIdId = mcId.Key;
|
||||
protected override BaseWorker[] DoWorkInternal()
|
||||
{
|
||||
if (DbContext.MangaConnectorToManga.Find(MangaConnectorIdId) is not { } MangaConnectorId)
|
||||
return []; //TODO Exception?
|
||||
MangaConnector mangaConnector = MangaConnectorId.MangaConnector;
|
||||
Manga manga = MangaConnectorId.Obj;
|
||||
// This gets all chapters that are not downloaded
|
||||
(Chapter, MangaConnectorId<Chapter>)[] allChapters =
|
||||
mangaConnector.GetChapters(MangaConnectorId, language).DistinctBy(c => c.Item1.Key).ToArray();
|
||||
|
||||
int addedChapters = 0;
|
||||
foreach ((Chapter chapter, MangaConnectorId<Chapter> mcId) newChapter in allChapters)
|
||||
{
|
||||
if (Tranga.AddChapterToContext(newChapter, DbContext, out Chapter? addedChapter) == false)
|
||||
continue;
|
||||
manga.Chapters.Add(addedChapter);
|
||||
}
|
||||
Log.Info($"{manga.Chapters.Count} existing + {addedChapters} new chapters.");
|
||||
|
||||
DbContext.Sync();
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
public override string ToString() => $"{base.ToString()} {MangaConnectorIdId}";
|
||||
}
|
49
API/Workers/MoveFileOrFolderWorker.cs
Normal file
49
API/Workers/MoveFileOrFolderWorker.cs
Normal file
@ -0,0 +1,49 @@
|
||||
namespace API.Workers;
|
||||
|
||||
public class MoveFileOrFolderWorker(string toLocation, string fromLocation, IEnumerable<BaseWorker>? dependsOn = null)
|
||||
: BaseWorker(dependsOn)
|
||||
{
|
||||
public readonly string FromLocation = fromLocation;
|
||||
public readonly string ToLocation = toLocation;
|
||||
|
||||
protected override BaseWorker[] DoWorkInternal()
|
||||
{
|
||||
try
|
||||
{
|
||||
FileInfo fi = new (FromLocation);
|
||||
if (!fi.Exists)
|
||||
{
|
||||
Log.Error($"File does not exist at {FromLocation}");
|
||||
return [];
|
||||
}
|
||||
|
||||
if (File.Exists(ToLocation))//Do not override existing
|
||||
{
|
||||
Log.Error($"File already exists at {ToLocation}");
|
||||
return [];
|
||||
}
|
||||
if(fi.Attributes.HasFlag(FileAttributes.Directory))
|
||||
MoveDirectory(fi, ToLocation);
|
||||
else
|
||||
MoveFile(fi, ToLocation);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e);
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
private void MoveDirectory(FileInfo from, string toLocation)
|
||||
{
|
||||
Directory.Move(from.FullName, toLocation);
|
||||
}
|
||||
|
||||
private void MoveFile(FileInfo from, string toLocation)
|
||||
{
|
||||
File.Move(from.FullName, toLocation);
|
||||
}
|
||||
|
||||
public override string ToString() => $"{base.ToString()} {FromLocation} {ToLocation}";
|
||||
}
|
26
API/Workers/MoveMangaLibraryWorker.cs
Normal file
26
API/Workers/MoveMangaLibraryWorker.cs
Normal file
@ -0,0 +1,26 @@
|
||||
using API.Schema.MangaContext;
|
||||
|
||||
namespace API.Workers;
|
||||
|
||||
public class MoveMangaLibraryWorker(Manga manga, FileLibrary toLibrary, IEnumerable<BaseWorker>? dependsOn = null)
|
||||
: BaseWorkerWithContext<MangaContext>(dependsOn)
|
||||
{
|
||||
internal readonly string MangaId = manga.Key;
|
||||
internal readonly string LibraryId = toLibrary.Key;
|
||||
protected override BaseWorker[] DoWorkInternal()
|
||||
{
|
||||
if (DbContext.Mangas.Find(MangaId) is not { } manga)
|
||||
return []; //TODO Exception?
|
||||
if (DbContext.FileLibraries.Find(LibraryId) is not { } toLibrary)
|
||||
return []; //TODO Exception?
|
||||
Dictionary<Chapter, string> oldPath = manga.Chapters.ToDictionary(c => c, c => c.FullArchiveFilePath);
|
||||
manga.Library = toLibrary;
|
||||
|
||||
if (DbContext.Sync() is { success: false })
|
||||
return [];
|
||||
|
||||
return manga.Chapters.Select(c => new MoveFileOrFolderWorker(c.FullArchiveFilePath, oldPath[c])).ToArray<BaseWorker>();
|
||||
}
|
||||
|
||||
public override string ToString() => $"{base.ToString()} {MangaId} {LibraryId}";
|
||||
}
|
22
API/Workers/PeriodicWorkers/CheckForNewChaptersWorker.cs
Normal file
22
API/Workers/PeriodicWorkers/CheckForNewChaptersWorker.cs
Normal file
@ -0,0 +1,22 @@
|
||||
using API.Schema.MangaContext;
|
||||
|
||||
namespace API.Workers;
|
||||
|
||||
public class CheckForNewChaptersWorker(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.FromMinutes(60);
|
||||
|
||||
protected override BaseWorker[] DoWorkInternal()
|
||||
{
|
||||
IQueryable<MangaConnectorId<Manga>> connectorIdsManga = DbContext.MangaConnectorToManga.Where(id => id.UseForDownload);
|
||||
|
||||
List<BaseWorker> newWorkers = new();
|
||||
foreach (MangaConnectorId<Manga> mangaConnectorId in connectorIdsManga)
|
||||
newWorkers.Add(new RetrieveMangaChaptersFromMangaconnectorWorker(mangaConnectorId, Tranga.Settings.DownloadLanguage));
|
||||
|
||||
return newWorkers.ToArray();
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
using API.Schema.MangaContext;
|
||||
|
||||
namespace API.Workers.MaintenanceWorkers;
|
||||
|
||||
public class CleanupMangaCoversWorker(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(24);
|
||||
|
||||
protected override BaseWorker[] DoWorkInternal()
|
||||
{
|
||||
Log.Info("Removing stale files...");
|
||||
if (!Directory.Exists(TrangaSettings.coverImageCache))
|
||||
return [];
|
||||
string[] usedFiles = DbContext.Mangas.Select(m => m.CoverFileNameInCache).Where(s => s != null).ToArray()!;
|
||||
string[] extraneousFiles = new DirectoryInfo(TrangaSettings.coverImageCache).GetFiles()
|
||||
.Where(f => usedFiles.Contains(f.FullName) == false)
|
||||
.Select(f => f.FullName)
|
||||
.ToArray();
|
||||
foreach (string path in extraneousFiles)
|
||||
{
|
||||
Log.Info($"Deleting {path}");
|
||||
File.Delete(path);
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
using API.Schema.NotificationsContext;
|
||||
|
||||
namespace API.Workers.MaintenanceWorkers;
|
||||
|
||||
public class RemoveOldNotificationsWorker(TimeSpan? interval = null, IEnumerable<BaseWorker>? dependsOn = null)
|
||||
: BaseWorkerWithContext<NotificationsContext>(dependsOn), IPeriodic
|
||||
{
|
||||
public DateTime LastExecution { get; set; } = DateTime.UnixEpoch;
|
||||
public TimeSpan Interval { get; set; } = interval ?? TimeSpan.FromHours(1);
|
||||
|
||||
protected override BaseWorker[] DoWorkInternal()
|
||||
{
|
||||
IQueryable<Notification> toRemove = DbContext.Notifications.Where(n => n.IsSent || DateTime.UtcNow - n.Date > Interval);
|
||||
DbContext.RemoveRange(toRemove);
|
||||
DbContext.Sync();
|
||||
return [];
|
||||
}
|
||||
|
||||
}
|
29
API/Workers/PeriodicWorkers/SendNotificationsWorker.cs
Normal file
29
API/Workers/PeriodicWorkers/SendNotificationsWorker.cs
Normal file
@ -0,0 +1,29 @@
|
||||
using API.Schema.NotificationsContext;
|
||||
using API.Schema.NotificationsContext.NotificationConnectors;
|
||||
|
||||
namespace API.Workers;
|
||||
|
||||
public class SendNotificationsWorker(TimeSpan? interval = null, IEnumerable<BaseWorker>? dependsOn = null)
|
||||
: BaseWorkerWithContext<NotificationsContext>(dependsOn), IPeriodic
|
||||
{
|
||||
public DateTime LastExecution { get; set; } = DateTime.UnixEpoch;
|
||||
public TimeSpan Interval { get; set; } = interval??TimeSpan.FromMinutes(1);
|
||||
protected override BaseWorker[] DoWorkInternal()
|
||||
{
|
||||
NotificationConnector[] connectors = DbContext.NotificationConnectors.ToArray();
|
||||
Notification[] notifications = DbContext.Notifications.Where(n => n.IsSent == false).ToArray();
|
||||
|
||||
foreach (Notification notification in notifications)
|
||||
{
|
||||
foreach (NotificationConnector connector in connectors)
|
||||
{
|
||||
connector.SendNotification(notification.Title, notification.Message);
|
||||
notification.IsSent = true;
|
||||
}
|
||||
}
|
||||
|
||||
DbContext.Sync();
|
||||
return [];
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
using API.Schema.MangaContext;
|
||||
|
||||
namespace API.Workers;
|
||||
|
||||
public class StartNewChapterDownloadsWorker(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.FromMinutes(1);
|
||||
protected override BaseWorker[] DoWorkInternal()
|
||||
{
|
||||
IQueryable<MangaConnectorId<Chapter>> mangaConnectorIds = DbContext.MangaConnectorToChapter.Where(id => id.Obj.Downloaded == false && id.UseForDownload);
|
||||
|
||||
List<BaseWorker> newWorkers = new();
|
||||
foreach (MangaConnectorId<Chapter> mangaConnectorId in mangaConnectorIds)
|
||||
newWorkers.Add(new DownloadChapterFromMangaconnectorWorker(mangaConnectorId));
|
||||
|
||||
return newWorkers.ToArray();
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user