mirror of
https://github.com/C9Glax/tranga.git
synced 2025-06-23 19:44:16 +02:00
Compare commits
17 Commits
99a3f2614d
...
cuttingedg
Author | SHA1 | Date | |
---|---|---|---|
12a542da39 | |||
3f5c9d0ca1 | |||
538825f0ef | |||
f0de0a29da | |||
d4227f2b8f | |||
cd00d35f22 | |||
4ef3e877ce | |||
7dba2518f9 | |||
7506a0201e | |||
91fb815153 | |||
6faf8bc733 | |||
bdff5b7aec | |||
5af8060d7b | |||
6ed8ff1d52 | |||
3324ed6e4a | |||
67fd9d284b | |||
08f26dd21d |
27
.dockerignore
Normal file
27
.dockerignore
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
**/.dockerignore
|
||||||
|
**/.env
|
||||||
|
**/.git
|
||||||
|
**/.gitignore
|
||||||
|
**/.project
|
||||||
|
**/.settings
|
||||||
|
**/.toolstarget
|
||||||
|
**/.vs
|
||||||
|
**/.vscode
|
||||||
|
**/.idea
|
||||||
|
**/*.*proj.user
|
||||||
|
**/*.dbmdl
|
||||||
|
**/*.jfm
|
||||||
|
**/azds.yaml
|
||||||
|
**/bin
|
||||||
|
**/charts
|
||||||
|
**/docker-compose*
|
||||||
|
**/Dockerfile*
|
||||||
|
**/node_modules
|
||||||
|
**/npm-debug.log
|
||||||
|
**/obj
|
||||||
|
**/secrets.dev.yaml
|
||||||
|
**/values.dev.yaml
|
||||||
|
LICENSE
|
||||||
|
README.md
|
||||||
|
Manga
|
||||||
|
settings
|
@ -22,7 +22,7 @@ jobs:
|
|||||||
# https://github.com/marketplace/actions/docker-setup-buildx
|
# https://github.com/marketplace/actions/docker-setup-buildx
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
id: buildx
|
id: buildx
|
||||||
uses: docker/setup-buildx-action@v3.10.0
|
uses: docker/setup-buildx-action@v3.11.0
|
||||||
|
|
||||||
# https://github.com/docker/login-action#docker-hub
|
# https://github.com/docker/login-action#docker-hub
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
@ -33,7 +33,7 @@ jobs:
|
|||||||
|
|
||||||
# https://github.com/docker/build-push-action#multi-platform-image
|
# https://github.com/docker/build-push-action#multi-platform-image
|
||||||
- name: Build and push API
|
- name: Build and push API
|
||||||
uses: docker/build-push-action@v6.15.0
|
uses: docker/build-push-action@v6.18.0
|
||||||
with:
|
with:
|
||||||
context: ./
|
context: ./
|
||||||
file: ./Dockerfile
|
file: ./Dockerfile
|
||||||
|
4
.github/workflows/docker-image-master.yml
vendored
4
.github/workflows/docker-image-master.yml
vendored
@ -22,7 +22,7 @@ jobs:
|
|||||||
# https://github.com/marketplace/actions/docker-setup-buildx
|
# https://github.com/marketplace/actions/docker-setup-buildx
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
id: buildx
|
id: buildx
|
||||||
uses: docker/setup-buildx-action@v3.10.0
|
uses: docker/setup-buildx-action@v3.11.0
|
||||||
|
|
||||||
# https://github.com/docker/login-action#docker-hub
|
# https://github.com/docker/login-action#docker-hub
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
@ -33,7 +33,7 @@ jobs:
|
|||||||
|
|
||||||
# https://github.com/docker/build-push-action#multi-platform-image
|
# https://github.com/docker/build-push-action#multi-platform-image
|
||||||
- name: Build and push API
|
- name: Build and push API
|
||||||
uses: docker/build-push-action@v6.15.0
|
uses: docker/build-push-action@v6.18.0
|
||||||
with:
|
with:
|
||||||
context: ./
|
context: ./
|
||||||
file: ./Dockerfile
|
file: ./Dockerfile
|
||||||
|
4
.github/workflows/docker-image-serverv2.yml
vendored
4
.github/workflows/docker-image-serverv2.yml
vendored
@ -22,7 +22,7 @@ jobs:
|
|||||||
# https://github.com/marketplace/actions/docker-setup-buildx
|
# https://github.com/marketplace/actions/docker-setup-buildx
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
id: buildx
|
id: buildx
|
||||||
uses: docker/setup-buildx-action@v3.10.0
|
uses: docker/setup-buildx-action@v3.11.0
|
||||||
|
|
||||||
# https://github.com/docker/login-action#docker-hub
|
# https://github.com/docker/login-action#docker-hub
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
@ -33,7 +33,7 @@ jobs:
|
|||||||
|
|
||||||
# https://github.com/docker/build-push-action#multi-platform-image
|
# https://github.com/docker/build-push-action#multi-platform-image
|
||||||
- name: Build and push API
|
- name: Build and push API
|
||||||
uses: docker/build-push-action@v6.15.0
|
uses: docker/build-push-action@v6.18.0
|
||||||
with:
|
with:
|
||||||
context: ./
|
context: ./
|
||||||
file: ./Dockerfile
|
file: ./Dockerfile
|
||||||
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -20,8 +20,6 @@ riderModule.iml
|
|||||||
cover.jpg
|
cover.jpg
|
||||||
cover.png
|
cover.png
|
||||||
/.vscode
|
/.vscode
|
||||||
/.vs/
|
|
||||||
Tranga/Properties/launchSettings.json
|
|
||||||
/Manga
|
/Manga
|
||||||
/settings
|
/settings
|
||||||
*.DotSettings.user
|
*.DotSettings.user
|
@ -1,38 +0,0 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<TargetFramework>net9.0</TargetFramework>
|
|
||||||
<Nullable>enable</Nullable>
|
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
|
||||||
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
|
|
||||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
|
||||||
<NoWarn>$(NoWarn);1591</NoWarn>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<PackageReference Include="Asp.Versioning.Mvc.ApiExplorer" Version="8.1.0" />
|
|
||||||
<PackageReference Include="HtmlAgilityPack" Version="1.12.0" />
|
|
||||||
<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" />
|
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.3">
|
|
||||||
<PrivateAssets>all</PrivateAssets>
|
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
|
||||||
</PackageReference>
|
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
|
||||||
<PackageReference Include="Npgsql" Version="9.0.3" />
|
|
||||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4" />
|
|
||||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.Design" Version="1.1.0" />
|
|
||||||
<PackageReference Include="PuppeteerSharp" Version="20.1.3" />
|
|
||||||
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.7" />
|
|
||||||
<PackageReference Include="Soenneker.Utils.String.NeedlemanWunsch" Version="3.0.929" />
|
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="8.1.0" />
|
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore.Newtonsoft" Version="8.1.0" />
|
|
||||||
<PackageReference Include="System.Drawing.Common" Version="9.0.3" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<Folder Include="Migrations\" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
</Project>
|
|
@ -1,6 +0,0 @@
|
|||||||
@API_HostAddress = http://localhost:5105
|
|
||||||
|
|
||||||
GET {{API_HostAddress}}/weatherforecast/
|
|
||||||
Accept: application/json
|
|
||||||
|
|
||||||
###
|
|
@ -1,5 +0,0 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
|
||||||
|
|
||||||
namespace API.APIEndpointRecords;
|
|
||||||
|
|
||||||
public record DownloadAvailableJobsRecord([Required]ulong recurrenceTimeMs, [Required]string localLibraryId);
|
|
@ -1,16 +0,0 @@
|
|||||||
namespace API.APIEndpointRecords;
|
|
||||||
|
|
||||||
public record GotifyRecord(string endpoint, string appToken, int priority)
|
|
||||||
{
|
|
||||||
public bool Validate()
|
|
||||||
{
|
|
||||||
if (endpoint == string.Empty)
|
|
||||||
return false;
|
|
||||||
if (appToken == string.Empty)
|
|
||||||
return false;
|
|
||||||
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);
|
|
@ -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,17 +0,0 @@
|
|||||||
namespace API.APIEndpointRecords;
|
|
||||||
|
|
||||||
public record NtfyRecord(string endpoint, string username, string password, string topic, int priority)
|
|
||||||
{
|
|
||||||
public bool Validate()
|
|
||||||
{
|
|
||||||
if (endpoint == string.Empty)
|
|
||||||
return false;
|
|
||||||
if (username == string.Empty)
|
|
||||||
return false;
|
|
||||||
if (password == string.Empty)
|
|
||||||
return false;
|
|
||||||
if (priority < 1 || priority > 5)
|
|
||||||
return false;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,13 +0,0 @@
|
|||||||
namespace API.APIEndpointRecords;
|
|
||||||
|
|
||||||
public record PushoverRecord(string apptoken, string user)
|
|
||||||
{
|
|
||||||
public bool Validate()
|
|
||||||
{
|
|
||||||
if (apptoken == string.Empty)
|
|
||||||
return false;
|
|
||||||
if (user == string.Empty)
|
|
||||||
return false;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,373 +0,0 @@
|
|||||||
using API.APIEndpointRecords;
|
|
||||||
using API.Schema;
|
|
||||||
using API.Schema.Jobs;
|
|
||||||
using Asp.Versioning;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
using static Microsoft.AspNetCore.Http.StatusCodes;
|
|
||||||
|
|
||||||
namespace API.Controllers;
|
|
||||||
|
|
||||||
[ApiVersion(2)]
|
|
||||||
[ApiController]
|
|
||||||
[Route("v{version:apiVersion}/[controller]")]
|
|
||||||
public class JobController(PgsqlContext context) : 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 Library with ID</response>
|
|
||||||
/// <response code="404">Could not find Manga with ID</response>
|
|
||||||
/// <response code="500">Error during Database Operation</response>
|
|
||||||
[HttpPut("DownloadAvailableChaptersJob/{MangaId}")]
|
|
||||||
[ProducesResponseType<string[]>(Status201Created, "application/json")]
|
|
||||||
[ProducesResponseType(Status400BadRequest)]
|
|
||||||
[ProducesResponseType(Status404NotFound)]
|
|
||||||
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
|
|
||||||
public IActionResult CreateDownloadAvailableChaptersJob(string MangaId, [FromBody]DownloadAvailableJobsRecord 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)
|
|
||||||
{
|
|
||||||
return StatusCode(500, e.Message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Job dep = new RetrieveChaptersJob(record.recurrenceTimeMs, MangaId);
|
|
||||||
Job job = new DownloadAvailableChaptersJob(record.recurrenceTimeMs, MangaId, null, [dep.JobId]);
|
|
||||||
return AddJobs([dep, job]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <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 null)
|
|
||||||
return NotFound();
|
|
||||||
Job job = new DownloadSingleChapterJob(ChapterId);
|
|
||||||
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 null)
|
|
||||||
return NotFound();
|
|
||||||
Job job = new UpdateFilesDownloadedJob(0, MangaId);
|
|
||||||
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<string> ids = context.Mangas.Select(m => m.MangaId).ToList();
|
|
||||||
List<UpdateFilesDownloadedJob> jobs = ids.Select(id => new UpdateFilesDownloadedJob(0, id)).ToList();
|
|
||||||
try
|
|
||||||
{
|
|
||||||
context.Jobs.AddRange(jobs);
|
|
||||||
context.SaveChanges();
|
|
||||||
return Created();
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
return StatusCode(500, e.Message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 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)
|
|
||||||
{
|
|
||||||
if(context.Mangas.Find(MangaId) is null)
|
|
||||||
return NotFound();
|
|
||||||
Job job = new UpdateMetadataJob(0, MangaId);
|
|
||||||
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("UpdateAllMetadataJob")]
|
|
||||||
[ProducesResponseType<string[]>(Status201Created, "application/json")]
|
|
||||||
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
|
|
||||||
public IActionResult CreateUpdateAllMetadataJob()
|
|
||||||
{
|
|
||||||
List<string> ids = context.Mangas.Select(m => m.MangaId).ToList();
|
|
||||||
List<UpdateMetadataJob> jobs = ids.Select(id => new UpdateMetadataJob(0, id)).ToList();
|
|
||||||
try
|
|
||||||
{
|
|
||||||
context.Jobs.AddRange(jobs);
|
|
||||||
context.SaveChanges();
|
|
||||||
return Created();
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
return StatusCode(500, e.Message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private IActionResult AddJobs(Job[] jobs)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
context.Jobs.AddRange(jobs);
|
|
||||||
context.SaveChanges();
|
|
||||||
return new CreatedResult((string?)null, jobs.Select(j => j.JobId).ToArray());
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
return StatusCode(500, e.Message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Delete Job with ID and all children
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="JobId">Job-ID</param>
|
|
||||||
/// <response code="200">Job(s) deleted</response>
|
|
||||||
/// <response code="404">Job could not be found</response>
|
|
||||||
/// <response code="500">Error during Database Operation</response>
|
|
||||||
[HttpDelete("{JobId}")]
|
|
||||||
[ProducesResponseType<string[]>(Status200OK, "application/json")]
|
|
||||||
[ProducesResponseType(Status404NotFound)]
|
|
||||||
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
|
|
||||||
public IActionResult DeleteJob(string JobId)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Job? ret = context.Jobs.Find(JobId);
|
|
||||||
if(ret is null)
|
|
||||||
return NotFound();
|
|
||||||
IQueryable<Job> children = GetChildJobs(JobId);
|
|
||||||
|
|
||||||
context.RemoveRange(children);
|
|
||||||
context.Remove(ret);
|
|
||||||
context.SaveChanges();
|
|
||||||
return new OkObjectResult(children.Select(x => x.JobId).Append(ret.JobId).ToArray());
|
|
||||||
}
|
|
||||||
catch (Exception 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)
|
|
||||||
{
|
|
||||||
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)
|
|
||||||
{
|
|
||||||
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(501);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,96 +0,0 @@
|
|||||||
using API.Schema;
|
|
||||||
using API.Schema.LibraryConnectors;
|
|
||||||
using Asp.Versioning;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
using static Microsoft.AspNetCore.Http.StatusCodes;
|
|
||||||
|
|
||||||
namespace API.Controllers;
|
|
||||||
|
|
||||||
[ApiVersion(2)]
|
|
||||||
[ApiController]
|
|
||||||
[Route("v{v:apiVersion}/[controller]")]
|
|
||||||
public class LibraryConnectorController(PgsqlContext context) : Controller
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Gets all configured Library-Connectors
|
|
||||||
/// </summary>
|
|
||||||
/// <response code="200"></response>
|
|
||||||
[HttpGet]
|
|
||||||
[ProducesResponseType<LibraryConnector[]>(Status200OK, "application/json")]
|
|
||||||
public IActionResult GetAllConnectors()
|
|
||||||
{
|
|
||||||
LibraryConnector[] connectors = context.LibraryConnectors.ToArray();
|
|
||||||
return Ok(connectors);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns Library-Connector with requested ID
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="LibraryControllerId">Library-Connector-ID</param>
|
|
||||||
/// <response code="200"></response>
|
|
||||||
/// <response code="404">Connector with ID not found.</response>
|
|
||||||
[HttpGet("{LibraryControllerId}")]
|
|
||||||
[ProducesResponseType<LibraryConnector>(Status200OK, "application/json")]
|
|
||||||
[ProducesResponseType(Status404NotFound)]
|
|
||||||
public IActionResult GetConnector(string LibraryControllerId)
|
|
||||||
{
|
|
||||||
LibraryConnector? ret = context.LibraryConnectors.Find(LibraryControllerId);
|
|
||||||
return (ret is not null) switch
|
|
||||||
{
|
|
||||||
true => Ok(ret),
|
|
||||||
false => NotFound()
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Creates a new Library-Connector
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="libraryConnector">Library-Connector</param>
|
|
||||||
/// <response code="201"></response>
|
|
||||||
/// <response code="500">Error during Database Operation</response>
|
|
||||||
[HttpPut]
|
|
||||||
[ProducesResponseType(Status201Created)]
|
|
||||||
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
|
|
||||||
public IActionResult CreateConnector([FromBody]LibraryConnector libraryConnector)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
context.LibraryConnectors.Add(libraryConnector);
|
|
||||||
context.SaveChanges();
|
|
||||||
return Created();
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
return StatusCode(500, e.Message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Deletes the Library-Connector with the requested ID
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="LibraryControllerId">Library-Connector-ID</param>
|
|
||||||
/// <response code="200"></response>
|
|
||||||
/// <response code="404">Connector with ID not found.</response>
|
|
||||||
/// <response code="500">Error during Database Operation</response>
|
|
||||||
[HttpDelete("{LibraryControllerId}")]
|
|
||||||
[ProducesResponseType(Status200OK)]
|
|
||||||
[ProducesResponseType(Status404NotFound)]
|
|
||||||
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
|
|
||||||
public IActionResult DeleteConnector(string LibraryControllerId)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
LibraryConnector? ret = context.LibraryConnectors.Find(LibraryControllerId);
|
|
||||||
if (ret is null)
|
|
||||||
return NotFound();
|
|
||||||
|
|
||||||
context.Remove(ret);
|
|
||||||
context.SaveChanges();
|
|
||||||
return Ok();
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
return StatusCode(500, e.Message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,157 +0,0 @@
|
|||||||
using API.APIEndpointRecords;
|
|
||||||
using API.Schema;
|
|
||||||
using Asp.Versioning;
|
|
||||||
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) : 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)
|
|
||||||
{
|
|
||||||
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)
|
|
||||||
{
|
|
||||||
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)
|
|
||||||
{
|
|
||||||
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)
|
|
||||||
{
|
|
||||||
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)
|
|
||||||
{
|
|
||||||
return StatusCode(500, e.Message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,80 +0,0 @@
|
|||||||
using API.Schema;
|
|
||||||
using API.Schema.MangaConnectors;
|
|
||||||
using Asp.Versioning;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
using static Microsoft.AspNetCore.Http.StatusCodes;
|
|
||||||
|
|
||||||
namespace API.Controllers;
|
|
||||||
|
|
||||||
[ApiVersion(2)]
|
|
||||||
[ApiController]
|
|
||||||
[Route("v{v:apiVersion}/[controller]")]
|
|
||||||
public class MangaConnectorController(PgsqlContext context) : Controller
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Get all available Connectors (Scanlation-Sites)
|
|
||||||
/// </summary>
|
|
||||||
/// <response code="200"></response>
|
|
||||||
[HttpGet]
|
|
||||||
[ProducesResponseType<MangaConnector[]>(Status200OK, "application/json")]
|
|
||||||
public IActionResult GetConnectors()
|
|
||||||
{
|
|
||||||
MangaConnector[] connectors = context.MangaConnectors.ToArray();
|
|
||||||
return Ok(connectors);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Get all enabled Connectors (Scanlation-Sites)
|
|
||||||
/// </summary>
|
|
||||||
/// <response code="200"></response>
|
|
||||||
[HttpGet("enabled")]
|
|
||||||
[ProducesResponseType<MangaConnector[]>(Status200OK, "application/json")]
|
|
||||||
public IActionResult GetEnabledConnectors()
|
|
||||||
{
|
|
||||||
MangaConnector[] connectors = context.MangaConnectors.Where(c => c.Enabled == true).ToArray();
|
|
||||||
return Ok(connectors);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Get all disabled Connectors (Scanlation-Sites)
|
|
||||||
/// </summary>
|
|
||||||
/// <response code="200"></response>
|
|
||||||
[HttpGet("disabled")]
|
|
||||||
[ProducesResponseType<MangaConnector[]>(Status200OK, "application/json")]
|
|
||||||
public IActionResult GetDisabledConnectors()
|
|
||||||
{
|
|
||||||
MangaConnector[] connectors = context.MangaConnectors.Where(c => c.Enabled == false).ToArray();
|
|
||||||
return Ok(connectors);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Enabled or disables a Connector
|
|
||||||
/// </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>
|
|
||||||
/// <response code="500">Error during Database Operation</response>
|
|
||||||
[HttpPatch("{MangaConnectorName}/SetEnabled/{enabled}")]
|
|
||||||
[ProducesResponseType(Status200OK)]
|
|
||||||
[ProducesResponseType(Status404NotFound)]
|
|
||||||
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
|
|
||||||
public IActionResult SetEnabled(string MangaConnectorName, bool enabled)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
MangaConnector? connector = context.MangaConnectors.Find(MangaConnectorName);
|
|
||||||
if (connector is null)
|
|
||||||
return NotFound();
|
|
||||||
|
|
||||||
connector.Enabled = enabled;
|
|
||||||
context.SaveChanges();
|
|
||||||
|
|
||||||
return Ok();
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
return StatusCode(500, e.Message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,355 +0,0 @@
|
|||||||
using API.Schema;
|
|
||||||
using API.Schema.Jobs;
|
|
||||||
using Asp.Versioning;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
using SixLabors.ImageSharp;
|
|
||||||
using SixLabors.ImageSharp.Formats.Jpeg;
|
|
||||||
using SixLabors.ImageSharp.Processing;
|
|
||||||
using SixLabors.ImageSharp.Processing.Processors.Transforms;
|
|
||||||
using static Microsoft.AspNetCore.Http.StatusCodes;
|
|
||||||
|
|
||||||
namespace API.Controllers;
|
|
||||||
|
|
||||||
[ApiVersion(2)]
|
|
||||||
[ApiController]
|
|
||||||
[Route("v{v:apiVersion}/[controller]")]
|
|
||||||
public class MangaController(PgsqlContext context) : Controller
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Returns all cached Manga
|
|
||||||
/// </summary>
|
|
||||||
/// <response code="200"></response>
|
|
||||||
[HttpGet]
|
|
||||||
[ProducesResponseType<Manga[]>(Status200OK, "application/json")]
|
|
||||||
public IActionResult GetAllManga()
|
|
||||||
{
|
|
||||||
Manga[] ret = context.Mangas.ToArray();
|
|
||||||
return Ok(ret);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns all cached Manga with IDs
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="ids">Array of Manga-IDs</param>
|
|
||||||
/// <response code="200"></response>
|
|
||||||
[HttpPost("WithIDs")]
|
|
||||||
[ProducesResponseType<Manga[]>(Status200OK, "application/json")]
|
|
||||||
public IActionResult GetManga([FromBody]string[] ids)
|
|
||||||
{
|
|
||||||
Manga[] ret = context.Mangas.Where(m => ids.Contains(m.MangaId)).ToArray();
|
|
||||||
return Ok(ret);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Return Manga with ID
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="MangaId">Manga-ID</param>
|
|
||||||
/// <response code="200"></response>
|
|
||||||
/// <response code="404">Manga with ID 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);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Delete Manga with ID
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="MangaId">Manga-ID</param>
|
|
||||||
/// <response code="200"></response>
|
|
||||||
/// <response code="404">Manga with ID not found</response>
|
|
||||||
/// <response code="500">Error during Database Operation</response>
|
|
||||||
[HttpDelete("{MangaId}")]
|
|
||||||
[ProducesResponseType(Status200OK)]
|
|
||||||
[ProducesResponseType(Status404NotFound)]
|
|
||||||
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
|
|
||||||
public IActionResult DeleteManga(string MangaId)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Manga? ret = context.Mangas.Find(MangaId);
|
|
||||||
if (ret is null)
|
|
||||||
return NotFound();
|
|
||||||
|
|
||||||
context.Remove(ret);
|
|
||||||
context.SaveChanges();
|
|
||||||
return Ok();
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
return StatusCode(500, e.Message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns Cover of Manga
|
|
||||||
/// </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>
|
|
||||||
/// <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="503">Retry later, downloading cover</response>
|
|
||||||
[HttpGet("{MangaId}/Cover")]
|
|
||||||
[ProducesResponseType<byte[]>(Status200OK,"image/jpeg")]
|
|
||||||
[ProducesResponseType(Status204NoContent)]
|
|
||||||
[ProducesResponseType(Status400BadRequest)]
|
|
||||||
[ProducesResponseType(Status404NotFound)]
|
|
||||||
[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 (!System.IO.File.Exists(m.CoverFileNameInCache))
|
|
||||||
{
|
|
||||||
List<Job> coverDownloadJobs = context.Jobs.Where(j => j.JobType == JobType.DownloadMangaCoverJob).ToList();
|
|
||||||
if (coverDownloadJobs.Any(j => j is DownloadMangaCoverJob dmc && dmc.MangaId == MangaId))
|
|
||||||
{
|
|
||||||
Response.Headers.Add("Retry-After", $"{TrangaSettings.startNewJobTimeoutMs * coverDownloadJobs.Count() * 2 / 1000:D}");
|
|
||||||
return StatusCode(Status503ServiceUnavailable, TrangaSettings.startNewJobTimeoutMs * coverDownloadJobs.Count() * 2 / 1000);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
return NoContent();
|
|
||||||
}
|
|
||||||
|
|
||||||
Image image = Image.Load(m.CoverFileNameInCache);
|
|
||||||
|
|
||||||
if (width is { } w && height is { } h)
|
|
||||||
{
|
|
||||||
if (width < 10 || height < 10 || width > 65535 || height > 65535)
|
|
||||||
return BadRequest();
|
|
||||||
image.Mutate(i => i.ApplyProcessor(new ResizeProcessor(new ResizeOptions()
|
|
||||||
{
|
|
||||||
Mode = ResizeMode.Max,
|
|
||||||
Size = new Size(w, h)
|
|
||||||
}, image.Size)));
|
|
||||||
}
|
|
||||||
|
|
||||||
using MemoryStream ms = new();
|
|
||||||
image.Save(ms, new JpegEncoder(){Quality = 100});
|
|
||||||
return File(ms.GetBuffer(), "image/jpeg");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns all Chapters of Manga
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="MangaId">Manga-ID</param>
|
|
||||||
/// <response code="200"></response>
|
|
||||||
/// <response code="404">Manga with ID 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();
|
|
||||||
|
|
||||||
Chapter[] ret = context.Chapters.Where(c => c.ParentMangaId == m.MangaId).ToArray();
|
|
||||||
return Ok(ret);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns all downloaded Chapters for Manga with ID
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="MangaId">Manga-ID</param>
|
|
||||||
/// <response code="200"></response>
|
|
||||||
/// <response code="204">No available chapters</response>
|
|
||||||
/// <response code="404">Manga with ID 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();
|
|
||||||
|
|
||||||
List<Chapter> chapters = context.Chapters.Where(c => c.ParentMangaId == m.MangaId && c.Downloaded == true).ToList();
|
|
||||||
if (chapters.Count == 0)
|
|
||||||
return NoContent();
|
|
||||||
|
|
||||||
return Ok(chapters);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns all Chapters not downloaded for Manga with ID
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="MangaId">Manga-ID</param>
|
|
||||||
/// <response code="200"></response>
|
|
||||||
/// <response code="204">No available chapters</response>
|
|
||||||
/// <response code="404">Manga with ID 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();
|
|
||||||
|
|
||||||
List<Chapter> chapters = context.Chapters.Where(c => c.ParentMangaId == m.MangaId && c.Downloaded == false).ToList();
|
|
||||||
if (chapters.Count == 0)
|
|
||||||
return NoContent();
|
|
||||||
|
|
||||||
return Ok(chapters);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns the latest Chapter of requested Manga available on Website
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="MangaId">Manga-ID</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="503">Retry after timeout, updating value</response>
|
|
||||||
[HttpGet("{MangaId}/Chapter/LatestAvailable")]
|
|
||||||
[ProducesResponseType<Chapter>(Status200OK, "application/json")]
|
|
||||||
[ProducesResponseType(Status204NoContent)]
|
|
||||||
[ProducesResponseType<string>(Status404NotFound, "text/plain")]
|
|
||||||
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
|
|
||||||
[ProducesResponseType<int>(Status503ServiceUnavailable, "text/plain")]
|
|
||||||
public IActionResult GetLatestChapter(string MangaId)
|
|
||||||
{
|
|
||||||
Manga? m = context.Mangas.Find(MangaId);
|
|
||||||
if (m is null)
|
|
||||||
return NotFound();
|
|
||||||
|
|
||||||
List<Chapter> chapters = context.Chapters.Where(c => c.ParentMangaId == m.MangaId).ToList();
|
|
||||||
if (chapters.Count == 0)
|
|
||||||
{
|
|
||||||
List<Job> retrieveChapterJobs = context.Jobs.Where(j => j.JobType == JobType.RetrieveChaptersJob).ToList();
|
|
||||||
if (retrieveChapterJobs.Any(j => j is RetrieveChaptersJob rcj && rcj.MangaId == MangaId))
|
|
||||||
{
|
|
||||||
Response.Headers.Add("Retry-After", $"{TrangaSettings.startNewJobTimeoutMs * retrieveChapterJobs.Count() * 2 / 1000:D}");
|
|
||||||
return StatusCode(Status503ServiceUnavailable, TrangaSettings.startNewJobTimeoutMs * retrieveChapterJobs.Count() * 2/ 1000);
|
|
||||||
}else
|
|
||||||
return NoContent();
|
|
||||||
}
|
|
||||||
|
|
||||||
Chapter? max = chapters.Max();
|
|
||||||
if (max is null)
|
|
||||||
return StatusCode(500, "Max chapter could not be found");
|
|
||||||
|
|
||||||
return Ok(max);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns the latest Chapter of requested Manga that is downloaded
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="MangaId">Manga-ID</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="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<int>(Status503ServiceUnavailable, "text/plain")]
|
|
||||||
public IActionResult GetLatestChapterDownloaded(string MangaId)
|
|
||||||
{
|
|
||||||
Manga? m = context.Mangas.Find(MangaId);
|
|
||||||
if (m is null)
|
|
||||||
return NotFound();
|
|
||||||
|
|
||||||
|
|
||||||
List<Chapter> chapters = context.Chapters.Where(c => c.ParentMangaId == m.MangaId && c.Downloaded == true).ToList();
|
|
||||||
if (chapters.Count == 0)
|
|
||||||
{
|
|
||||||
List<Job> retrieveChapterJobs = context.Jobs.Where(j => j.JobType == JobType.RetrieveChaptersJob).ToList();
|
|
||||||
if (retrieveChapterJobs.Any(j => j is RetrieveChaptersJob rcj && rcj.MangaId == MangaId))
|
|
||||||
{
|
|
||||||
Response.Headers.Add("Retry-After", $"{TrangaSettings.startNewJobTimeoutMs * retrieveChapterJobs.Count() * 2 / 1000:D}");
|
|
||||||
return StatusCode(Status503ServiceUnavailable, TrangaSettings.startNewJobTimeoutMs * retrieveChapterJobs.Count() * 2 / 1000);
|
|
||||||
}else
|
|
||||||
return NoContent();
|
|
||||||
}
|
|
||||||
|
|
||||||
Chapter? max = chapters.Max();
|
|
||||||
if (max is null)
|
|
||||||
return StatusCode(500, "Max chapter could not be found");
|
|
||||||
|
|
||||||
return Ok(max);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Configure the cut-off for Manga
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="MangaId">Manga-ID</param>
|
|
||||||
/// <response code="200"></response>
|
|
||||||
/// <response code="404">Manga with ID not found.</response>
|
|
||||||
/// <response code="500">Error during Database Operation</response>
|
|
||||||
[HttpPatch("{MangaId}/IgnoreChaptersBefore")]
|
|
||||||
[ProducesResponseType(Status200OK)]
|
|
||||||
[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)
|
|
||||||
return NotFound();
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
m.IgnoreChapterBefore = chapterThreshold;
|
|
||||||
context.SaveChanges();
|
|
||||||
return Ok();
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
return StatusCode(500, e.Message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Move Manga to different Library
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="MangaId">Manga-ID</param>
|
|
||||||
/// <param name="LibraryId">Library-Id</param>
|
|
||||||
/// <response code="202">Folder is going to be moved</response>
|
|
||||||
/// <response code="404">MangaId or LibraryId not found</response>
|
|
||||||
/// <response code="500">Error during Database Operation</response>
|
|
||||||
[HttpPost("{MangaId}/ChangeLibrary/{LibraryId}")]
|
|
||||||
[ProducesResponseType(Status202Accepted)]
|
|
||||||
[ProducesResponseType(Status404NotFound)]
|
|
||||||
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
|
|
||||||
public IActionResult MoveFolder(string MangaId, string LibraryId)
|
|
||||||
{
|
|
||||||
Manga? manga = context.Mangas.Find(MangaId);
|
|
||||||
if (manga is null)
|
|
||||||
return NotFound();
|
|
||||||
LocalLibrary? library = context.LocalLibraries.Find(LibraryId);
|
|
||||||
if (library is null)
|
|
||||||
return NotFound();
|
|
||||||
|
|
||||||
MoveMangaLibraryJob dep = new (MangaId, LibraryId);
|
|
||||||
UpdateFilesDownloadedJob up = new (0, manga.MangaId, null, [dep.JobId]);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
context.Jobs.AddRange([dep, up]);
|
|
||||||
context.SaveChanges();
|
|
||||||
return Accepted();
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
return StatusCode(500, e.Message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,215 +0,0 @@
|
|||||||
using System.Text;
|
|
||||||
using API.APIEndpointRecords;
|
|
||||||
using API.Schema;
|
|
||||||
using API.Schema.NotificationConnectors;
|
|
||||||
using Asp.Versioning;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
using static Microsoft.AspNetCore.Http.StatusCodes;
|
|
||||||
|
|
||||||
namespace API.Controllers;
|
|
||||||
|
|
||||||
[ApiVersion(2)]
|
|
||||||
[ApiController]
|
|
||||||
[Produces("application/json")]
|
|
||||||
[Route("v{v:apiVersion}/[controller]")]
|
|
||||||
public class NotificationConnectorController(PgsqlContext context) : Controller
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Gets all configured Notification-Connectors
|
|
||||||
/// </summary>
|
|
||||||
/// <response code="200"></response>
|
|
||||||
[HttpGet]
|
|
||||||
[ProducesResponseType<NotificationConnector[]>(Status200OK, "application/json")]
|
|
||||||
public IActionResult GetAllConnectors()
|
|
||||||
{
|
|
||||||
NotificationConnector[] ret = context.NotificationConnectors.ToArray();
|
|
||||||
return Ok(ret);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns Notification-Connector with requested ID
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="NotificationConnectorId">Notification-Connector-ID</param>
|
|
||||||
/// <response code="200"></response>
|
|
||||||
/// <response code="404">NotificationConnector with ID not found</response>
|
|
||||||
[HttpGet("{NotificationConnectorId}")]
|
|
||||||
[ProducesResponseType<NotificationConnector>(Status200OK, "application/json")]
|
|
||||||
[ProducesResponseType(Status404NotFound)]
|
|
||||||
public IActionResult GetConnector(string NotificationConnectorId)
|
|
||||||
{
|
|
||||||
NotificationConnector? ret = context.NotificationConnectors.Find(NotificationConnectorId);
|
|
||||||
return (ret is not null) switch
|
|
||||||
{
|
|
||||||
true => Ok(ret),
|
|
||||||
false => NotFound()
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Creates a new REST-Notification-Connector
|
|
||||||
/// </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="500">Error during Database Operation</response>
|
|
||||||
[HttpPut]
|
|
||||||
[ProducesResponseType<string>(Status201Created, "application/json")]
|
|
||||||
[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)
|
|
||||||
{
|
|
||||||
return StatusCode(500, e.Message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Creates a new Gotify-Notification-Connector
|
|
||||||
/// </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="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();
|
|
||||||
|
|
||||||
NotificationConnector gotifyConnector = new NotificationConnector(TokenGen.CreateToken("Gotify"),
|
|
||||||
gotifyData.endpoint,
|
|
||||||
new Dictionary<string, string>() { { "X-Gotify-Key", gotifyData.appToken } },
|
|
||||||
"POST",
|
|
||||||
$"{{\"message\": \"%text\", \"title\": \"%title\", \"priority\": {gotifyData.priority}}}");
|
|
||||||
return CreateConnector(gotifyConnector);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Creates a new Ntfy-Notification-Connector
|
|
||||||
/// </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="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();
|
|
||||||
|
|
||||||
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}",
|
|
||||||
new Dictionary<string, string>()
|
|
||||||
{
|
|
||||||
{"Title", "%title"},
|
|
||||||
{"Priority", ntfyRecord.priority.ToString()},
|
|
||||||
},
|
|
||||||
"POST",
|
|
||||||
"%text");
|
|
||||||
return CreateConnector(ntfyConnector);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Creates a new Lunasea-Notification-Connector
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>https://docs.lunasea.app/lunasea/notifications/custom-notifications for id. Either device/:device_id or user/:user_id</remarks>
|
|
||||||
/// <response code="201">ID of new connector</response>
|
|
||||||
/// <response code="400"></response>
|
|
||||||
/// <response code="409">A NotificationConnector with name already exists</response>
|
|
||||||
/// <response code="500">Error during Database Operation</response>
|
|
||||||
[HttpPut("Lunasea")]
|
|
||||||
[ProducesResponseType<string>(Status201Created, "application/json")]
|
|
||||||
[ProducesResponseType(Status400BadRequest)]
|
|
||||||
[ProducesResponseType(Status409Conflict)]
|
|
||||||
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
|
|
||||||
public IActionResult CreateLunaseaConnector([FromBody]LunaseaRecord lunaseaRecord)
|
|
||||||
{
|
|
||||||
if(!lunaseaRecord.Validate())
|
|
||||||
return BadRequest();
|
|
||||||
|
|
||||||
NotificationConnector lunaseaConnector = new (TokenGen.CreateToken("Lunasea"),
|
|
||||||
$"https://notify.lunasea.app/v1/custom/{lunaseaRecord.id}",
|
|
||||||
new Dictionary<string, string>(),
|
|
||||||
"POST",
|
|
||||||
"{\"title\": \"%title\", \"body\": \"%text\"}");
|
|
||||||
return CreateConnector(lunaseaConnector);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Creates a new Pushover-Notification-Connector
|
|
||||||
/// </summary>
|
|
||||||
/// <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();
|
|
||||||
|
|
||||||
NotificationConnector pushoverConnector = new (TokenGen.CreateToken("Pushover"),
|
|
||||||
$"https://api.pushover.net/1/messages.json",
|
|
||||||
new Dictionary<string, string>(),
|
|
||||||
"POST",
|
|
||||||
$"{{\"token\": \"{pushoverRecord.apptoken}\", \"user\": \"{pushoverRecord.user}\", \"message:\":\"%text\", \"%title\" }}");
|
|
||||||
return CreateConnector(pushoverConnector);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Deletes the Notification-Connector with the requested ID
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="NotificationConnectorId">Notification-Connector-ID</param>
|
|
||||||
/// <response code="200"></response>
|
|
||||||
/// <response code="404">NotificationConnector with ID not found</response>
|
|
||||||
/// <response code="500">Error during Database Operation</response>
|
|
||||||
[HttpDelete("{NotificationConnectorId}")]
|
|
||||||
[ProducesResponseType(Status200OK)]
|
|
||||||
[ProducesResponseType(Status404NotFound)]
|
|
||||||
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
|
|
||||||
public IActionResult DeleteConnector(string NotificationConnectorId)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
NotificationConnector? ret = context.NotificationConnectors.Find(NotificationConnectorId);
|
|
||||||
if(ret is null)
|
|
||||||
return NotFound();
|
|
||||||
|
|
||||||
context.Remove(ret);
|
|
||||||
context.SaveChanges();
|
|
||||||
return Ok();
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
return StatusCode(500, e.Message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,103 +0,0 @@
|
|||||||
using API.Schema;
|
|
||||||
using Asp.Versioning;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
using static Microsoft.AspNetCore.Http.StatusCodes;
|
|
||||||
|
|
||||||
namespace API.Controllers;
|
|
||||||
|
|
||||||
[ApiVersion(2)]
|
|
||||||
[ApiController]
|
|
||||||
[Route("v{v:apiVersion}/[controller]")]
|
|
||||||
public class QueryController(PgsqlContext context) : Controller
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Returns the Author-Information for Author-ID
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="AuthorId">Author-Id</param>
|
|
||||||
/// <response code="200"></response>
|
|
||||||
/// <response code="404">Author with ID 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)
|
|
||||||
return NotFound();
|
|
||||||
return Ok(ret);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns all Mangas which where Authored by Author with AuthorId
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="AuthorId">Author-ID</param>
|
|
||||||
/// <response code="200"></response>
|
|
||||||
[HttpGet("Mangas/WithAuthorId/{AuthorId}")]
|
|
||||||
[ProducesResponseType<Manga[]>(Status200OK, "application/json")]
|
|
||||||
public IActionResult GetMangaWithAuthorIds(string AuthorId)
|
|
||||||
{
|
|
||||||
return Ok(context.Mangas.Where(m => m.AuthorIds.Contains(AuthorId)));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <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);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns AltTitle-Information for AltTitle-Id
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="AltTitleId"></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>
|
|
||||||
[HttpGet("Mangas/WithTag/{Tag}")]
|
|
||||||
[ProducesResponseType<Manga[]>(Status200OK, "application/json")]
|
|
||||||
public IActionResult GetMangasWithTag(string Tag)
|
|
||||||
{
|
|
||||||
return Ok(context.Mangas.Where(m => m.Tags.Contains(Tag)));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns Chapter-Information for Chapter-Id
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="ChapterId"></param>
|
|
||||||
/// <response code="200"></response>
|
|
||||||
/// <response code="404">Chapter with ID 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)
|
|
||||||
return NotFound();
|
|
||||||
return Ok(ret);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,201 +0,0 @@
|
|||||||
using API.Schema;
|
|
||||||
using API.Schema.Jobs;
|
|
||||||
using API.Schema.MangaConnectors;
|
|
||||||
using Asp.Versioning;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using static Microsoft.AspNetCore.Http.StatusCodes;
|
|
||||||
|
|
||||||
namespace API.Controllers;
|
|
||||||
|
|
||||||
[ApiVersion(2)]
|
|
||||||
[ApiController]
|
|
||||||
[Route("v{v:apiVersion}/[controller]")]
|
|
||||||
public class SearchController(PgsqlContext context) : Controller
|
|
||||||
{
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initiate a search for a Manga on all Connectors
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="name">Name/Title of the Manga</param>
|
|
||||||
/// <response code="200"></response>
|
|
||||||
/// <response code="500">Error during Database Operation</response>
|
|
||||||
[HttpPost("Name")]
|
|
||||||
[ProducesResponseType<Manga[]>(Status200OK, "application/json")]
|
|
||||||
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
|
|
||||||
public IActionResult SearchMangaGlobal([FromBody]string name)
|
|
||||||
{
|
|
||||||
List<(Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)> allManga = new();
|
|
||||||
foreach (MangaConnector contextMangaConnector in context.MangaConnectors.Where(connector => connector.Enabled))
|
|
||||||
allManga.AddRange(contextMangaConnector.GetManga(name));
|
|
||||||
|
|
||||||
List<Manga> retMangas = new();
|
|
||||||
foreach ((Manga? manga, List<Author>? authors, List<MangaTag>? tags, List<Link>? links, List<MangaAltTitle>? altTitles) in allManga)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Manga? add = AddMangaToContext(manga, authors, tags, links, altTitles);
|
|
||||||
if(add is not null)
|
|
||||||
retMangas.Add(add);
|
|
||||||
}
|
|
||||||
catch (DbUpdateException e)
|
|
||||||
{
|
|
||||||
return StatusCode(500, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return Ok(retMangas.ToArray());
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initiate a search for a Manga on a specific Connector
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="MangaConnectorName">Manga-Connector-ID</param>
|
|
||||||
/// <param name="name">Name/Title of the Manga</param>
|
|
||||||
/// <response code="200"></response>
|
|
||||||
/// <response code="404">MangaConnector with ID not found</response>
|
|
||||||
/// <response code="406">MangaConnector with ID is disabled</response>
|
|
||||||
/// <response code="500">Error during Database Operation</response>
|
|
||||||
[HttpPost("{MangaConnectorName}")]
|
|
||||||
[ProducesResponseType<Manga[]>(Status200OK, "application/json")]
|
|
||||||
[ProducesResponseType(Status404NotFound)]
|
|
||||||
[ProducesResponseType(Status406NotAcceptable)]
|
|
||||||
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
|
|
||||||
public IActionResult SearchManga(string MangaConnectorName, [FromBody]string name)
|
|
||||||
{
|
|
||||||
MangaConnector? connector = context.MangaConnectors.Find(MangaConnectorName);
|
|
||||||
if (connector is null)
|
|
||||||
return NotFound();
|
|
||||||
else if (connector.Enabled is false)
|
|
||||||
return StatusCode(406);
|
|
||||||
|
|
||||||
(Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)[] mangas = connector.GetManga(name);
|
|
||||||
List<Manga> retMangas = new();
|
|
||||||
foreach ((Manga? manga, List<Author>? authors, List<MangaTag>? tags, List<Link>? links, List<MangaAltTitle>? altTitles) in mangas)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Manga? add = AddMangaToContext(manga, authors, tags, links, altTitles);
|
|
||||||
if(add is not null)
|
|
||||||
retMangas.Add(add);
|
|
||||||
}
|
|
||||||
catch (DbUpdateException e)
|
|
||||||
{
|
|
||||||
return StatusCode(500, e.Message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Ok(retMangas.ToArray());
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns Manga from MangaConnector associated with URL
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="url">Manga-Page 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="500">Error during Database Operation</response>
|
|
||||||
[HttpPost("Url")]
|
|
||||||
[ProducesResponseType<Manga>(Status200OK, "application/json")]
|
|
||||||
[ProducesResponseType(Status300MultipleChoices)]
|
|
||||||
[ProducesResponseType(Status400BadRequest)]
|
|
||||||
[ProducesResponseType(Status404NotFound)]
|
|
||||||
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
|
|
||||||
public IActionResult GetMangaFromUrl([FromBody]string url)
|
|
||||||
{
|
|
||||||
List<MangaConnector> connectors = context.MangaConnectors.AsEnumerable().Where(c => c.ValidateUrl(url)).ToList();
|
|
||||||
if (connectors.Count == 0)
|
|
||||||
return NotFound();
|
|
||||||
else if (connectors.Count > 1)
|
|
||||||
return StatusCode(Status300MultipleChoices);
|
|
||||||
|
|
||||||
(Manga manga, List<Author>? authors, List<MangaTag>? tags, List<Link>? links, List<MangaAltTitle>? altTitles)? x = connectors.First().GetMangaFromUrl(url);
|
|
||||||
if (x is null)
|
|
||||||
return BadRequest();
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Manga? add = AddMangaToContext(x.Value.manga, x.Value.authors, x.Value.tags, x.Value.links, x.Value.altTitles);
|
|
||||||
if (add is not null)
|
|
||||||
return Ok(add);
|
|
||||||
return StatusCode(500);
|
|
||||||
}
|
|
||||||
catch (DbUpdateException e)
|
|
||||||
{
|
|
||||||
return StatusCode(500, e.Message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private Manga? AddMangaToContext(Manga? manga, List<Author>? authors, List<MangaTag>? tags, List<Link>? links,
|
|
||||||
List<MangaAltTitle>? altTitles)
|
|
||||||
{
|
|
||||||
if (manga is null)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
Manga? existing = context.Mangas.Find(manga.MangaId);
|
|
||||||
|
|
||||||
if (tags is not null)
|
|
||||||
{
|
|
||||||
IEnumerable<MangaTag> mergedTags = tags.Select(mt =>
|
|
||||||
{
|
|
||||||
MangaTag? inDb = context.Tags.Find(mt.Tag);
|
|
||||||
return inDb ?? mt;
|
|
||||||
});
|
|
||||||
manga.MangaTags = mergedTags.ToList();
|
|
||||||
IEnumerable<MangaTag> newTags = manga.MangaTags
|
|
||||||
.Where(mt => !context.Tags.Select(t => t.Tag).Contains(mt.Tag));
|
|
||||||
context.Tags.AddRange(newTags);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (authors is not null)
|
|
||||||
{
|
|
||||||
IEnumerable<Author> mergedAuthors = authors.Select(ma =>
|
|
||||||
{
|
|
||||||
Author? inDb = context.Authors.Find(ma.AuthorId);
|
|
||||||
return inDb ?? ma;
|
|
||||||
});
|
|
||||||
manga.Authors = mergedAuthors.ToList();
|
|
||||||
IEnumerable<Author> newAuthors = manga.Authors
|
|
||||||
.Where(ma => !context.Authors.Select(a => a.AuthorId).Contains(ma.AuthorId));
|
|
||||||
context.Authors.AddRange(newAuthors);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (links is not null)
|
|
||||||
{
|
|
||||||
IEnumerable<Link> mergedLinks = links.Select(ml =>
|
|
||||||
{
|
|
||||||
Link? inDb = context.Links.Find(ml.LinkId);
|
|
||||||
return inDb ?? ml;
|
|
||||||
});
|
|
||||||
manga.Links = mergedLinks.ToList();
|
|
||||||
IEnumerable<Link> newLinks = manga.Links
|
|
||||||
.Where(ml => !context.Links.Select(l => l.LinkId).Contains(ml.LinkId));
|
|
||||||
context.Links.AddRange(newLinks);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (altTitles is not null)
|
|
||||||
{
|
|
||||||
IEnumerable<MangaAltTitle> mergedAltTitles = altTitles.Select(mat =>
|
|
||||||
{
|
|
||||||
MangaAltTitle? inDb = context.AltTitles.Find(mat.AltTitleId);
|
|
||||||
return inDb ?? mat;
|
|
||||||
});
|
|
||||||
manga.AltTitles = mergedAltTitles.ToList();
|
|
||||||
IEnumerable<MangaAltTitle> newAltTitles = manga.AltTitles
|
|
||||||
.Where(mat => !context.AltTitles.Select(at => at.AltTitleId).Contains(mat.AltTitleId));
|
|
||||||
context.AltTitles.AddRange(newAltTitles);
|
|
||||||
}
|
|
||||||
|
|
||||||
existing?.UpdateWithInfo(manga);
|
|
||||||
if(existing is not null)
|
|
||||||
context.Mangas.Update(existing);
|
|
||||||
else
|
|
||||||
context.Mangas.Add(manga);
|
|
||||||
|
|
||||||
context.Jobs.Add(new DownloadMangaCoverJob(manga.MangaId));
|
|
||||||
context.Jobs.Add(new RetrieveChaptersJob(0, manga.MangaId));
|
|
||||||
|
|
||||||
context.SaveChanges();
|
|
||||||
return existing ?? manga;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,266 +0,0 @@
|
|||||||
using API.MangaDownloadClients;
|
|
||||||
using API.Schema;
|
|
||||||
using API.Schema.Jobs;
|
|
||||||
using Asp.Versioning;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
using Newtonsoft.Json.Linq;
|
|
||||||
using static Microsoft.AspNetCore.Http.StatusCodes;
|
|
||||||
|
|
||||||
namespace API.Controllers;
|
|
||||||
|
|
||||||
[ApiVersion(2)]
|
|
||||||
[ApiController]
|
|
||||||
[Route("v{v:apiVersion}/[controller]")]
|
|
||||||
public class SettingsController(PgsqlContext context) : Controller
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Get all Settings
|
|
||||||
/// </summary>
|
|
||||||
/// <response code="200"></response>
|
|
||||||
[HttpGet]
|
|
||||||
[ProducesResponseType<JObject>(Status200OK, "application/json")]
|
|
||||||
public IActionResult GetSettings()
|
|
||||||
{
|
|
||||||
return Ok(JObject.Parse(TrangaSettings.Serialize()));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Get the current UserAgent used by Tranga
|
|
||||||
/// </summary>
|
|
||||||
/// <response code="200"></response>
|
|
||||||
[HttpGet("UserAgent")]
|
|
||||||
[ProducesResponseType<string>(Status200OK, "text/plain")]
|
|
||||||
public IActionResult GetUserAgent()
|
|
||||||
{
|
|
||||||
return Ok(TrangaSettings.userAgent);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Set a new UserAgent
|
|
||||||
/// </summary>
|
|
||||||
/// <response code="200"></response>
|
|
||||||
[HttpPatch("UserAgent")]
|
|
||||||
[ProducesResponseType(Status200OK)]
|
|
||||||
public IActionResult SetUserAgent([FromBody]string userAgent)
|
|
||||||
{
|
|
||||||
TrangaSettings.UpdateUserAgent(userAgent);
|
|
||||||
return Ok();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Reset the UserAgent to default
|
|
||||||
/// </summary>
|
|
||||||
/// <response code="200"></response>
|
|
||||||
[HttpDelete("UserAgent")]
|
|
||||||
[ProducesResponseType(Status200OK)]
|
|
||||||
public IActionResult ResetUserAgent()
|
|
||||||
{
|
|
||||||
TrangaSettings.UpdateUserAgent(TrangaSettings.DefaultUserAgent);
|
|
||||||
return Ok();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Get all Request-Limits
|
|
||||||
/// </summary>
|
|
||||||
/// <response code="200"></response>
|
|
||||||
[HttpGet("RequestLimits")]
|
|
||||||
[ProducesResponseType<Dictionary<RequestType,int>>(Status200OK, "application/json")]
|
|
||||||
public IActionResult GetRequestLimits()
|
|
||||||
{
|
|
||||||
return Ok(TrangaSettings.requestLimits);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Update all Request-Limits to new values
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks><h1>NOT IMPLEMENTED</h1></remarks>
|
|
||||||
[HttpPatch("RequestLimits")]
|
|
||||||
[ProducesResponseType(Status501NotImplemented)]
|
|
||||||
public IActionResult SetRequestLimits()
|
|
||||||
{
|
|
||||||
return StatusCode(501);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Updates a Request-Limit value
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="RequestType">Type of Request</param>
|
|
||||||
/// <param name="requestLimit">New limit in Requests/Minute</param>
|
|
||||||
/// <response code="200"></response>
|
|
||||||
/// <response code="400">Limit needs to be greater than 0</response>
|
|
||||||
[HttpPatch("RequestLimits/{RequestType}")]
|
|
||||||
[ProducesResponseType(Status200OK)]
|
|
||||||
[ProducesResponseType(Status400BadRequest)]
|
|
||||||
public IActionResult SetRequestLimit(RequestType RequestType, [FromBody]int requestLimit)
|
|
||||||
{
|
|
||||||
if (requestLimit <= 0)
|
|
||||||
return BadRequest();
|
|
||||||
TrangaSettings.UpdateRequestLimit(RequestType, requestLimit);
|
|
||||||
return Ok();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Reset Request-Limit
|
|
||||||
/// </summary>
|
|
||||||
/// <response code="200"></response>
|
|
||||||
[HttpDelete("RequestLimits/{RequestType}")]
|
|
||||||
[ProducesResponseType<string>(Status200OK)]
|
|
||||||
public IActionResult ResetRequestLimits(RequestType RequestType)
|
|
||||||
{
|
|
||||||
TrangaSettings.UpdateRequestLimit(RequestType, TrangaSettings.DefaultRequestLimits[RequestType]);
|
|
||||||
return Ok();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Reset Request-Limit
|
|
||||||
/// </summary>
|
|
||||||
/// <response code="200"></response>
|
|
||||||
[HttpDelete("RequestLimits")]
|
|
||||||
[ProducesResponseType<string>(Status200OK)]
|
|
||||||
public IActionResult ResetRequestLimits()
|
|
||||||
{
|
|
||||||
TrangaSettings.ResetRequestLimits();
|
|
||||||
return Ok();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns Level of Image-Compression for Images
|
|
||||||
/// </summary>
|
|
||||||
/// <response code="200">JPEG compression-level as Integer</response>
|
|
||||||
[HttpGet("ImageCompression")]
|
|
||||||
[ProducesResponseType<int>(Status200OK, "text/plain")]
|
|
||||||
public IActionResult GetImageCompression()
|
|
||||||
{
|
|
||||||
return Ok(TrangaSettings.compression);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Set the Image-Compression-Level for Images
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="level">100 to disable, 0-99 for JPEG compression-Level</param>
|
|
||||||
/// <response code="200"></response>
|
|
||||||
/// <response code="400">Level outside permitted range</response>
|
|
||||||
[HttpPatch("ImageCompression")]
|
|
||||||
[ProducesResponseType(Status200OK)]
|
|
||||||
[ProducesResponseType(Status400BadRequest)]
|
|
||||||
public IActionResult SetImageCompression([FromBody]int level)
|
|
||||||
{
|
|
||||||
if (level < 1 || level > 100)
|
|
||||||
return BadRequest();
|
|
||||||
TrangaSettings.UpdateCompressImages(level);
|
|
||||||
return Ok();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Get state of Black/White-Image setting
|
|
||||||
/// </summary>
|
|
||||||
/// <response code="200">True if enabled</response>
|
|
||||||
[HttpGet("BWImages")]
|
|
||||||
[ProducesResponseType<bool>(Status200OK, "text/plain")]
|
|
||||||
public IActionResult GetBwImagesToggle()
|
|
||||||
{
|
|
||||||
return Ok(TrangaSettings.bwImages);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Enable/Disable conversion of Images to Black and White
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="enabled">true to enable</param>
|
|
||||||
/// <response code="200"></response>
|
|
||||||
[HttpPatch("BWImages")]
|
|
||||||
[ProducesResponseType(Status200OK)]
|
|
||||||
public IActionResult SetBwImagesToggle([FromBody]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);
|
|
||||||
return Ok();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the Chapter Naming Scheme
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// Placeholders:
|
|
||||||
/// %M Manga Name
|
|
||||||
/// %V Volume
|
|
||||||
/// %C Chapter
|
|
||||||
/// %T Title
|
|
||||||
/// %A Author (first in list)
|
|
||||||
/// %I Chapter Internal ID
|
|
||||||
/// %i Manga Internal ID
|
|
||||||
/// %Y Year (Manga)
|
|
||||||
///
|
|
||||||
/// ?_(...) 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>
|
|
||||||
[HttpGet("ChapterNamingScheme")]
|
|
||||||
[ProducesResponseType<string>(Status200OK, "text/plain")]
|
|
||||||
public IActionResult GetCustomNamingScheme()
|
|
||||||
{
|
|
||||||
return Ok(TrangaSettings.chapterNamingScheme);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Sets the Chapter Naming Scheme
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// Placeholders:
|
|
||||||
/// %M Manga Name
|
|
||||||
/// %V Volume
|
|
||||||
/// %C Chapter
|
|
||||||
/// %T Title
|
|
||||||
/// %A Author (first in list)
|
|
||||||
/// %I Chapter Internal ID
|
|
||||||
/// %i Manga Internal ID
|
|
||||||
/// %Y Year (Manga)
|
|
||||||
///
|
|
||||||
/// ?_(...) 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
|
|
||||||
{
|
|
||||||
TrangaSettings.UpdateChapterNamingScheme(namingScheme);
|
|
||||||
MoveFileOrFolderJob[] newJobs =
|
|
||||||
context.Chapters.Where(c => c.Downloaded).Select(c => c.UpdateArchiveFileName()).Where(x => x != null).ToArray()!;
|
|
||||||
context.Jobs.AddRange(newJobs);
|
|
||||||
return Ok();
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
return StatusCode(500, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,35 +0,0 @@
|
|||||||
namespace API;
|
|
||||||
|
|
||||||
public interface IHttpRequestTimeFeature
|
|
||||||
{
|
|
||||||
DateTime RequestTime { get; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class HttpRequestTimeFeature : IHttpRequestTimeFeature
|
|
||||||
{
|
|
||||||
public DateTime RequestTime { get; }
|
|
||||||
|
|
||||||
public HttpRequestTimeFeature()
|
|
||||||
{
|
|
||||||
RequestTime = DateTime.Now;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class RequestTimeMiddleware
|
|
||||||
{
|
|
||||||
private readonly RequestDelegate _next;
|
|
||||||
|
|
||||||
public RequestTimeMiddleware(RequestDelegate next)
|
|
||||||
{
|
|
||||||
_next = next;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task InvokeAsync(HttpContext context)
|
|
||||||
{
|
|
||||||
var httpRequestTimeFeature = new HttpRequestTimeFeature();
|
|
||||||
context.Features.Set<IHttpRequestTimeFeature>(httpRequestTimeFeature);
|
|
||||||
|
|
||||||
// Call the next delegate/middleware in the pipeline
|
|
||||||
return this._next(context);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,821 +0,0 @@
|
|||||||
// <auto-generated />
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using API.Schema;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
|
||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
|
||||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
|
||||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
namespace API.Migrations
|
|
||||||
{
|
|
||||||
[DbContext(typeof(PgsqlContext))]
|
|
||||||
[Migration("20250316143014_dev-160325-Initial")]
|
|
||||||
partial class dev160325Initial
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
|
||||||
{
|
|
||||||
#pragma warning disable 612, 618
|
|
||||||
modelBuilder
|
|
||||||
.HasAnnotation("ProductVersion", "9.0.3")
|
|
||||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
|
||||||
|
|
||||||
NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "hstore");
|
|
||||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.Author", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("AuthorId")
|
|
||||||
.HasMaxLength(64)
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.Property<string>("AuthorName")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(128)
|
|
||||||
.HasColumnType("character varying(128)");
|
|
||||||
|
|
||||||
b.HasKey("AuthorId");
|
|
||||||
|
|
||||||
b.ToTable("Authors");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.Chapter", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("ChapterId")
|
|
||||||
.HasMaxLength(64)
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.Property<string>("ChapterNumber")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(10)
|
|
||||||
.HasColumnType("character varying(10)");
|
|
||||||
|
|
||||||
b.Property<bool>("Downloaded")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<string>("FileName")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(256)
|
|
||||||
.HasColumnType("character varying(256)");
|
|
||||||
|
|
||||||
b.Property<string>("ParentMangaId")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(64)
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.Property<string>("Title")
|
|
||||||
.HasMaxLength(256)
|
|
||||||
.HasColumnType("character varying(256)");
|
|
||||||
|
|
||||||
b.Property<string>("Url")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(2048)
|
|
||||||
.HasColumnType("character varying(2048)");
|
|
||||||
|
|
||||||
b.Property<int?>("VolumeNumber")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.HasKey("ChapterId");
|
|
||||||
|
|
||||||
b.HasIndex("ParentMangaId");
|
|
||||||
|
|
||||||
b.ToTable("Chapters");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.Jobs.Job", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("JobId")
|
|
||||||
.HasMaxLength(64)
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.PrimitiveCollection<string[]>("DependsOnJobsIds")
|
|
||||||
.HasMaxLength(64)
|
|
||||||
.HasColumnType("text[]");
|
|
||||||
|
|
||||||
b.Property<bool>("Enabled")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<byte>("JobType")
|
|
||||||
.HasColumnType("smallint");
|
|
||||||
|
|
||||||
b.Property<DateTime>("LastExecution")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("ParentJobId")
|
|
||||||
.HasMaxLength(64)
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.Property<decimal>("RecurrenceMs")
|
|
||||||
.HasColumnType("numeric(20,0)");
|
|
||||||
|
|
||||||
b.Property<byte>("state")
|
|
||||||
.HasColumnType("smallint");
|
|
||||||
|
|
||||||
b.HasKey("JobId");
|
|
||||||
|
|
||||||
b.HasIndex("ParentJobId");
|
|
||||||
|
|
||||||
b.ToTable("Jobs");
|
|
||||||
|
|
||||||
b.HasDiscriminator<byte>("JobType");
|
|
||||||
|
|
||||||
b.UseTphMappingStrategy();
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.LibraryConnectors.LibraryConnector", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("LibraryConnectorId")
|
|
||||||
.HasMaxLength(64)
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.Property<string>("Auth")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(256)
|
|
||||||
.HasColumnType("character varying(256)");
|
|
||||||
|
|
||||||
b.Property<string>("BaseUrl")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(256)
|
|
||||||
.HasColumnType("character varying(256)");
|
|
||||||
|
|
||||||
b.Property<byte>("LibraryType")
|
|
||||||
.HasColumnType("smallint");
|
|
||||||
|
|
||||||
b.HasKey("LibraryConnectorId");
|
|
||||||
|
|
||||||
b.ToTable("LibraryConnectors");
|
|
||||||
|
|
||||||
b.HasDiscriminator<byte>("LibraryType");
|
|
||||||
|
|
||||||
b.UseTphMappingStrategy();
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.Link", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("LinkId")
|
|
||||||
.HasMaxLength(64)
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.Property<string>("LinkProvider")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(64)
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.Property<string>("LinkUrl")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(2048)
|
|
||||||
.HasColumnType("character varying(2048)");
|
|
||||||
|
|
||||||
b.Property<string>("MangaId")
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.HasKey("LinkId");
|
|
||||||
|
|
||||||
b.HasIndex("MangaId");
|
|
||||||
|
|
||||||
b.ToTable("Links");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.LocalLibrary", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("LocalLibraryId")
|
|
||||||
.HasMaxLength(64)
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.Property<string>("BasePath")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(256)
|
|
||||||
.HasColumnType("character varying(256)");
|
|
||||||
|
|
||||||
b.Property<string>("LibraryName")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(512)
|
|
||||||
.HasColumnType("character varying(512)");
|
|
||||||
|
|
||||||
b.HasKey("LocalLibraryId");
|
|
||||||
|
|
||||||
b.ToTable("LocalLibraries");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.Manga", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("MangaId")
|
|
||||||
.HasMaxLength(64)
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.Property<string>("CoverFileNameInCache")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("CoverUrl")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Description")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("DirectoryName")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(256)
|
|
||||||
.HasColumnType("character varying(256)");
|
|
||||||
|
|
||||||
b.Property<string>("IdOnConnectorSite")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(128)
|
|
||||||
.HasColumnType("character varying(128)");
|
|
||||||
|
|
||||||
b.Property<float>("IgnoreChapterBefore")
|
|
||||||
.HasColumnType("real");
|
|
||||||
|
|
||||||
b.Property<string>("LibraryLocalLibraryId")
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.Property<string>("MangaConnectorId")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(64)
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.Property<string>("Name")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(256)
|
|
||||||
.HasColumnType("character varying(256)");
|
|
||||||
|
|
||||||
b.Property<string>("OriginalLanguage")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(8)
|
|
||||||
.HasColumnType("character varying(8)");
|
|
||||||
|
|
||||||
b.Property<byte>("ReleaseStatus")
|
|
||||||
.HasColumnType("smallint");
|
|
||||||
|
|
||||||
b.Property<string>("WebsiteUrl")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(256)
|
|
||||||
.HasColumnType("character varying(256)");
|
|
||||||
|
|
||||||
b.Property<long>("Year")
|
|
||||||
.HasColumnType("bigint");
|
|
||||||
|
|
||||||
b.HasKey("MangaId");
|
|
||||||
|
|
||||||
b.HasIndex("LibraryLocalLibraryId");
|
|
||||||
|
|
||||||
b.HasIndex("MangaConnectorId");
|
|
||||||
|
|
||||||
b.ToTable("Mangas");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.MangaAltTitle", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("AltTitleId")
|
|
||||||
.HasMaxLength(64)
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.Property<string>("Language")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(8)
|
|
||||||
.HasColumnType("character varying(8)");
|
|
||||||
|
|
||||||
b.Property<string>("MangaId")
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.Property<string>("Title")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(256)
|
|
||||||
.HasColumnType("character varying(256)");
|
|
||||||
|
|
||||||
b.HasKey("AltTitleId");
|
|
||||||
|
|
||||||
b.HasIndex("MangaId");
|
|
||||||
|
|
||||||
b.ToTable("AltTitles");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.MangaConnectors.MangaConnector", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Name")
|
|
||||||
.HasMaxLength(32)
|
|
||||||
.HasColumnType("character varying(32)");
|
|
||||||
|
|
||||||
b.PrimitiveCollection<string[]>("BaseUris")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(256)
|
|
||||||
.HasColumnType("text[]");
|
|
||||||
|
|
||||||
b.Property<bool>("Enabled")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<string>("IconUrl")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(2048)
|
|
||||||
.HasColumnType("character varying(2048)");
|
|
||||||
|
|
||||||
b.PrimitiveCollection<string[]>("SupportedLanguages")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(8)
|
|
||||||
.HasColumnType("text[]");
|
|
||||||
|
|
||||||
b.HasKey("Name");
|
|
||||||
|
|
||||||
b.ToTable("MangaConnectors");
|
|
||||||
|
|
||||||
b.HasDiscriminator<string>("Name").HasValue("MangaConnector");
|
|
||||||
|
|
||||||
b.UseTphMappingStrategy();
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.MangaTag", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Tag")
|
|
||||||
.HasMaxLength(64)
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.HasKey("Tag");
|
|
||||||
|
|
||||||
b.ToTable("Tags");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.Notification", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("NotificationId")
|
|
||||||
.HasMaxLength(64)
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.Property<DateTime>("Date")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("Message")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(512)
|
|
||||||
.HasColumnType("character varying(512)");
|
|
||||||
|
|
||||||
b.Property<string>("Title")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(128)
|
|
||||||
.HasColumnType("character varying(128)");
|
|
||||||
|
|
||||||
b.Property<byte>("Urgency")
|
|
||||||
.HasColumnType("smallint");
|
|
||||||
|
|
||||||
b.HasKey("NotificationId");
|
|
||||||
|
|
||||||
b.ToTable("Notifications");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.NotificationConnectors.NotificationConnector", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Name")
|
|
||||||
.HasMaxLength(64)
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.Property<string>("Body")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(4096)
|
|
||||||
.HasColumnType("character varying(4096)");
|
|
||||||
|
|
||||||
b.Property<Dictionary<string, string>>("Headers")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("hstore");
|
|
||||||
|
|
||||||
b.Property<string>("HttpMethod")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(8)
|
|
||||||
.HasColumnType("character varying(8)");
|
|
||||||
|
|
||||||
b.Property<string>("Url")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(2048)
|
|
||||||
.HasColumnType("character varying(2048)");
|
|
||||||
|
|
||||||
b.HasKey("Name");
|
|
||||||
|
|
||||||
b.ToTable("NotificationConnectors");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("AuthorManga", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("AuthorsAuthorId")
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.Property<string>("MangaId")
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.HasKey("AuthorsAuthorId", "MangaId");
|
|
||||||
|
|
||||||
b.HasIndex("MangaId");
|
|
||||||
|
|
||||||
b.ToTable("AuthorManga");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("JobJob", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("DependsOnJobsJobId")
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.Property<string>("JobId")
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.HasKey("DependsOnJobsJobId", "JobId");
|
|
||||||
|
|
||||||
b.HasIndex("JobId");
|
|
||||||
|
|
||||||
b.ToTable("JobJob");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("MangaMangaTag", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("MangaId")
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.Property<string>("MangaTagsTag")
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.HasKey("MangaId", "MangaTagsTag");
|
|
||||||
|
|
||||||
b.HasIndex("MangaTagsTag");
|
|
||||||
|
|
||||||
b.ToTable("MangaMangaTag");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.Jobs.DownloadAvailableChaptersJob", b =>
|
|
||||||
{
|
|
||||||
b.HasBaseType("API.Schema.Jobs.Job");
|
|
||||||
|
|
||||||
b.Property<string>("MangaId")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(64)
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.HasIndex("MangaId");
|
|
||||||
|
|
||||||
b.ToTable("Jobs", t =>
|
|
||||||
{
|
|
||||||
t.Property("MangaId")
|
|
||||||
.HasColumnName("DownloadAvailableChaptersJob_MangaId");
|
|
||||||
});
|
|
||||||
|
|
||||||
b.HasDiscriminator().HasValue((byte)1);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.Jobs.DownloadMangaCoverJob", b =>
|
|
||||||
{
|
|
||||||
b.HasBaseType("API.Schema.Jobs.Job");
|
|
||||||
|
|
||||||
b.Property<string>("MangaId")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(64)
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.HasIndex("MangaId");
|
|
||||||
|
|
||||||
b.HasDiscriminator().HasValue((byte)4);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.Jobs.DownloadSingleChapterJob", b =>
|
|
||||||
{
|
|
||||||
b.HasBaseType("API.Schema.Jobs.Job");
|
|
||||||
|
|
||||||
b.Property<string>("ChapterId")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(64)
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.HasIndex("ChapterId");
|
|
||||||
|
|
||||||
b.HasDiscriminator().HasValue((byte)0);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.Jobs.MoveFileOrFolderJob", b =>
|
|
||||||
{
|
|
||||||
b.HasBaseType("API.Schema.Jobs.Job");
|
|
||||||
|
|
||||||
b.Property<string>("FromLocation")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(256)
|
|
||||||
.HasColumnType("character varying(256)");
|
|
||||||
|
|
||||||
b.Property<string>("ToLocation")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(256)
|
|
||||||
.HasColumnType("character varying(256)");
|
|
||||||
|
|
||||||
b.HasDiscriminator().HasValue((byte)3);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.Jobs.RetrieveChaptersJob", b =>
|
|
||||||
{
|
|
||||||
b.HasBaseType("API.Schema.Jobs.Job");
|
|
||||||
|
|
||||||
b.Property<string>("MangaId")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(64)
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.HasIndex("MangaId");
|
|
||||||
|
|
||||||
b.ToTable("Jobs", t =>
|
|
||||||
{
|
|
||||||
t.Property("MangaId")
|
|
||||||
.HasColumnName("RetrieveChaptersJob_MangaId");
|
|
||||||
});
|
|
||||||
|
|
||||||
b.HasDiscriminator().HasValue((byte)5);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.Jobs.UpdateFilesDownloadedJob", b =>
|
|
||||||
{
|
|
||||||
b.HasBaseType("API.Schema.Jobs.Job");
|
|
||||||
|
|
||||||
b.Property<string>("MangaId")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(64)
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.HasIndex("MangaId");
|
|
||||||
|
|
||||||
b.ToTable("Jobs", t =>
|
|
||||||
{
|
|
||||||
t.Property("MangaId")
|
|
||||||
.HasColumnName("UpdateFilesDownloadedJob_MangaId");
|
|
||||||
});
|
|
||||||
|
|
||||||
b.HasDiscriminator().HasValue((byte)6);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.Jobs.UpdateMetadataJob", b =>
|
|
||||||
{
|
|
||||||
b.HasBaseType("API.Schema.Jobs.Job");
|
|
||||||
|
|
||||||
b.Property<string>("MangaId")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(64)
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.HasIndex("MangaId");
|
|
||||||
|
|
||||||
b.ToTable("Jobs", t =>
|
|
||||||
{
|
|
||||||
t.Property("MangaId")
|
|
||||||
.HasColumnName("UpdateMetadataJob_MangaId");
|
|
||||||
});
|
|
||||||
|
|
||||||
b.HasDiscriminator().HasValue((byte)2);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.LibraryConnectors.Kavita", b =>
|
|
||||||
{
|
|
||||||
b.HasBaseType("API.Schema.LibraryConnectors.LibraryConnector");
|
|
||||||
|
|
||||||
b.HasDiscriminator().HasValue((byte)1);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.LibraryConnectors.Komga", b =>
|
|
||||||
{
|
|
||||||
b.HasBaseType("API.Schema.LibraryConnectors.LibraryConnector");
|
|
||||||
|
|
||||||
b.HasDiscriminator().HasValue((byte)0);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.MangaConnectors.AsuraToon", b =>
|
|
||||||
{
|
|
||||||
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
|
|
||||||
|
|
||||||
b.HasDiscriminator().HasValue("AsuraToon");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.MangaConnectors.Bato", b =>
|
|
||||||
{
|
|
||||||
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
|
|
||||||
|
|
||||||
b.HasDiscriminator().HasValue("Bato");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.MangaConnectors.MangaDex", b =>
|
|
||||||
{
|
|
||||||
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
|
|
||||||
|
|
||||||
b.HasDiscriminator().HasValue("MangaDex");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.MangaConnectors.MangaHere", b =>
|
|
||||||
{
|
|
||||||
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
|
|
||||||
|
|
||||||
b.HasDiscriminator().HasValue("MangaHere");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.MangaConnectors.MangaKatana", b =>
|
|
||||||
{
|
|
||||||
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
|
|
||||||
|
|
||||||
b.HasDiscriminator().HasValue("MangaKatana");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.MangaConnectors.Manganato", b =>
|
|
||||||
{
|
|
||||||
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
|
|
||||||
|
|
||||||
b.HasDiscriminator().HasValue("Manganato");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.MangaConnectors.Mangaworld", b =>
|
|
||||||
{
|
|
||||||
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
|
|
||||||
|
|
||||||
b.HasDiscriminator().HasValue("Mangaworld");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.MangaConnectors.ManhuaPlus", b =>
|
|
||||||
{
|
|
||||||
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
|
|
||||||
|
|
||||||
b.HasDiscriminator().HasValue("ManhuaPlus");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.MangaConnectors.Weebcentral", b =>
|
|
||||||
{
|
|
||||||
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
|
|
||||||
|
|
||||||
b.HasDiscriminator().HasValue("Weebcentral");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.Chapter", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("API.Schema.Manga", "ParentManga")
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("ParentMangaId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.Navigation("ParentManga");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.Jobs.Job", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("API.Schema.Jobs.Job", "ParentJob")
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("ParentJobId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade);
|
|
||||||
|
|
||||||
b.Navigation("ParentJob");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.Link", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("API.Schema.Manga", null)
|
|
||||||
.WithMany("Links")
|
|
||||||
.HasForeignKey("MangaId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.Manga", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("API.Schema.LocalLibrary", "Library")
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("LibraryLocalLibraryId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade);
|
|
||||||
|
|
||||||
b.HasOne("API.Schema.MangaConnectors.MangaConnector", "MangaConnector")
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("MangaConnectorId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.Navigation("Library");
|
|
||||||
|
|
||||||
b.Navigation("MangaConnector");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.MangaAltTitle", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("API.Schema.Manga", null)
|
|
||||||
.WithMany("AltTitles")
|
|
||||||
.HasForeignKey("MangaId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("AuthorManga", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("API.Schema.Author", null)
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("AuthorsAuthorId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.HasOne("API.Schema.Manga", null)
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("MangaId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("JobJob", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("API.Schema.Jobs.Job", null)
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("DependsOnJobsJobId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.HasOne("API.Schema.Jobs.Job", null)
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("JobId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("MangaMangaTag", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("API.Schema.Manga", null)
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("MangaId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.HasOne("API.Schema.MangaTag", null)
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("MangaTagsTag")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.Jobs.DownloadAvailableChaptersJob", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("API.Schema.Manga", "Manga")
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("MangaId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.Navigation("Manga");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.Jobs.DownloadMangaCoverJob", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("API.Schema.Manga", "Manga")
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("MangaId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.Navigation("Manga");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.Jobs.DownloadSingleChapterJob", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("API.Schema.Chapter", "Chapter")
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("ChapterId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.Navigation("Chapter");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.Jobs.RetrieveChaptersJob", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("API.Schema.Manga", "Manga")
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("MangaId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.Navigation("Manga");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.Jobs.UpdateFilesDownloadedJob", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("API.Schema.Manga", "Manga")
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("MangaId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.Navigation("Manga");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.Jobs.UpdateMetadataJob", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("API.Schema.Manga", "Manga")
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("MangaId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.Navigation("Manga");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.Manga", b =>
|
|
||||||
{
|
|
||||||
b.Navigation("AltTitles");
|
|
||||||
|
|
||||||
b.Navigation("Links");
|
|
||||||
});
|
|
||||||
#pragma warning restore 612, 618
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,478 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
namespace API.Migrations
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
public partial class dev160325Initial : Migration
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Up(MigrationBuilder migrationBuilder)
|
|
||||||
{
|
|
||||||
migrationBuilder.AlterDatabase()
|
|
||||||
.Annotation("Npgsql:PostgresExtension:hstore", ",,");
|
|
||||||
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "Authors",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
AuthorId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
|
|
||||||
AuthorName = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: false)
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("PK_Authors", x => x.AuthorId);
|
|
||||||
});
|
|
||||||
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "LibraryConnectors",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
LibraryConnectorId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
|
|
||||||
LibraryType = table.Column<byte>(type: "smallint", nullable: false),
|
|
||||||
BaseUrl = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
|
|
||||||
Auth = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false)
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("PK_LibraryConnectors", x => x.LibraryConnectorId);
|
|
||||||
});
|
|
||||||
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "LocalLibraries",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
LocalLibraryId = table.Column<string>(type: "character varying(64)", maxLength: 64, 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);
|
|
||||||
});
|
|
||||||
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "MangaConnectors",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
Name = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: false),
|
|
||||||
SupportedLanguages = table.Column<string[]>(type: "text[]", maxLength: 8, nullable: false),
|
|
||||||
IconUrl = table.Column<string>(type: "character varying(2048)", maxLength: 2048, nullable: false),
|
|
||||||
BaseUris = table.Column<string[]>(type: "text[]", maxLength: 256, nullable: false),
|
|
||||||
Enabled = table.Column<bool>(type: "boolean", nullable: false)
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("PK_MangaConnectors", x => x.Name);
|
|
||||||
});
|
|
||||||
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "NotificationConnectors",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
Name = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
|
|
||||||
Url = table.Column<string>(type: "character varying(2048)", maxLength: 2048, nullable: false),
|
|
||||||
Headers = table.Column<Dictionary<string, string>>(type: "hstore", nullable: false),
|
|
||||||
HttpMethod = table.Column<string>(type: "character varying(8)", maxLength: 8, nullable: false),
|
|
||||||
Body = table.Column<string>(type: "character varying(4096)", maxLength: 4096, nullable: false)
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("PK_NotificationConnectors", x => x.Name);
|
|
||||||
});
|
|
||||||
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "Notifications",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
NotificationId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
|
|
||||||
Urgency = table.Column<byte>(type: "smallint", nullable: false),
|
|
||||||
Title = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: false),
|
|
||||||
Message = table.Column<string>(type: "character varying(512)", maxLength: 512, nullable: false),
|
|
||||||
Date = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("PK_Notifications", x => x.NotificationId);
|
|
||||||
});
|
|
||||||
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "Tags",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
Tag = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false)
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("PK_Tags", x => x.Tag);
|
|
||||||
});
|
|
||||||
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "Mangas",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
MangaId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
|
|
||||||
IdOnConnectorSite = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: false),
|
|
||||||
Name = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
|
|
||||||
Description = table.Column<string>(type: "text", nullable: false),
|
|
||||||
WebsiteUrl = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
|
|
||||||
CoverUrl = table.Column<string>(type: "text", nullable: false),
|
|
||||||
CoverFileNameInCache = table.Column<string>(type: "text", nullable: true),
|
|
||||||
Year = table.Column<long>(type: "bigint", nullable: false),
|
|
||||||
OriginalLanguage = table.Column<string>(type: "character varying(8)", maxLength: 8, nullable: false),
|
|
||||||
ReleaseStatus = table.Column<byte>(type: "smallint", nullable: false),
|
|
||||||
DirectoryName = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
|
|
||||||
LibraryLocalLibraryId = table.Column<string>(type: "character varying(64)", nullable: true),
|
|
||||||
IgnoreChapterBefore = table.Column<float>(type: "real", nullable: false),
|
|
||||||
MangaConnectorId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false)
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("PK_Mangas", x => x.MangaId);
|
|
||||||
table.ForeignKey(
|
|
||||||
name: "FK_Mangas_LocalLibraries_LibraryLocalLibraryId",
|
|
||||||
column: x => x.LibraryLocalLibraryId,
|
|
||||||
principalTable: "LocalLibraries",
|
|
||||||
principalColumn: "LocalLibraryId",
|
|
||||||
onDelete: ReferentialAction.Cascade);
|
|
||||||
table.ForeignKey(
|
|
||||||
name: "FK_Mangas_MangaConnectors_MangaConnectorId",
|
|
||||||
column: x => x.MangaConnectorId,
|
|
||||||
principalTable: "MangaConnectors",
|
|
||||||
principalColumn: "Name",
|
|
||||||
onDelete: ReferentialAction.Cascade);
|
|
||||||
});
|
|
||||||
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "AltTitles",
|
|
||||||
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: true)
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("PK_AltTitles", x => x.AltTitleId);
|
|
||||||
table.ForeignKey(
|
|
||||||
name: "FK_AltTitles_Mangas_MangaId",
|
|
||||||
column: x => x.MangaId,
|
|
||||||
principalTable: "Mangas",
|
|
||||||
principalColumn: "MangaId",
|
|
||||||
onDelete: ReferentialAction.Cascade);
|
|
||||||
});
|
|
||||||
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "AuthorManga",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
AuthorsAuthorId = table.Column<string>(type: "character varying(64)", nullable: false),
|
|
||||||
MangaId = table.Column<string>(type: "character varying(64)", nullable: false)
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("PK_AuthorManga", x => new { x.AuthorsAuthorId, x.MangaId });
|
|
||||||
table.ForeignKey(
|
|
||||||
name: "FK_AuthorManga_Authors_AuthorsAuthorId",
|
|
||||||
column: x => x.AuthorsAuthorId,
|
|
||||||
principalTable: "Authors",
|
|
||||||
principalColumn: "AuthorId",
|
|
||||||
onDelete: ReferentialAction.Cascade);
|
|
||||||
table.ForeignKey(
|
|
||||||
name: "FK_AuthorManga_Mangas_MangaId",
|
|
||||||
column: x => x.MangaId,
|
|
||||||
principalTable: "Mangas",
|
|
||||||
principalColumn: "MangaId",
|
|
||||||
onDelete: ReferentialAction.Cascade);
|
|
||||||
});
|
|
||||||
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "Chapters",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
ChapterId = 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),
|
|
||||||
ParentMangaId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false)
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("PK_Chapters", x => x.ChapterId);
|
|
||||||
table.ForeignKey(
|
|
||||||
name: "FK_Chapters_Mangas_ParentMangaId",
|
|
||||||
column: x => x.ParentMangaId,
|
|
||||||
principalTable: "Mangas",
|
|
||||||
principalColumn: "MangaId",
|
|
||||||
onDelete: ReferentialAction.Cascade);
|
|
||||||
});
|
|
||||||
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "Links",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
LinkId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
|
|
||||||
LinkProvider = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
|
|
||||||
LinkUrl = table.Column<string>(type: "character varying(2048)", maxLength: 2048, nullable: false),
|
|
||||||
MangaId = table.Column<string>(type: "character varying(64)", nullable: true)
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("PK_Links", x => x.LinkId);
|
|
||||||
table.ForeignKey(
|
|
||||||
name: "FK_Links_Mangas_MangaId",
|
|
||||||
column: x => x.MangaId,
|
|
||||||
principalTable: "Mangas",
|
|
||||||
principalColumn: "MangaId",
|
|
||||||
onDelete: ReferentialAction.Cascade);
|
|
||||||
});
|
|
||||||
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "MangaMangaTag",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
MangaId = table.Column<string>(type: "character varying(64)", nullable: false),
|
|
||||||
MangaTagsTag = table.Column<string>(type: "character varying(64)", nullable: false)
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("PK_MangaMangaTag", x => new { x.MangaId, x.MangaTagsTag });
|
|
||||||
table.ForeignKey(
|
|
||||||
name: "FK_MangaMangaTag_Mangas_MangaId",
|
|
||||||
column: x => x.MangaId,
|
|
||||||
principalTable: "Mangas",
|
|
||||||
principalColumn: "MangaId",
|
|
||||||
onDelete: ReferentialAction.Cascade);
|
|
||||||
table.ForeignKey(
|
|
||||||
name: "FK_MangaMangaTag_Tags_MangaTagsTag",
|
|
||||||
column: x => x.MangaTagsTag,
|
|
||||||
principalTable: "Tags",
|
|
||||||
principalColumn: "Tag",
|
|
||||||
onDelete: ReferentialAction.Cascade);
|
|
||||||
});
|
|
||||||
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "Jobs",
|
|
||||||
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),
|
|
||||||
DependsOnJobsIds = table.Column<string[]>(type: "text[]", maxLength: 64, nullable: true),
|
|
||||||
JobType = table.Column<byte>(type: "smallint", nullable: false),
|
|
||||||
RecurrenceMs = table.Column<decimal>(type: "numeric(20,0)", nullable: false),
|
|
||||||
LastExecution = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
|
||||||
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),
|
|
||||||
RetrieveChaptersJob_MangaId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: true),
|
|
||||||
UpdateFilesDownloadedJob_MangaId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: true),
|
|
||||||
UpdateMetadataJob_MangaId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: true)
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("PK_Jobs", x => x.JobId);
|
|
||||||
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_Mangas_DownloadAvailableChaptersJob_MangaId",
|
|
||||||
column: x => x.DownloadAvailableChaptersJob_MangaId,
|
|
||||||
principalTable: "Mangas",
|
|
||||||
principalColumn: "MangaId",
|
|
||||||
onDelete: ReferentialAction.Cascade);
|
|
||||||
table.ForeignKey(
|
|
||||||
name: "FK_Jobs_Mangas_MangaId",
|
|
||||||
column: x => x.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",
|
|
||||||
onDelete: ReferentialAction.Cascade);
|
|
||||||
table.ForeignKey(
|
|
||||||
name: "FK_Jobs_Mangas_UpdateMetadataJob_MangaId",
|
|
||||||
column: x => x.UpdateMetadataJob_MangaId,
|
|
||||||
principalTable: "Mangas",
|
|
||||||
principalColumn: "MangaId",
|
|
||||||
onDelete: ReferentialAction.Cascade);
|
|
||||||
});
|
|
||||||
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "JobJob",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
DependsOnJobsJobId = table.Column<string>(type: "character varying(64)", nullable: false),
|
|
||||||
JobId = table.Column<string>(type: "character varying(64)", nullable: false)
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("PK_JobJob", x => new { x.DependsOnJobsJobId, x.JobId });
|
|
||||||
table.ForeignKey(
|
|
||||||
name: "FK_JobJob_Jobs_DependsOnJobsJobId",
|
|
||||||
column: x => x.DependsOnJobsJobId,
|
|
||||||
principalTable: "Jobs",
|
|
||||||
principalColumn: "JobId",
|
|
||||||
onDelete: ReferentialAction.Cascade);
|
|
||||||
table.ForeignKey(
|
|
||||||
name: "FK_JobJob_Jobs_JobId",
|
|
||||||
column: x => x.JobId,
|
|
||||||
principalTable: "Jobs",
|
|
||||||
principalColumn: "JobId",
|
|
||||||
onDelete: ReferentialAction.Cascade);
|
|
||||||
});
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_AltTitles_MangaId",
|
|
||||||
table: "AltTitles",
|
|
||||||
column: "MangaId");
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_AuthorManga_MangaId",
|
|
||||||
table: "AuthorManga",
|
|
||||||
column: "MangaId");
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_Chapters_ParentMangaId",
|
|
||||||
table: "Chapters",
|
|
||||||
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_ParentJobId",
|
|
||||||
table: "Jobs",
|
|
||||||
column: "ParentJobId");
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_Jobs_RetrieveChaptersJob_MangaId",
|
|
||||||
table: "Jobs",
|
|
||||||
column: "RetrieveChaptersJob_MangaId");
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_Jobs_UpdateFilesDownloadedJob_MangaId",
|
|
||||||
table: "Jobs",
|
|
||||||
column: "UpdateFilesDownloadedJob_MangaId");
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_Jobs_UpdateMetadataJob_MangaId",
|
|
||||||
table: "Jobs",
|
|
||||||
column: "UpdateMetadataJob_MangaId");
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_Links_MangaId",
|
|
||||||
table: "Links",
|
|
||||||
column: "MangaId");
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_MangaMangaTag_MangaTagsTag",
|
|
||||||
table: "MangaMangaTag",
|
|
||||||
column: "MangaTagsTag");
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_Mangas_LibraryLocalLibraryId",
|
|
||||||
table: "Mangas",
|
|
||||||
column: "LibraryLocalLibraryId");
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_Mangas_MangaConnectorId",
|
|
||||||
table: "Mangas",
|
|
||||||
column: "MangaConnectorId");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Down(MigrationBuilder migrationBuilder)
|
|
||||||
{
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "AltTitles");
|
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "AuthorManga");
|
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "JobJob");
|
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "LibraryConnectors");
|
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "Links");
|
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "MangaMangaTag");
|
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "NotificationConnectors");
|
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "Notifications");
|
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "Authors");
|
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "Jobs");
|
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "Tags");
|
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "Chapters");
|
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "Mangas");
|
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "LocalLibraries");
|
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "MangaConnectors");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
821
API/Migrations/20250316150158_dev-160325-2.Designer.cs
generated
821
API/Migrations/20250316150158_dev-160325-2.Designer.cs
generated
@ -1,821 +0,0 @@
|
|||||||
// <auto-generated />
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using API.Schema;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
|
||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
|
||||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
|
||||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
namespace API.Migrations
|
|
||||||
{
|
|
||||||
[DbContext(typeof(PgsqlContext))]
|
|
||||||
[Migration("20250316150158_dev-160325-2")]
|
|
||||||
partial class dev1603252
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
|
||||||
{
|
|
||||||
#pragma warning disable 612, 618
|
|
||||||
modelBuilder
|
|
||||||
.HasAnnotation("ProductVersion", "9.0.3")
|
|
||||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
|
||||||
|
|
||||||
NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "hstore");
|
|
||||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.Author", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("AuthorId")
|
|
||||||
.HasMaxLength(64)
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.Property<string>("AuthorName")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(128)
|
|
||||||
.HasColumnType("character varying(128)");
|
|
||||||
|
|
||||||
b.HasKey("AuthorId");
|
|
||||||
|
|
||||||
b.ToTable("Authors");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.Chapter", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("ChapterId")
|
|
||||||
.HasMaxLength(64)
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.Property<string>("ChapterNumber")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(10)
|
|
||||||
.HasColumnType("character varying(10)");
|
|
||||||
|
|
||||||
b.Property<bool>("Downloaded")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<string>("FileName")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(256)
|
|
||||||
.HasColumnType("character varying(256)");
|
|
||||||
|
|
||||||
b.Property<string>("ParentMangaId")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(64)
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.Property<string>("Title")
|
|
||||||
.HasMaxLength(256)
|
|
||||||
.HasColumnType("character varying(256)");
|
|
||||||
|
|
||||||
b.Property<string>("Url")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(2048)
|
|
||||||
.HasColumnType("character varying(2048)");
|
|
||||||
|
|
||||||
b.Property<int?>("VolumeNumber")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.HasKey("ChapterId");
|
|
||||||
|
|
||||||
b.HasIndex("ParentMangaId");
|
|
||||||
|
|
||||||
b.ToTable("Chapters");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.Jobs.Job", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("JobId")
|
|
||||||
.HasMaxLength(64)
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.PrimitiveCollection<string[]>("DependsOnJobsIds")
|
|
||||||
.HasMaxLength(64)
|
|
||||||
.HasColumnType("text[]");
|
|
||||||
|
|
||||||
b.Property<bool>("Enabled")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<byte>("JobType")
|
|
||||||
.HasColumnType("smallint");
|
|
||||||
|
|
||||||
b.Property<DateTime>("LastExecution")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("ParentJobId")
|
|
||||||
.HasMaxLength(64)
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.Property<decimal>("RecurrenceMs")
|
|
||||||
.HasColumnType("numeric(20,0)");
|
|
||||||
|
|
||||||
b.Property<byte>("state")
|
|
||||||
.HasColumnType("smallint");
|
|
||||||
|
|
||||||
b.HasKey("JobId");
|
|
||||||
|
|
||||||
b.HasIndex("ParentJobId");
|
|
||||||
|
|
||||||
b.ToTable("Jobs");
|
|
||||||
|
|
||||||
b.HasDiscriminator<byte>("JobType");
|
|
||||||
|
|
||||||
b.UseTphMappingStrategy();
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.LibraryConnectors.LibraryConnector", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("LibraryConnectorId")
|
|
||||||
.HasMaxLength(64)
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.Property<string>("Auth")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(256)
|
|
||||||
.HasColumnType("character varying(256)");
|
|
||||||
|
|
||||||
b.Property<string>("BaseUrl")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(256)
|
|
||||||
.HasColumnType("character varying(256)");
|
|
||||||
|
|
||||||
b.Property<byte>("LibraryType")
|
|
||||||
.HasColumnType("smallint");
|
|
||||||
|
|
||||||
b.HasKey("LibraryConnectorId");
|
|
||||||
|
|
||||||
b.ToTable("LibraryConnectors");
|
|
||||||
|
|
||||||
b.HasDiscriminator<byte>("LibraryType");
|
|
||||||
|
|
||||||
b.UseTphMappingStrategy();
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.Link", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("LinkId")
|
|
||||||
.HasMaxLength(64)
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.Property<string>("LinkProvider")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(64)
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.Property<string>("LinkUrl")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(2048)
|
|
||||||
.HasColumnType("character varying(2048)");
|
|
||||||
|
|
||||||
b.Property<string>("MangaId")
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.HasKey("LinkId");
|
|
||||||
|
|
||||||
b.HasIndex("MangaId");
|
|
||||||
|
|
||||||
b.ToTable("Links");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.LocalLibrary", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("LocalLibraryId")
|
|
||||||
.HasMaxLength(64)
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.Property<string>("BasePath")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(256)
|
|
||||||
.HasColumnType("character varying(256)");
|
|
||||||
|
|
||||||
b.Property<string>("LibraryName")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(512)
|
|
||||||
.HasColumnType("character varying(512)");
|
|
||||||
|
|
||||||
b.HasKey("LocalLibraryId");
|
|
||||||
|
|
||||||
b.ToTable("LocalLibraries");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.Manga", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("MangaId")
|
|
||||||
.HasMaxLength(64)
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.Property<string>("CoverFileNameInCache")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("CoverUrl")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Description")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("DirectoryName")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(256)
|
|
||||||
.HasColumnType("character varying(256)");
|
|
||||||
|
|
||||||
b.Property<string>("IdOnConnectorSite")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(128)
|
|
||||||
.HasColumnType("character varying(128)");
|
|
||||||
|
|
||||||
b.Property<float>("IgnoreChapterBefore")
|
|
||||||
.HasColumnType("real");
|
|
||||||
|
|
||||||
b.Property<string>("LibraryLocalLibraryId")
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.Property<string>("MangaConnectorId")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(64)
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.Property<string>("Name")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(256)
|
|
||||||
.HasColumnType("character varying(256)");
|
|
||||||
|
|
||||||
b.Property<string>("OriginalLanguage")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(8)
|
|
||||||
.HasColumnType("character varying(8)");
|
|
||||||
|
|
||||||
b.Property<byte>("ReleaseStatus")
|
|
||||||
.HasColumnType("smallint");
|
|
||||||
|
|
||||||
b.Property<string>("WebsiteUrl")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(256)
|
|
||||||
.HasColumnType("character varying(256)");
|
|
||||||
|
|
||||||
b.Property<long>("Year")
|
|
||||||
.HasColumnType("bigint");
|
|
||||||
|
|
||||||
b.HasKey("MangaId");
|
|
||||||
|
|
||||||
b.HasIndex("LibraryLocalLibraryId");
|
|
||||||
|
|
||||||
b.HasIndex("MangaConnectorId");
|
|
||||||
|
|
||||||
b.ToTable("Mangas");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.MangaAltTitle", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("AltTitleId")
|
|
||||||
.HasMaxLength(64)
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.Property<string>("Language")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(8)
|
|
||||||
.HasColumnType("character varying(8)");
|
|
||||||
|
|
||||||
b.Property<string>("MangaId")
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.Property<string>("Title")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(256)
|
|
||||||
.HasColumnType("character varying(256)");
|
|
||||||
|
|
||||||
b.HasKey("AltTitleId");
|
|
||||||
|
|
||||||
b.HasIndex("MangaId");
|
|
||||||
|
|
||||||
b.ToTable("AltTitles");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.MangaConnectors.MangaConnector", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Name")
|
|
||||||
.HasMaxLength(32)
|
|
||||||
.HasColumnType("character varying(32)");
|
|
||||||
|
|
||||||
b.PrimitiveCollection<string[]>("BaseUris")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(256)
|
|
||||||
.HasColumnType("text[]");
|
|
||||||
|
|
||||||
b.Property<bool>("Enabled")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<string>("IconUrl")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(2048)
|
|
||||||
.HasColumnType("character varying(2048)");
|
|
||||||
|
|
||||||
b.PrimitiveCollection<string[]>("SupportedLanguages")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(8)
|
|
||||||
.HasColumnType("text[]");
|
|
||||||
|
|
||||||
b.HasKey("Name");
|
|
||||||
|
|
||||||
b.ToTable("MangaConnectors");
|
|
||||||
|
|
||||||
b.HasDiscriminator<string>("Name").HasValue("MangaConnector");
|
|
||||||
|
|
||||||
b.UseTphMappingStrategy();
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.MangaTag", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Tag")
|
|
||||||
.HasMaxLength(64)
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.HasKey("Tag");
|
|
||||||
|
|
||||||
b.ToTable("Tags");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.Notification", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("NotificationId")
|
|
||||||
.HasMaxLength(64)
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.Property<DateTime>("Date")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("Message")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(512)
|
|
||||||
.HasColumnType("character varying(512)");
|
|
||||||
|
|
||||||
b.Property<string>("Title")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(128)
|
|
||||||
.HasColumnType("character varying(128)");
|
|
||||||
|
|
||||||
b.Property<byte>("Urgency")
|
|
||||||
.HasColumnType("smallint");
|
|
||||||
|
|
||||||
b.HasKey("NotificationId");
|
|
||||||
|
|
||||||
b.ToTable("Notifications");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.NotificationConnectors.NotificationConnector", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Name")
|
|
||||||
.HasMaxLength(64)
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.Property<string>("Body")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(4096)
|
|
||||||
.HasColumnType("character varying(4096)");
|
|
||||||
|
|
||||||
b.Property<Dictionary<string, string>>("Headers")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("hstore");
|
|
||||||
|
|
||||||
b.Property<string>("HttpMethod")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(8)
|
|
||||||
.HasColumnType("character varying(8)");
|
|
||||||
|
|
||||||
b.Property<string>("Url")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(2048)
|
|
||||||
.HasColumnType("character varying(2048)");
|
|
||||||
|
|
||||||
b.HasKey("Name");
|
|
||||||
|
|
||||||
b.ToTable("NotificationConnectors");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("AuthorManga", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("AuthorsAuthorId")
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.Property<string>("MangaId")
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.HasKey("AuthorsAuthorId", "MangaId");
|
|
||||||
|
|
||||||
b.HasIndex("MangaId");
|
|
||||||
|
|
||||||
b.ToTable("AuthorManga");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("JobJob", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("DependsOnJobsJobId")
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.Property<string>("JobId")
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.HasKey("DependsOnJobsJobId", "JobId");
|
|
||||||
|
|
||||||
b.HasIndex("JobId");
|
|
||||||
|
|
||||||
b.ToTable("JobJob");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("MangaMangaTag", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("MangaId")
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.Property<string>("MangaTagsTag")
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.HasKey("MangaId", "MangaTagsTag");
|
|
||||||
|
|
||||||
b.HasIndex("MangaTagsTag");
|
|
||||||
|
|
||||||
b.ToTable("MangaMangaTag");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.Jobs.DownloadAvailableChaptersJob", b =>
|
|
||||||
{
|
|
||||||
b.HasBaseType("API.Schema.Jobs.Job");
|
|
||||||
|
|
||||||
b.Property<string>("MangaId")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(64)
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.HasIndex("MangaId");
|
|
||||||
|
|
||||||
b.ToTable("Jobs", t =>
|
|
||||||
{
|
|
||||||
t.Property("MangaId")
|
|
||||||
.HasColumnName("DownloadAvailableChaptersJob_MangaId");
|
|
||||||
});
|
|
||||||
|
|
||||||
b.HasDiscriminator().HasValue((byte)1);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.Jobs.DownloadMangaCoverJob", b =>
|
|
||||||
{
|
|
||||||
b.HasBaseType("API.Schema.Jobs.Job");
|
|
||||||
|
|
||||||
b.Property<string>("MangaId")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(64)
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.HasIndex("MangaId");
|
|
||||||
|
|
||||||
b.HasDiscriminator().HasValue((byte)4);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.Jobs.DownloadSingleChapterJob", b =>
|
|
||||||
{
|
|
||||||
b.HasBaseType("API.Schema.Jobs.Job");
|
|
||||||
|
|
||||||
b.Property<string>("ChapterId")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(64)
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.HasIndex("ChapterId");
|
|
||||||
|
|
||||||
b.HasDiscriminator().HasValue((byte)0);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.Jobs.MoveFileOrFolderJob", b =>
|
|
||||||
{
|
|
||||||
b.HasBaseType("API.Schema.Jobs.Job");
|
|
||||||
|
|
||||||
b.Property<string>("FromLocation")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(256)
|
|
||||||
.HasColumnType("character varying(256)");
|
|
||||||
|
|
||||||
b.Property<string>("ToLocation")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(256)
|
|
||||||
.HasColumnType("character varying(256)");
|
|
||||||
|
|
||||||
b.HasDiscriminator().HasValue((byte)3);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.Jobs.RetrieveChaptersJob", b =>
|
|
||||||
{
|
|
||||||
b.HasBaseType("API.Schema.Jobs.Job");
|
|
||||||
|
|
||||||
b.Property<string>("MangaId")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(64)
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.HasIndex("MangaId");
|
|
||||||
|
|
||||||
b.ToTable("Jobs", t =>
|
|
||||||
{
|
|
||||||
t.Property("MangaId")
|
|
||||||
.HasColumnName("RetrieveChaptersJob_MangaId");
|
|
||||||
});
|
|
||||||
|
|
||||||
b.HasDiscriminator().HasValue((byte)5);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.Jobs.UpdateFilesDownloadedJob", b =>
|
|
||||||
{
|
|
||||||
b.HasBaseType("API.Schema.Jobs.Job");
|
|
||||||
|
|
||||||
b.Property<string>("MangaId")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(64)
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.HasIndex("MangaId");
|
|
||||||
|
|
||||||
b.ToTable("Jobs", t =>
|
|
||||||
{
|
|
||||||
t.Property("MangaId")
|
|
||||||
.HasColumnName("UpdateFilesDownloadedJob_MangaId");
|
|
||||||
});
|
|
||||||
|
|
||||||
b.HasDiscriminator().HasValue((byte)6);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.Jobs.UpdateMetadataJob", b =>
|
|
||||||
{
|
|
||||||
b.HasBaseType("API.Schema.Jobs.Job");
|
|
||||||
|
|
||||||
b.Property<string>("MangaId")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(64)
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.HasIndex("MangaId");
|
|
||||||
|
|
||||||
b.ToTable("Jobs", t =>
|
|
||||||
{
|
|
||||||
t.Property("MangaId")
|
|
||||||
.HasColumnName("UpdateMetadataJob_MangaId");
|
|
||||||
});
|
|
||||||
|
|
||||||
b.HasDiscriminator().HasValue((byte)2);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.LibraryConnectors.Kavita", b =>
|
|
||||||
{
|
|
||||||
b.HasBaseType("API.Schema.LibraryConnectors.LibraryConnector");
|
|
||||||
|
|
||||||
b.HasDiscriminator().HasValue((byte)1);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.LibraryConnectors.Komga", b =>
|
|
||||||
{
|
|
||||||
b.HasBaseType("API.Schema.LibraryConnectors.LibraryConnector");
|
|
||||||
|
|
||||||
b.HasDiscriminator().HasValue((byte)0);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.MangaConnectors.AsuraToon", b =>
|
|
||||||
{
|
|
||||||
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
|
|
||||||
|
|
||||||
b.HasDiscriminator().HasValue("AsuraToon");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.MangaConnectors.Bato", b =>
|
|
||||||
{
|
|
||||||
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
|
|
||||||
|
|
||||||
b.HasDiscriminator().HasValue("Bato");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.MangaConnectors.MangaDex", b =>
|
|
||||||
{
|
|
||||||
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
|
|
||||||
|
|
||||||
b.HasDiscriminator().HasValue("MangaDex");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.MangaConnectors.MangaHere", b =>
|
|
||||||
{
|
|
||||||
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
|
|
||||||
|
|
||||||
b.HasDiscriminator().HasValue("MangaHere");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.MangaConnectors.MangaKatana", b =>
|
|
||||||
{
|
|
||||||
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
|
|
||||||
|
|
||||||
b.HasDiscriminator().HasValue("MangaKatana");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.MangaConnectors.Manganato", b =>
|
|
||||||
{
|
|
||||||
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
|
|
||||||
|
|
||||||
b.HasDiscriminator().HasValue("Manganato");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.MangaConnectors.Mangaworld", b =>
|
|
||||||
{
|
|
||||||
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
|
|
||||||
|
|
||||||
b.HasDiscriminator().HasValue("Mangaworld");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.MangaConnectors.ManhuaPlus", b =>
|
|
||||||
{
|
|
||||||
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
|
|
||||||
|
|
||||||
b.HasDiscriminator().HasValue("ManhuaPlus");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.MangaConnectors.Weebcentral", b =>
|
|
||||||
{
|
|
||||||
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
|
|
||||||
|
|
||||||
b.HasDiscriminator().HasValue("Weebcentral");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.Chapter", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("API.Schema.Manga", "ParentManga")
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("ParentMangaId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.Navigation("ParentManga");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.Jobs.Job", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("API.Schema.Jobs.Job", "ParentJob")
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("ParentJobId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade);
|
|
||||||
|
|
||||||
b.Navigation("ParentJob");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.Link", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("API.Schema.Manga", null)
|
|
||||||
.WithMany("Links")
|
|
||||||
.HasForeignKey("MangaId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.Manga", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("API.Schema.LocalLibrary", "Library")
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("LibraryLocalLibraryId")
|
|
||||||
.OnDelete(DeleteBehavior.Restrict);
|
|
||||||
|
|
||||||
b.HasOne("API.Schema.MangaConnectors.MangaConnector", "MangaConnector")
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("MangaConnectorId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.Navigation("Library");
|
|
||||||
|
|
||||||
b.Navigation("MangaConnector");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.MangaAltTitle", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("API.Schema.Manga", null)
|
|
||||||
.WithMany("AltTitles")
|
|
||||||
.HasForeignKey("MangaId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("AuthorManga", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("API.Schema.Author", null)
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("AuthorsAuthorId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.HasOne("API.Schema.Manga", null)
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("MangaId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("JobJob", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("API.Schema.Jobs.Job", null)
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("DependsOnJobsJobId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.HasOne("API.Schema.Jobs.Job", null)
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("JobId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("MangaMangaTag", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("API.Schema.Manga", null)
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("MangaId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.HasOne("API.Schema.MangaTag", null)
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("MangaTagsTag")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.Jobs.DownloadAvailableChaptersJob", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("API.Schema.Manga", "Manga")
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("MangaId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.Navigation("Manga");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.Jobs.DownloadMangaCoverJob", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("API.Schema.Manga", "Manga")
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("MangaId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.Navigation("Manga");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.Jobs.DownloadSingleChapterJob", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("API.Schema.Chapter", "Chapter")
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("ChapterId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.Navigation("Chapter");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.Jobs.RetrieveChaptersJob", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("API.Schema.Manga", "Manga")
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("MangaId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.Navigation("Manga");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.Jobs.UpdateFilesDownloadedJob", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("API.Schema.Manga", "Manga")
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("MangaId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.Navigation("Manga");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.Jobs.UpdateMetadataJob", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("API.Schema.Manga", "Manga")
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("MangaId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.Navigation("Manga");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.Manga", b =>
|
|
||||||
{
|
|
||||||
b.Navigation("AltTitles");
|
|
||||||
|
|
||||||
b.Navigation("Links");
|
|
||||||
});
|
|
||||||
#pragma warning restore 612, 618
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,42 +0,0 @@
|
|||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
namespace API.Migrations
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
public partial class dev1603252 : Migration
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Up(MigrationBuilder migrationBuilder)
|
|
||||||
{
|
|
||||||
migrationBuilder.DropForeignKey(
|
|
||||||
name: "FK_Mangas_LocalLibraries_LibraryLocalLibraryId",
|
|
||||||
table: "Mangas");
|
|
||||||
|
|
||||||
migrationBuilder.AddForeignKey(
|
|
||||||
name: "FK_Mangas_LocalLibraries_LibraryLocalLibraryId",
|
|
||||||
table: "Mangas",
|
|
||||||
column: "LibraryLocalLibraryId",
|
|
||||||
principalTable: "LocalLibraries",
|
|
||||||
principalColumn: "LocalLibraryId",
|
|
||||||
onDelete: ReferentialAction.Restrict);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Down(MigrationBuilder migrationBuilder)
|
|
||||||
{
|
|
||||||
migrationBuilder.DropForeignKey(
|
|
||||||
name: "FK_Mangas_LocalLibraries_LibraryLocalLibraryId",
|
|
||||||
table: "Mangas");
|
|
||||||
|
|
||||||
migrationBuilder.AddForeignKey(
|
|
||||||
name: "FK_Mangas_LocalLibraries_LibraryLocalLibraryId",
|
|
||||||
table: "Mangas",
|
|
||||||
column: "LibraryLocalLibraryId",
|
|
||||||
principalTable: "LocalLibraries",
|
|
||||||
principalColumn: "LocalLibraryId",
|
|
||||||
onDelete: ReferentialAction.Cascade);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
827
API/Migrations/20250401001439_dev-010425-1.Designer.cs
generated
827
API/Migrations/20250401001439_dev-010425-1.Designer.cs
generated
@ -1,827 +0,0 @@
|
|||||||
// <auto-generated />
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using API.Schema;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
|
||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
|
||||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
|
||||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
namespace API.Migrations
|
|
||||||
{
|
|
||||||
[DbContext(typeof(PgsqlContext))]
|
|
||||||
[Migration("20250401001439_dev-010425-1")]
|
|
||||||
partial class dev0104251
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
|
||||||
{
|
|
||||||
#pragma warning disable 612, 618
|
|
||||||
modelBuilder
|
|
||||||
.HasAnnotation("ProductVersion", "9.0.3")
|
|
||||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
|
||||||
|
|
||||||
NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "hstore");
|
|
||||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.Author", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("AuthorId")
|
|
||||||
.HasMaxLength(64)
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.Property<string>("AuthorName")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(128)
|
|
||||||
.HasColumnType("character varying(128)");
|
|
||||||
|
|
||||||
b.HasKey("AuthorId");
|
|
||||||
|
|
||||||
b.ToTable("Authors");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.Chapter", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("ChapterId")
|
|
||||||
.HasMaxLength(64)
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.Property<string>("ChapterNumber")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(10)
|
|
||||||
.HasColumnType("character varying(10)");
|
|
||||||
|
|
||||||
b.Property<bool>("Downloaded")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<string>("FileName")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(256)
|
|
||||||
.HasColumnType("character varying(256)");
|
|
||||||
|
|
||||||
b.Property<string>("ParentMangaId")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(64)
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.Property<string>("Title")
|
|
||||||
.HasMaxLength(256)
|
|
||||||
.HasColumnType("character varying(256)");
|
|
||||||
|
|
||||||
b.Property<string>("Url")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(2048)
|
|
||||||
.HasColumnType("character varying(2048)");
|
|
||||||
|
|
||||||
b.Property<int?>("VolumeNumber")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.HasKey("ChapterId");
|
|
||||||
|
|
||||||
b.HasIndex("ParentMangaId");
|
|
||||||
|
|
||||||
b.ToTable("Chapters");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.Jobs.Job", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("JobId")
|
|
||||||
.HasMaxLength(64)
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.PrimitiveCollection<string[]>("DependsOnJobsIds")
|
|
||||||
.HasMaxLength(64)
|
|
||||||
.HasColumnType("text[]");
|
|
||||||
|
|
||||||
b.Property<bool>("Enabled")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<byte>("JobType")
|
|
||||||
.HasColumnType("smallint");
|
|
||||||
|
|
||||||
b.Property<DateTime>("LastExecution")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("ParentJobId")
|
|
||||||
.HasMaxLength(64)
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.Property<decimal>("RecurrenceMs")
|
|
||||||
.HasColumnType("numeric(20,0)");
|
|
||||||
|
|
||||||
b.Property<byte>("state")
|
|
||||||
.HasColumnType("smallint");
|
|
||||||
|
|
||||||
b.HasKey("JobId");
|
|
||||||
|
|
||||||
b.HasIndex("ParentJobId");
|
|
||||||
|
|
||||||
b.ToTable("Jobs");
|
|
||||||
|
|
||||||
b.HasDiscriminator<byte>("JobType");
|
|
||||||
|
|
||||||
b.UseTphMappingStrategy();
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.LibraryConnectors.LibraryConnector", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("LibraryConnectorId")
|
|
||||||
.HasMaxLength(64)
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.Property<string>("Auth")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(256)
|
|
||||||
.HasColumnType("character varying(256)");
|
|
||||||
|
|
||||||
b.Property<string>("BaseUrl")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(256)
|
|
||||||
.HasColumnType("character varying(256)");
|
|
||||||
|
|
||||||
b.Property<byte>("LibraryType")
|
|
||||||
.HasColumnType("smallint");
|
|
||||||
|
|
||||||
b.HasKey("LibraryConnectorId");
|
|
||||||
|
|
||||||
b.ToTable("LibraryConnectors");
|
|
||||||
|
|
||||||
b.HasDiscriminator<byte>("LibraryType");
|
|
||||||
|
|
||||||
b.UseTphMappingStrategy();
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.Link", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("LinkId")
|
|
||||||
.HasMaxLength(64)
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.Property<string>("LinkProvider")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(64)
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.Property<string>("LinkUrl")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(2048)
|
|
||||||
.HasColumnType("character varying(2048)");
|
|
||||||
|
|
||||||
b.Property<string>("MangaId")
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.HasKey("LinkId");
|
|
||||||
|
|
||||||
b.HasIndex("MangaId");
|
|
||||||
|
|
||||||
b.ToTable("Links");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.LocalLibrary", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("LocalLibraryId")
|
|
||||||
.HasMaxLength(64)
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.Property<string>("BasePath")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(256)
|
|
||||||
.HasColumnType("character varying(256)");
|
|
||||||
|
|
||||||
b.Property<string>("LibraryName")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(512)
|
|
||||||
.HasColumnType("character varying(512)");
|
|
||||||
|
|
||||||
b.HasKey("LocalLibraryId");
|
|
||||||
|
|
||||||
b.ToTable("LocalLibraries");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.Manga", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("MangaId")
|
|
||||||
.HasMaxLength(64)
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.Property<string>("CoverFileNameInCache")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("CoverUrl")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Description")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("DirectoryName")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(256)
|
|
||||||
.HasColumnType("character varying(256)");
|
|
||||||
|
|
||||||
b.Property<string>("IdOnConnectorSite")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(128)
|
|
||||||
.HasColumnType("character varying(128)");
|
|
||||||
|
|
||||||
b.Property<float>("IgnoreChapterBefore")
|
|
||||||
.HasColumnType("real");
|
|
||||||
|
|
||||||
b.Property<string>("LibraryLocalLibraryId")
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.Property<string>("MangaConnectorId")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(64)
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.Property<string>("Name")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(256)
|
|
||||||
.HasColumnType("character varying(256)");
|
|
||||||
|
|
||||||
b.Property<string>("OriginalLanguage")
|
|
||||||
.HasMaxLength(8)
|
|
||||||
.HasColumnType("character varying(8)");
|
|
||||||
|
|
||||||
b.Property<byte>("ReleaseStatus")
|
|
||||||
.HasColumnType("smallint");
|
|
||||||
|
|
||||||
b.Property<string>("WebsiteUrl")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(256)
|
|
||||||
.HasColumnType("character varying(256)");
|
|
||||||
|
|
||||||
b.Property<long>("Year")
|
|
||||||
.HasColumnType("bigint");
|
|
||||||
|
|
||||||
b.HasKey("MangaId");
|
|
||||||
|
|
||||||
b.HasIndex("LibraryLocalLibraryId");
|
|
||||||
|
|
||||||
b.HasIndex("MangaConnectorId");
|
|
||||||
|
|
||||||
b.ToTable("Mangas");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.MangaAltTitle", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("AltTitleId")
|
|
||||||
.HasMaxLength(64)
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.Property<string>("Language")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(8)
|
|
||||||
.HasColumnType("character varying(8)");
|
|
||||||
|
|
||||||
b.Property<string>("MangaId")
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.Property<string>("Title")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(256)
|
|
||||||
.HasColumnType("character varying(256)");
|
|
||||||
|
|
||||||
b.HasKey("AltTitleId");
|
|
||||||
|
|
||||||
b.HasIndex("MangaId");
|
|
||||||
|
|
||||||
b.ToTable("AltTitles");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.MangaConnectors.MangaConnector", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Name")
|
|
||||||
.HasMaxLength(32)
|
|
||||||
.HasColumnType("character varying(32)");
|
|
||||||
|
|
||||||
b.PrimitiveCollection<string[]>("BaseUris")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(256)
|
|
||||||
.HasColumnType("text[]");
|
|
||||||
|
|
||||||
b.Property<bool>("Enabled")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<string>("IconUrl")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(2048)
|
|
||||||
.HasColumnType("character varying(2048)");
|
|
||||||
|
|
||||||
b.PrimitiveCollection<string[]>("SupportedLanguages")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(8)
|
|
||||||
.HasColumnType("text[]");
|
|
||||||
|
|
||||||
b.HasKey("Name");
|
|
||||||
|
|
||||||
b.ToTable("MangaConnectors");
|
|
||||||
|
|
||||||
b.HasDiscriminator<string>("Name").HasValue("MangaConnector");
|
|
||||||
|
|
||||||
b.UseTphMappingStrategy();
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.MangaTag", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Tag")
|
|
||||||
.HasMaxLength(64)
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.HasKey("Tag");
|
|
||||||
|
|
||||||
b.ToTable("Tags");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.Notification", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("NotificationId")
|
|
||||||
.HasMaxLength(64)
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.Property<DateTime>("Date")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("Message")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(512)
|
|
||||||
.HasColumnType("character varying(512)");
|
|
||||||
|
|
||||||
b.Property<string>("Title")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(128)
|
|
||||||
.HasColumnType("character varying(128)");
|
|
||||||
|
|
||||||
b.Property<byte>("Urgency")
|
|
||||||
.HasColumnType("smallint");
|
|
||||||
|
|
||||||
b.HasKey("NotificationId");
|
|
||||||
|
|
||||||
b.ToTable("Notifications");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.NotificationConnectors.NotificationConnector", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Name")
|
|
||||||
.HasMaxLength(64)
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.Property<string>("Body")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(4096)
|
|
||||||
.HasColumnType("character varying(4096)");
|
|
||||||
|
|
||||||
b.Property<Dictionary<string, string>>("Headers")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("hstore");
|
|
||||||
|
|
||||||
b.Property<string>("HttpMethod")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(8)
|
|
||||||
.HasColumnType("character varying(8)");
|
|
||||||
|
|
||||||
b.Property<string>("Url")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(2048)
|
|
||||||
.HasColumnType("character varying(2048)");
|
|
||||||
|
|
||||||
b.HasKey("Name");
|
|
||||||
|
|
||||||
b.ToTable("NotificationConnectors");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("AuthorManga", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("AuthorsAuthorId")
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.Property<string>("MangaId")
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.HasKey("AuthorsAuthorId", "MangaId");
|
|
||||||
|
|
||||||
b.HasIndex("MangaId");
|
|
||||||
|
|
||||||
b.ToTable("AuthorManga");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("JobJob", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("DependsOnJobsJobId")
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.Property<string>("JobId")
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.HasKey("DependsOnJobsJobId", "JobId");
|
|
||||||
|
|
||||||
b.HasIndex("JobId");
|
|
||||||
|
|
||||||
b.ToTable("JobJob");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("MangaMangaTag", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("MangaId")
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.Property<string>("MangaTagsTag")
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.HasKey("MangaId", "MangaTagsTag");
|
|
||||||
|
|
||||||
b.HasIndex("MangaTagsTag");
|
|
||||||
|
|
||||||
b.ToTable("MangaMangaTag");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.Jobs.DownloadAvailableChaptersJob", b =>
|
|
||||||
{
|
|
||||||
b.HasBaseType("API.Schema.Jobs.Job");
|
|
||||||
|
|
||||||
b.Property<string>("MangaId")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(64)
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.HasIndex("MangaId");
|
|
||||||
|
|
||||||
b.ToTable("Jobs", t =>
|
|
||||||
{
|
|
||||||
t.Property("MangaId")
|
|
||||||
.HasColumnName("DownloadAvailableChaptersJob_MangaId");
|
|
||||||
});
|
|
||||||
|
|
||||||
b.HasDiscriminator().HasValue((byte)1);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.Jobs.DownloadMangaCoverJob", b =>
|
|
||||||
{
|
|
||||||
b.HasBaseType("API.Schema.Jobs.Job");
|
|
||||||
|
|
||||||
b.Property<string>("MangaId")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(64)
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.HasIndex("MangaId");
|
|
||||||
|
|
||||||
b.HasDiscriminator().HasValue((byte)4);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.Jobs.DownloadSingleChapterJob", b =>
|
|
||||||
{
|
|
||||||
b.HasBaseType("API.Schema.Jobs.Job");
|
|
||||||
|
|
||||||
b.Property<string>("ChapterId")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(64)
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.HasIndex("ChapterId");
|
|
||||||
|
|
||||||
b.HasDiscriminator().HasValue((byte)0);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.Jobs.MoveFileOrFolderJob", b =>
|
|
||||||
{
|
|
||||||
b.HasBaseType("API.Schema.Jobs.Job");
|
|
||||||
|
|
||||||
b.Property<string>("FromLocation")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(256)
|
|
||||||
.HasColumnType("character varying(256)");
|
|
||||||
|
|
||||||
b.Property<string>("ToLocation")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(256)
|
|
||||||
.HasColumnType("character varying(256)");
|
|
||||||
|
|
||||||
b.HasDiscriminator().HasValue((byte)3);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.Jobs.RetrieveChaptersJob", b =>
|
|
||||||
{
|
|
||||||
b.HasBaseType("API.Schema.Jobs.Job");
|
|
||||||
|
|
||||||
b.Property<string>("MangaId")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(64)
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.HasIndex("MangaId");
|
|
||||||
|
|
||||||
b.ToTable("Jobs", t =>
|
|
||||||
{
|
|
||||||
t.Property("MangaId")
|
|
||||||
.HasColumnName("RetrieveChaptersJob_MangaId");
|
|
||||||
});
|
|
||||||
|
|
||||||
b.HasDiscriminator().HasValue((byte)5);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.Jobs.UpdateFilesDownloadedJob", b =>
|
|
||||||
{
|
|
||||||
b.HasBaseType("API.Schema.Jobs.Job");
|
|
||||||
|
|
||||||
b.Property<string>("MangaId")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(64)
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.HasIndex("MangaId");
|
|
||||||
|
|
||||||
b.ToTable("Jobs", t =>
|
|
||||||
{
|
|
||||||
t.Property("MangaId")
|
|
||||||
.HasColumnName("UpdateFilesDownloadedJob_MangaId");
|
|
||||||
});
|
|
||||||
|
|
||||||
b.HasDiscriminator().HasValue((byte)6);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.Jobs.UpdateMetadataJob", b =>
|
|
||||||
{
|
|
||||||
b.HasBaseType("API.Schema.Jobs.Job");
|
|
||||||
|
|
||||||
b.Property<string>("MangaId")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(64)
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.HasIndex("MangaId");
|
|
||||||
|
|
||||||
b.ToTable("Jobs", t =>
|
|
||||||
{
|
|
||||||
t.Property("MangaId")
|
|
||||||
.HasColumnName("UpdateMetadataJob_MangaId");
|
|
||||||
});
|
|
||||||
|
|
||||||
b.HasDiscriminator().HasValue((byte)2);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.LibraryConnectors.Kavita", b =>
|
|
||||||
{
|
|
||||||
b.HasBaseType("API.Schema.LibraryConnectors.LibraryConnector");
|
|
||||||
|
|
||||||
b.HasDiscriminator().HasValue((byte)1);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.LibraryConnectors.Komga", b =>
|
|
||||||
{
|
|
||||||
b.HasBaseType("API.Schema.LibraryConnectors.LibraryConnector");
|
|
||||||
|
|
||||||
b.HasDiscriminator().HasValue((byte)0);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.MangaConnectors.AsuraToon", b =>
|
|
||||||
{
|
|
||||||
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
|
|
||||||
|
|
||||||
b.HasDiscriminator().HasValue("AsuraToon");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.MangaConnectors.Bato", b =>
|
|
||||||
{
|
|
||||||
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
|
|
||||||
|
|
||||||
b.HasDiscriminator().HasValue("Bato");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.MangaConnectors.Global", b =>
|
|
||||||
{
|
|
||||||
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
|
|
||||||
|
|
||||||
b.HasDiscriminator().HasValue("Global");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.MangaConnectors.MangaDex", b =>
|
|
||||||
{
|
|
||||||
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
|
|
||||||
|
|
||||||
b.HasDiscriminator().HasValue("MangaDex");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.MangaConnectors.MangaHere", b =>
|
|
||||||
{
|
|
||||||
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
|
|
||||||
|
|
||||||
b.HasDiscriminator().HasValue("MangaHere");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.MangaConnectors.MangaKatana", b =>
|
|
||||||
{
|
|
||||||
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
|
|
||||||
|
|
||||||
b.HasDiscriminator().HasValue("MangaKatana");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.MangaConnectors.Manganato", b =>
|
|
||||||
{
|
|
||||||
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
|
|
||||||
|
|
||||||
b.HasDiscriminator().HasValue("Manganato");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.MangaConnectors.Mangaworld", b =>
|
|
||||||
{
|
|
||||||
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
|
|
||||||
|
|
||||||
b.HasDiscriminator().HasValue("Mangaworld");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.MangaConnectors.ManhuaPlus", b =>
|
|
||||||
{
|
|
||||||
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
|
|
||||||
|
|
||||||
b.HasDiscriminator().HasValue("ManhuaPlus");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.MangaConnectors.Weebcentral", b =>
|
|
||||||
{
|
|
||||||
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
|
|
||||||
|
|
||||||
b.HasDiscriminator().HasValue("Weebcentral");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.Chapter", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("API.Schema.Manga", "ParentManga")
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("ParentMangaId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.Navigation("ParentManga");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.Jobs.Job", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("API.Schema.Jobs.Job", "ParentJob")
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("ParentJobId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade);
|
|
||||||
|
|
||||||
b.Navigation("ParentJob");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.Link", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("API.Schema.Manga", null)
|
|
||||||
.WithMany("Links")
|
|
||||||
.HasForeignKey("MangaId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.Manga", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("API.Schema.LocalLibrary", "Library")
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("LibraryLocalLibraryId")
|
|
||||||
.OnDelete(DeleteBehavior.Restrict);
|
|
||||||
|
|
||||||
b.HasOne("API.Schema.MangaConnectors.MangaConnector", "MangaConnector")
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("MangaConnectorId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.Navigation("Library");
|
|
||||||
|
|
||||||
b.Navigation("MangaConnector");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.MangaAltTitle", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("API.Schema.Manga", null)
|
|
||||||
.WithMany("AltTitles")
|
|
||||||
.HasForeignKey("MangaId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("AuthorManga", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("API.Schema.Author", null)
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("AuthorsAuthorId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.HasOne("API.Schema.Manga", null)
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("MangaId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("JobJob", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("API.Schema.Jobs.Job", null)
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("DependsOnJobsJobId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.HasOne("API.Schema.Jobs.Job", null)
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("JobId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("MangaMangaTag", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("API.Schema.Manga", null)
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("MangaId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.HasOne("API.Schema.MangaTag", null)
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("MangaTagsTag")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.Jobs.DownloadAvailableChaptersJob", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("API.Schema.Manga", "Manga")
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("MangaId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.Navigation("Manga");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.Jobs.DownloadMangaCoverJob", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("API.Schema.Manga", "Manga")
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("MangaId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.Navigation("Manga");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.Jobs.DownloadSingleChapterJob", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("API.Schema.Chapter", "Chapter")
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("ChapterId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.Navigation("Chapter");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.Jobs.RetrieveChaptersJob", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("API.Schema.Manga", "Manga")
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("MangaId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.Navigation("Manga");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.Jobs.UpdateFilesDownloadedJob", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("API.Schema.Manga", "Manga")
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("MangaId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.Navigation("Manga");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.Jobs.UpdateMetadataJob", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("API.Schema.Manga", "Manga")
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("MangaId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.Navigation("Manga");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.Manga", b =>
|
|
||||||
{
|
|
||||||
b.Navigation("AltTitles");
|
|
||||||
|
|
||||||
b.Navigation("Links");
|
|
||||||
});
|
|
||||||
#pragma warning restore 612, 618
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,40 +0,0 @@
|
|||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
namespace API.Migrations
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
public partial class dev0104251 : Migration
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Up(MigrationBuilder migrationBuilder)
|
|
||||||
{
|
|
||||||
migrationBuilder.AlterColumn<string>(
|
|
||||||
name: "OriginalLanguage",
|
|
||||||
table: "Mangas",
|
|
||||||
type: "character varying(8)",
|
|
||||||
maxLength: 8,
|
|
||||||
nullable: true,
|
|
||||||
oldClrType: typeof(string),
|
|
||||||
oldType: "character varying(8)",
|
|
||||||
oldMaxLength: 8);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Down(MigrationBuilder migrationBuilder)
|
|
||||||
{
|
|
||||||
migrationBuilder.AlterColumn<string>(
|
|
||||||
name: "OriginalLanguage",
|
|
||||||
table: "Mangas",
|
|
||||||
type: "character varying(8)",
|
|
||||||
maxLength: 8,
|
|
||||||
nullable: false,
|
|
||||||
defaultValue: "",
|
|
||||||
oldClrType: typeof(string),
|
|
||||||
oldType: "character varying(8)",
|
|
||||||
oldMaxLength: 8,
|
|
||||||
oldNullable: true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,827 +0,0 @@
|
|||||||
// <auto-generated />
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using API.Schema;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
|
||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
|
||||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
|
||||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
namespace API.Migrations
|
|
||||||
{
|
|
||||||
[DbContext(typeof(PgsqlContext))]
|
|
||||||
[Migration("20250401162026_dev-010425-2-Longer_Var_Chars")]
|
|
||||||
partial class dev0104252Longer_Var_Chars
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
|
||||||
{
|
|
||||||
#pragma warning disable 612, 618
|
|
||||||
modelBuilder
|
|
||||||
.HasAnnotation("ProductVersion", "9.0.3")
|
|
||||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
|
||||||
|
|
||||||
NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "hstore");
|
|
||||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.Author", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("AuthorId")
|
|
||||||
.HasMaxLength(64)
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.Property<string>("AuthorName")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(128)
|
|
||||||
.HasColumnType("character varying(128)");
|
|
||||||
|
|
||||||
b.HasKey("AuthorId");
|
|
||||||
|
|
||||||
b.ToTable("Authors");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.Chapter", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("ChapterId")
|
|
||||||
.HasMaxLength(64)
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.Property<string>("ChapterNumber")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(10)
|
|
||||||
.HasColumnType("character varying(10)");
|
|
||||||
|
|
||||||
b.Property<bool>("Downloaded")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<string>("FileName")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(256)
|
|
||||||
.HasColumnType("character varying(256)");
|
|
||||||
|
|
||||||
b.Property<string>("ParentMangaId")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(64)
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.Property<string>("Title")
|
|
||||||
.HasMaxLength(256)
|
|
||||||
.HasColumnType("character varying(256)");
|
|
||||||
|
|
||||||
b.Property<string>("Url")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(2048)
|
|
||||||
.HasColumnType("character varying(2048)");
|
|
||||||
|
|
||||||
b.Property<int?>("VolumeNumber")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.HasKey("ChapterId");
|
|
||||||
|
|
||||||
b.HasIndex("ParentMangaId");
|
|
||||||
|
|
||||||
b.ToTable("Chapters");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.Jobs.Job", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("JobId")
|
|
||||||
.HasMaxLength(64)
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.PrimitiveCollection<string[]>("DependsOnJobsIds")
|
|
||||||
.HasMaxLength(64)
|
|
||||||
.HasColumnType("text[]");
|
|
||||||
|
|
||||||
b.Property<bool>("Enabled")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<byte>("JobType")
|
|
||||||
.HasColumnType("smallint");
|
|
||||||
|
|
||||||
b.Property<DateTime>("LastExecution")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("ParentJobId")
|
|
||||||
.HasMaxLength(64)
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.Property<decimal>("RecurrenceMs")
|
|
||||||
.HasColumnType("numeric(20,0)");
|
|
||||||
|
|
||||||
b.Property<byte>("state")
|
|
||||||
.HasColumnType("smallint");
|
|
||||||
|
|
||||||
b.HasKey("JobId");
|
|
||||||
|
|
||||||
b.HasIndex("ParentJobId");
|
|
||||||
|
|
||||||
b.ToTable("Jobs");
|
|
||||||
|
|
||||||
b.HasDiscriminator<byte>("JobType");
|
|
||||||
|
|
||||||
b.UseTphMappingStrategy();
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.LibraryConnectors.LibraryConnector", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("LibraryConnectorId")
|
|
||||||
.HasMaxLength(64)
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.Property<string>("Auth")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(256)
|
|
||||||
.HasColumnType("character varying(256)");
|
|
||||||
|
|
||||||
b.Property<string>("BaseUrl")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(256)
|
|
||||||
.HasColumnType("character varying(256)");
|
|
||||||
|
|
||||||
b.Property<byte>("LibraryType")
|
|
||||||
.HasColumnType("smallint");
|
|
||||||
|
|
||||||
b.HasKey("LibraryConnectorId");
|
|
||||||
|
|
||||||
b.ToTable("LibraryConnectors");
|
|
||||||
|
|
||||||
b.HasDiscriminator<byte>("LibraryType");
|
|
||||||
|
|
||||||
b.UseTphMappingStrategy();
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.Link", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("LinkId")
|
|
||||||
.HasMaxLength(64)
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.Property<string>("LinkProvider")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(64)
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.Property<string>("LinkUrl")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(2048)
|
|
||||||
.HasColumnType("character varying(2048)");
|
|
||||||
|
|
||||||
b.Property<string>("MangaId")
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.HasKey("LinkId");
|
|
||||||
|
|
||||||
b.HasIndex("MangaId");
|
|
||||||
|
|
||||||
b.ToTable("Links");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.LocalLibrary", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("LocalLibraryId")
|
|
||||||
.HasMaxLength(64)
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.Property<string>("BasePath")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(256)
|
|
||||||
.HasColumnType("character varying(256)");
|
|
||||||
|
|
||||||
b.Property<string>("LibraryName")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(512)
|
|
||||||
.HasColumnType("character varying(512)");
|
|
||||||
|
|
||||||
b.HasKey("LocalLibraryId");
|
|
||||||
|
|
||||||
b.ToTable("LocalLibraries");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.Manga", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("MangaId")
|
|
||||||
.HasMaxLength(64)
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.Property<string>("CoverFileNameInCache")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("CoverUrl")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Description")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("DirectoryName")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(1024)
|
|
||||||
.HasColumnType("character varying(1024)");
|
|
||||||
|
|
||||||
b.Property<string>("IdOnConnectorSite")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(256)
|
|
||||||
.HasColumnType("character varying(256)");
|
|
||||||
|
|
||||||
b.Property<float>("IgnoreChapterBefore")
|
|
||||||
.HasColumnType("real");
|
|
||||||
|
|
||||||
b.Property<string>("LibraryLocalLibraryId")
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.Property<string>("MangaConnectorId")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(64)
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.Property<string>("Name")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(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("LibraryLocalLibraryId");
|
|
||||||
|
|
||||||
b.HasIndex("MangaConnectorId");
|
|
||||||
|
|
||||||
b.ToTable("Mangas");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.MangaAltTitle", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("AltTitleId")
|
|
||||||
.HasMaxLength(64)
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.Property<string>("Language")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(8)
|
|
||||||
.HasColumnType("character varying(8)");
|
|
||||||
|
|
||||||
b.Property<string>("MangaId")
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.Property<string>("Title")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(256)
|
|
||||||
.HasColumnType("character varying(256)");
|
|
||||||
|
|
||||||
b.HasKey("AltTitleId");
|
|
||||||
|
|
||||||
b.HasIndex("MangaId");
|
|
||||||
|
|
||||||
b.ToTable("AltTitles");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.MangaConnectors.MangaConnector", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Name")
|
|
||||||
.HasMaxLength(32)
|
|
||||||
.HasColumnType("character varying(32)");
|
|
||||||
|
|
||||||
b.PrimitiveCollection<string[]>("BaseUris")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(256)
|
|
||||||
.HasColumnType("text[]");
|
|
||||||
|
|
||||||
b.Property<bool>("Enabled")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<string>("IconUrl")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(2048)
|
|
||||||
.HasColumnType("character varying(2048)");
|
|
||||||
|
|
||||||
b.PrimitiveCollection<string[]>("SupportedLanguages")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(8)
|
|
||||||
.HasColumnType("text[]");
|
|
||||||
|
|
||||||
b.HasKey("Name");
|
|
||||||
|
|
||||||
b.ToTable("MangaConnectors");
|
|
||||||
|
|
||||||
b.HasDiscriminator<string>("Name").HasValue("MangaConnector");
|
|
||||||
|
|
||||||
b.UseTphMappingStrategy();
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.MangaTag", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Tag")
|
|
||||||
.HasMaxLength(64)
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.HasKey("Tag");
|
|
||||||
|
|
||||||
b.ToTable("Tags");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.Notification", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("NotificationId")
|
|
||||||
.HasMaxLength(64)
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.Property<DateTime>("Date")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("Message")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(512)
|
|
||||||
.HasColumnType("character varying(512)");
|
|
||||||
|
|
||||||
b.Property<string>("Title")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(128)
|
|
||||||
.HasColumnType("character varying(128)");
|
|
||||||
|
|
||||||
b.Property<byte>("Urgency")
|
|
||||||
.HasColumnType("smallint");
|
|
||||||
|
|
||||||
b.HasKey("NotificationId");
|
|
||||||
|
|
||||||
b.ToTable("Notifications");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.NotificationConnectors.NotificationConnector", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Name")
|
|
||||||
.HasMaxLength(64)
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.Property<string>("Body")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(4096)
|
|
||||||
.HasColumnType("character varying(4096)");
|
|
||||||
|
|
||||||
b.Property<Dictionary<string, string>>("Headers")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("hstore");
|
|
||||||
|
|
||||||
b.Property<string>("HttpMethod")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(8)
|
|
||||||
.HasColumnType("character varying(8)");
|
|
||||||
|
|
||||||
b.Property<string>("Url")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(2048)
|
|
||||||
.HasColumnType("character varying(2048)");
|
|
||||||
|
|
||||||
b.HasKey("Name");
|
|
||||||
|
|
||||||
b.ToTable("NotificationConnectors");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("AuthorManga", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("AuthorsAuthorId")
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.Property<string>("MangaId")
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.HasKey("AuthorsAuthorId", "MangaId");
|
|
||||||
|
|
||||||
b.HasIndex("MangaId");
|
|
||||||
|
|
||||||
b.ToTable("AuthorManga");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("JobJob", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("DependsOnJobsJobId")
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.Property<string>("JobId")
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.HasKey("DependsOnJobsJobId", "JobId");
|
|
||||||
|
|
||||||
b.HasIndex("JobId");
|
|
||||||
|
|
||||||
b.ToTable("JobJob");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("MangaMangaTag", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("MangaId")
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.Property<string>("MangaTagsTag")
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.HasKey("MangaId", "MangaTagsTag");
|
|
||||||
|
|
||||||
b.HasIndex("MangaTagsTag");
|
|
||||||
|
|
||||||
b.ToTable("MangaMangaTag");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.Jobs.DownloadAvailableChaptersJob", b =>
|
|
||||||
{
|
|
||||||
b.HasBaseType("API.Schema.Jobs.Job");
|
|
||||||
|
|
||||||
b.Property<string>("MangaId")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(64)
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.HasIndex("MangaId");
|
|
||||||
|
|
||||||
b.ToTable("Jobs", t =>
|
|
||||||
{
|
|
||||||
t.Property("MangaId")
|
|
||||||
.HasColumnName("DownloadAvailableChaptersJob_MangaId");
|
|
||||||
});
|
|
||||||
|
|
||||||
b.HasDiscriminator().HasValue((byte)1);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.Jobs.DownloadMangaCoverJob", b =>
|
|
||||||
{
|
|
||||||
b.HasBaseType("API.Schema.Jobs.Job");
|
|
||||||
|
|
||||||
b.Property<string>("MangaId")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(64)
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.HasIndex("MangaId");
|
|
||||||
|
|
||||||
b.HasDiscriminator().HasValue((byte)4);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.Jobs.DownloadSingleChapterJob", b =>
|
|
||||||
{
|
|
||||||
b.HasBaseType("API.Schema.Jobs.Job");
|
|
||||||
|
|
||||||
b.Property<string>("ChapterId")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(64)
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.HasIndex("ChapterId");
|
|
||||||
|
|
||||||
b.HasDiscriminator().HasValue((byte)0);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.Jobs.MoveFileOrFolderJob", b =>
|
|
||||||
{
|
|
||||||
b.HasBaseType("API.Schema.Jobs.Job");
|
|
||||||
|
|
||||||
b.Property<string>("FromLocation")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(256)
|
|
||||||
.HasColumnType("character varying(256)");
|
|
||||||
|
|
||||||
b.Property<string>("ToLocation")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(256)
|
|
||||||
.HasColumnType("character varying(256)");
|
|
||||||
|
|
||||||
b.HasDiscriminator().HasValue((byte)3);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.Jobs.RetrieveChaptersJob", b =>
|
|
||||||
{
|
|
||||||
b.HasBaseType("API.Schema.Jobs.Job");
|
|
||||||
|
|
||||||
b.Property<string>("MangaId")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(64)
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.HasIndex("MangaId");
|
|
||||||
|
|
||||||
b.ToTable("Jobs", t =>
|
|
||||||
{
|
|
||||||
t.Property("MangaId")
|
|
||||||
.HasColumnName("RetrieveChaptersJob_MangaId");
|
|
||||||
});
|
|
||||||
|
|
||||||
b.HasDiscriminator().HasValue((byte)5);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.Jobs.UpdateFilesDownloadedJob", b =>
|
|
||||||
{
|
|
||||||
b.HasBaseType("API.Schema.Jobs.Job");
|
|
||||||
|
|
||||||
b.Property<string>("MangaId")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(64)
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.HasIndex("MangaId");
|
|
||||||
|
|
||||||
b.ToTable("Jobs", t =>
|
|
||||||
{
|
|
||||||
t.Property("MangaId")
|
|
||||||
.HasColumnName("UpdateFilesDownloadedJob_MangaId");
|
|
||||||
});
|
|
||||||
|
|
||||||
b.HasDiscriminator().HasValue((byte)6);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.Jobs.UpdateMetadataJob", b =>
|
|
||||||
{
|
|
||||||
b.HasBaseType("API.Schema.Jobs.Job");
|
|
||||||
|
|
||||||
b.Property<string>("MangaId")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(64)
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.HasIndex("MangaId");
|
|
||||||
|
|
||||||
b.ToTable("Jobs", t =>
|
|
||||||
{
|
|
||||||
t.Property("MangaId")
|
|
||||||
.HasColumnName("UpdateMetadataJob_MangaId");
|
|
||||||
});
|
|
||||||
|
|
||||||
b.HasDiscriminator().HasValue((byte)2);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.LibraryConnectors.Kavita", b =>
|
|
||||||
{
|
|
||||||
b.HasBaseType("API.Schema.LibraryConnectors.LibraryConnector");
|
|
||||||
|
|
||||||
b.HasDiscriminator().HasValue((byte)1);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.LibraryConnectors.Komga", b =>
|
|
||||||
{
|
|
||||||
b.HasBaseType("API.Schema.LibraryConnectors.LibraryConnector");
|
|
||||||
|
|
||||||
b.HasDiscriminator().HasValue((byte)0);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.MangaConnectors.AsuraToon", b =>
|
|
||||||
{
|
|
||||||
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
|
|
||||||
|
|
||||||
b.HasDiscriminator().HasValue("AsuraToon");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.MangaConnectors.Bato", b =>
|
|
||||||
{
|
|
||||||
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
|
|
||||||
|
|
||||||
b.HasDiscriminator().HasValue("Bato");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.MangaConnectors.Global", b =>
|
|
||||||
{
|
|
||||||
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
|
|
||||||
|
|
||||||
b.HasDiscriminator().HasValue("Global");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.MangaConnectors.MangaDex", b =>
|
|
||||||
{
|
|
||||||
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
|
|
||||||
|
|
||||||
b.HasDiscriminator().HasValue("MangaDex");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.MangaConnectors.MangaHere", b =>
|
|
||||||
{
|
|
||||||
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
|
|
||||||
|
|
||||||
b.HasDiscriminator().HasValue("MangaHere");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.MangaConnectors.MangaKatana", b =>
|
|
||||||
{
|
|
||||||
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
|
|
||||||
|
|
||||||
b.HasDiscriminator().HasValue("MangaKatana");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.MangaConnectors.Manganato", b =>
|
|
||||||
{
|
|
||||||
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
|
|
||||||
|
|
||||||
b.HasDiscriminator().HasValue("Manganato");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.MangaConnectors.Mangaworld", b =>
|
|
||||||
{
|
|
||||||
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
|
|
||||||
|
|
||||||
b.HasDiscriminator().HasValue("Mangaworld");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.MangaConnectors.ManhuaPlus", b =>
|
|
||||||
{
|
|
||||||
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
|
|
||||||
|
|
||||||
b.HasDiscriminator().HasValue("ManhuaPlus");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.MangaConnectors.Weebcentral", b =>
|
|
||||||
{
|
|
||||||
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
|
|
||||||
|
|
||||||
b.HasDiscriminator().HasValue("Weebcentral");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.Chapter", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("API.Schema.Manga", "ParentManga")
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("ParentMangaId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.Navigation("ParentManga");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.Jobs.Job", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("API.Schema.Jobs.Job", "ParentJob")
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("ParentJobId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade);
|
|
||||||
|
|
||||||
b.Navigation("ParentJob");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.Link", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("API.Schema.Manga", null)
|
|
||||||
.WithMany("Links")
|
|
||||||
.HasForeignKey("MangaId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.Manga", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("API.Schema.LocalLibrary", "Library")
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("LibraryLocalLibraryId")
|
|
||||||
.OnDelete(DeleteBehavior.Restrict);
|
|
||||||
|
|
||||||
b.HasOne("API.Schema.MangaConnectors.MangaConnector", "MangaConnector")
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("MangaConnectorId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.Navigation("Library");
|
|
||||||
|
|
||||||
b.Navigation("MangaConnector");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.MangaAltTitle", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("API.Schema.Manga", null)
|
|
||||||
.WithMany("AltTitles")
|
|
||||||
.HasForeignKey("MangaId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("AuthorManga", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("API.Schema.Author", null)
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("AuthorsAuthorId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.HasOne("API.Schema.Manga", null)
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("MangaId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("JobJob", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("API.Schema.Jobs.Job", null)
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("DependsOnJobsJobId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.HasOne("API.Schema.Jobs.Job", null)
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("JobId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("MangaMangaTag", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("API.Schema.Manga", null)
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("MangaId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.HasOne("API.Schema.MangaTag", null)
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("MangaTagsTag")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.Jobs.DownloadAvailableChaptersJob", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("API.Schema.Manga", "Manga")
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("MangaId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.Navigation("Manga");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.Jobs.DownloadMangaCoverJob", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("API.Schema.Manga", "Manga")
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("MangaId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.Navigation("Manga");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.Jobs.DownloadSingleChapterJob", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("API.Schema.Chapter", "Chapter")
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("ChapterId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.Navigation("Chapter");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.Jobs.RetrieveChaptersJob", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("API.Schema.Manga", "Manga")
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("MangaId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.Navigation("Manga");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.Jobs.UpdateFilesDownloadedJob", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("API.Schema.Manga", "Manga")
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("MangaId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.Navigation("Manga");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.Jobs.UpdateMetadataJob", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("API.Schema.Manga", "Manga")
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("MangaId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.Navigation("Manga");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.Manga", b =>
|
|
||||||
{
|
|
||||||
b.Navigation("AltTitles");
|
|
||||||
|
|
||||||
b.Navigation("Links");
|
|
||||||
});
|
|
||||||
#pragma warning restore 612, 618
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,98 +0,0 @@
|
|||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
namespace API.Migrations
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
public partial class dev0104252Longer_Var_Chars : Migration
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Up(MigrationBuilder migrationBuilder)
|
|
||||||
{
|
|
||||||
migrationBuilder.AlterColumn<string>(
|
|
||||||
name: "WebsiteUrl",
|
|
||||||
table: "Mangas",
|
|
||||||
type: "character varying(512)",
|
|
||||||
maxLength: 512,
|
|
||||||
nullable: false,
|
|
||||||
oldClrType: typeof(string),
|
|
||||||
oldType: "character varying(256)",
|
|
||||||
oldMaxLength: 256);
|
|
||||||
|
|
||||||
migrationBuilder.AlterColumn<string>(
|
|
||||||
name: "Name",
|
|
||||||
table: "Mangas",
|
|
||||||
type: "character varying(512)",
|
|
||||||
maxLength: 512,
|
|
||||||
nullable: false,
|
|
||||||
oldClrType: typeof(string),
|
|
||||||
oldType: "character varying(256)",
|
|
||||||
oldMaxLength: 256);
|
|
||||||
|
|
||||||
migrationBuilder.AlterColumn<string>(
|
|
||||||
name: "IdOnConnectorSite",
|
|
||||||
table: "Mangas",
|
|
||||||
type: "character varying(256)",
|
|
||||||
maxLength: 256,
|
|
||||||
nullable: false,
|
|
||||||
oldClrType: typeof(string),
|
|
||||||
oldType: "character varying(128)",
|
|
||||||
oldMaxLength: 128);
|
|
||||||
|
|
||||||
migrationBuilder.AlterColumn<string>(
|
|
||||||
name: "DirectoryName",
|
|
||||||
table: "Mangas",
|
|
||||||
type: "character varying(1024)",
|
|
||||||
maxLength: 1024,
|
|
||||||
nullable: false,
|
|
||||||
oldClrType: typeof(string),
|
|
||||||
oldType: "character varying(256)",
|
|
||||||
oldMaxLength: 256);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Down(MigrationBuilder migrationBuilder)
|
|
||||||
{
|
|
||||||
migrationBuilder.AlterColumn<string>(
|
|
||||||
name: "WebsiteUrl",
|
|
||||||
table: "Mangas",
|
|
||||||
type: "character varying(256)",
|
|
||||||
maxLength: 256,
|
|
||||||
nullable: false,
|
|
||||||
oldClrType: typeof(string),
|
|
||||||
oldType: "character varying(512)",
|
|
||||||
oldMaxLength: 512);
|
|
||||||
|
|
||||||
migrationBuilder.AlterColumn<string>(
|
|
||||||
name: "Name",
|
|
||||||
table: "Mangas",
|
|
||||||
type: "character varying(256)",
|
|
||||||
maxLength: 256,
|
|
||||||
nullable: false,
|
|
||||||
oldClrType: typeof(string),
|
|
||||||
oldType: "character varying(512)",
|
|
||||||
oldMaxLength: 512);
|
|
||||||
|
|
||||||
migrationBuilder.AlterColumn<string>(
|
|
||||||
name: "IdOnConnectorSite",
|
|
||||||
table: "Mangas",
|
|
||||||
type: "character varying(128)",
|
|
||||||
maxLength: 128,
|
|
||||||
nullable: false,
|
|
||||||
oldClrType: typeof(string),
|
|
||||||
oldType: "character varying(256)",
|
|
||||||
oldMaxLength: 256);
|
|
||||||
|
|
||||||
migrationBuilder.AlterColumn<string>(
|
|
||||||
name: "DirectoryName",
|
|
||||||
table: "Mangas",
|
|
||||||
type: "character varying(256)",
|
|
||||||
maxLength: 256,
|
|
||||||
nullable: false,
|
|
||||||
oldClrType: typeof(string),
|
|
||||||
oldType: "character varying(1024)",
|
|
||||||
oldMaxLength: 1024);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,824 +0,0 @@
|
|||||||
// <auto-generated />
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using API.Schema;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
|
||||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
|
||||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
namespace API.Migrations
|
|
||||||
{
|
|
||||||
[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.HasPostgresExtension(modelBuilder, "hstore");
|
|
||||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.Author", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("AuthorId")
|
|
||||||
.HasMaxLength(64)
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.Property<string>("AuthorName")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(128)
|
|
||||||
.HasColumnType("character varying(128)");
|
|
||||||
|
|
||||||
b.HasKey("AuthorId");
|
|
||||||
|
|
||||||
b.ToTable("Authors");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.Chapter", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("ChapterId")
|
|
||||||
.HasMaxLength(64)
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.Property<string>("ChapterNumber")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(10)
|
|
||||||
.HasColumnType("character varying(10)");
|
|
||||||
|
|
||||||
b.Property<bool>("Downloaded")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<string>("FileName")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(256)
|
|
||||||
.HasColumnType("character varying(256)");
|
|
||||||
|
|
||||||
b.Property<string>("ParentMangaId")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(64)
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.Property<string>("Title")
|
|
||||||
.HasMaxLength(256)
|
|
||||||
.HasColumnType("character varying(256)");
|
|
||||||
|
|
||||||
b.Property<string>("Url")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(2048)
|
|
||||||
.HasColumnType("character varying(2048)");
|
|
||||||
|
|
||||||
b.Property<int?>("VolumeNumber")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.HasKey("ChapterId");
|
|
||||||
|
|
||||||
b.HasIndex("ParentMangaId");
|
|
||||||
|
|
||||||
b.ToTable("Chapters");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.Jobs.Job", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("JobId")
|
|
||||||
.HasMaxLength(64)
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.PrimitiveCollection<string[]>("DependsOnJobsIds")
|
|
||||||
.HasMaxLength(64)
|
|
||||||
.HasColumnType("text[]");
|
|
||||||
|
|
||||||
b.Property<bool>("Enabled")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<byte>("JobType")
|
|
||||||
.HasColumnType("smallint");
|
|
||||||
|
|
||||||
b.Property<DateTime>("LastExecution")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("ParentJobId")
|
|
||||||
.HasMaxLength(64)
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.Property<decimal>("RecurrenceMs")
|
|
||||||
.HasColumnType("numeric(20,0)");
|
|
||||||
|
|
||||||
b.Property<byte>("state")
|
|
||||||
.HasColumnType("smallint");
|
|
||||||
|
|
||||||
b.HasKey("JobId");
|
|
||||||
|
|
||||||
b.HasIndex("ParentJobId");
|
|
||||||
|
|
||||||
b.ToTable("Jobs");
|
|
||||||
|
|
||||||
b.HasDiscriminator<byte>("JobType");
|
|
||||||
|
|
||||||
b.UseTphMappingStrategy();
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.LibraryConnectors.LibraryConnector", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("LibraryConnectorId")
|
|
||||||
.HasMaxLength(64)
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.Property<string>("Auth")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(256)
|
|
||||||
.HasColumnType("character varying(256)");
|
|
||||||
|
|
||||||
b.Property<string>("BaseUrl")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(256)
|
|
||||||
.HasColumnType("character varying(256)");
|
|
||||||
|
|
||||||
b.Property<byte>("LibraryType")
|
|
||||||
.HasColumnType("smallint");
|
|
||||||
|
|
||||||
b.HasKey("LibraryConnectorId");
|
|
||||||
|
|
||||||
b.ToTable("LibraryConnectors");
|
|
||||||
|
|
||||||
b.HasDiscriminator<byte>("LibraryType");
|
|
||||||
|
|
||||||
b.UseTphMappingStrategy();
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.Link", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("LinkId")
|
|
||||||
.HasMaxLength(64)
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.Property<string>("LinkProvider")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(64)
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.Property<string>("LinkUrl")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(2048)
|
|
||||||
.HasColumnType("character varying(2048)");
|
|
||||||
|
|
||||||
b.Property<string>("MangaId")
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.HasKey("LinkId");
|
|
||||||
|
|
||||||
b.HasIndex("MangaId");
|
|
||||||
|
|
||||||
b.ToTable("Links");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.LocalLibrary", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("LocalLibraryId")
|
|
||||||
.HasMaxLength(64)
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.Property<string>("BasePath")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(256)
|
|
||||||
.HasColumnType("character varying(256)");
|
|
||||||
|
|
||||||
b.Property<string>("LibraryName")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(512)
|
|
||||||
.HasColumnType("character varying(512)");
|
|
||||||
|
|
||||||
b.HasKey("LocalLibraryId");
|
|
||||||
|
|
||||||
b.ToTable("LocalLibraries");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.Manga", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("MangaId")
|
|
||||||
.HasMaxLength(64)
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.Property<string>("CoverFileNameInCache")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("CoverUrl")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Description")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("DirectoryName")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(1024)
|
|
||||||
.HasColumnType("character varying(1024)");
|
|
||||||
|
|
||||||
b.Property<string>("IdOnConnectorSite")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(256)
|
|
||||||
.HasColumnType("character varying(256)");
|
|
||||||
|
|
||||||
b.Property<float>("IgnoreChapterBefore")
|
|
||||||
.HasColumnType("real");
|
|
||||||
|
|
||||||
b.Property<string>("LibraryLocalLibraryId")
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.Property<string>("MangaConnectorId")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(64)
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.Property<string>("Name")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(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("LibraryLocalLibraryId");
|
|
||||||
|
|
||||||
b.HasIndex("MangaConnectorId");
|
|
||||||
|
|
||||||
b.ToTable("Mangas");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.MangaAltTitle", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("AltTitleId")
|
|
||||||
.HasMaxLength(64)
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.Property<string>("Language")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(8)
|
|
||||||
.HasColumnType("character varying(8)");
|
|
||||||
|
|
||||||
b.Property<string>("MangaId")
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.Property<string>("Title")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(256)
|
|
||||||
.HasColumnType("character varying(256)");
|
|
||||||
|
|
||||||
b.HasKey("AltTitleId");
|
|
||||||
|
|
||||||
b.HasIndex("MangaId");
|
|
||||||
|
|
||||||
b.ToTable("AltTitles");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.MangaConnectors.MangaConnector", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Name")
|
|
||||||
.HasMaxLength(32)
|
|
||||||
.HasColumnType("character varying(32)");
|
|
||||||
|
|
||||||
b.PrimitiveCollection<string[]>("BaseUris")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(256)
|
|
||||||
.HasColumnType("text[]");
|
|
||||||
|
|
||||||
b.Property<bool>("Enabled")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<string>("IconUrl")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(2048)
|
|
||||||
.HasColumnType("character varying(2048)");
|
|
||||||
|
|
||||||
b.PrimitiveCollection<string[]>("SupportedLanguages")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(8)
|
|
||||||
.HasColumnType("text[]");
|
|
||||||
|
|
||||||
b.HasKey("Name");
|
|
||||||
|
|
||||||
b.ToTable("MangaConnectors");
|
|
||||||
|
|
||||||
b.HasDiscriminator<string>("Name").HasValue("MangaConnector");
|
|
||||||
|
|
||||||
b.UseTphMappingStrategy();
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.MangaTag", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Tag")
|
|
||||||
.HasMaxLength(64)
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.HasKey("Tag");
|
|
||||||
|
|
||||||
b.ToTable("Tags");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.Notification", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("NotificationId")
|
|
||||||
.HasMaxLength(64)
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.Property<DateTime>("Date")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("Message")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(512)
|
|
||||||
.HasColumnType("character varying(512)");
|
|
||||||
|
|
||||||
b.Property<string>("Title")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(128)
|
|
||||||
.HasColumnType("character varying(128)");
|
|
||||||
|
|
||||||
b.Property<byte>("Urgency")
|
|
||||||
.HasColumnType("smallint");
|
|
||||||
|
|
||||||
b.HasKey("NotificationId");
|
|
||||||
|
|
||||||
b.ToTable("Notifications");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.NotificationConnectors.NotificationConnector", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Name")
|
|
||||||
.HasMaxLength(64)
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.Property<string>("Body")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(4096)
|
|
||||||
.HasColumnType("character varying(4096)");
|
|
||||||
|
|
||||||
b.Property<Dictionary<string, string>>("Headers")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("hstore");
|
|
||||||
|
|
||||||
b.Property<string>("HttpMethod")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(8)
|
|
||||||
.HasColumnType("character varying(8)");
|
|
||||||
|
|
||||||
b.Property<string>("Url")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(2048)
|
|
||||||
.HasColumnType("character varying(2048)");
|
|
||||||
|
|
||||||
b.HasKey("Name");
|
|
||||||
|
|
||||||
b.ToTable("NotificationConnectors");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("AuthorManga", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("AuthorsAuthorId")
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.Property<string>("MangaId")
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.HasKey("AuthorsAuthorId", "MangaId");
|
|
||||||
|
|
||||||
b.HasIndex("MangaId");
|
|
||||||
|
|
||||||
b.ToTable("AuthorManga");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("JobJob", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("DependsOnJobsJobId")
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.Property<string>("JobId")
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.HasKey("DependsOnJobsJobId", "JobId");
|
|
||||||
|
|
||||||
b.HasIndex("JobId");
|
|
||||||
|
|
||||||
b.ToTable("JobJob");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("MangaMangaTag", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("MangaId")
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.Property<string>("MangaTagsTag")
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.HasKey("MangaId", "MangaTagsTag");
|
|
||||||
|
|
||||||
b.HasIndex("MangaTagsTag");
|
|
||||||
|
|
||||||
b.ToTable("MangaMangaTag");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.Jobs.DownloadAvailableChaptersJob", b =>
|
|
||||||
{
|
|
||||||
b.HasBaseType("API.Schema.Jobs.Job");
|
|
||||||
|
|
||||||
b.Property<string>("MangaId")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(64)
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.HasIndex("MangaId");
|
|
||||||
|
|
||||||
b.ToTable("Jobs", t =>
|
|
||||||
{
|
|
||||||
t.Property("MangaId")
|
|
||||||
.HasColumnName("DownloadAvailableChaptersJob_MangaId");
|
|
||||||
});
|
|
||||||
|
|
||||||
b.HasDiscriminator().HasValue((byte)1);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.Jobs.DownloadMangaCoverJob", b =>
|
|
||||||
{
|
|
||||||
b.HasBaseType("API.Schema.Jobs.Job");
|
|
||||||
|
|
||||||
b.Property<string>("MangaId")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(64)
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.HasIndex("MangaId");
|
|
||||||
|
|
||||||
b.HasDiscriminator().HasValue((byte)4);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.Jobs.DownloadSingleChapterJob", b =>
|
|
||||||
{
|
|
||||||
b.HasBaseType("API.Schema.Jobs.Job");
|
|
||||||
|
|
||||||
b.Property<string>("ChapterId")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(64)
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.HasIndex("ChapterId");
|
|
||||||
|
|
||||||
b.HasDiscriminator().HasValue((byte)0);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.Jobs.MoveFileOrFolderJob", b =>
|
|
||||||
{
|
|
||||||
b.HasBaseType("API.Schema.Jobs.Job");
|
|
||||||
|
|
||||||
b.Property<string>("FromLocation")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(256)
|
|
||||||
.HasColumnType("character varying(256)");
|
|
||||||
|
|
||||||
b.Property<string>("ToLocation")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(256)
|
|
||||||
.HasColumnType("character varying(256)");
|
|
||||||
|
|
||||||
b.HasDiscriminator().HasValue((byte)3);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.Jobs.RetrieveChaptersJob", b =>
|
|
||||||
{
|
|
||||||
b.HasBaseType("API.Schema.Jobs.Job");
|
|
||||||
|
|
||||||
b.Property<string>("MangaId")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(64)
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.HasIndex("MangaId");
|
|
||||||
|
|
||||||
b.ToTable("Jobs", t =>
|
|
||||||
{
|
|
||||||
t.Property("MangaId")
|
|
||||||
.HasColumnName("RetrieveChaptersJob_MangaId");
|
|
||||||
});
|
|
||||||
|
|
||||||
b.HasDiscriminator().HasValue((byte)5);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.Jobs.UpdateFilesDownloadedJob", b =>
|
|
||||||
{
|
|
||||||
b.HasBaseType("API.Schema.Jobs.Job");
|
|
||||||
|
|
||||||
b.Property<string>("MangaId")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(64)
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.HasIndex("MangaId");
|
|
||||||
|
|
||||||
b.ToTable("Jobs", t =>
|
|
||||||
{
|
|
||||||
t.Property("MangaId")
|
|
||||||
.HasColumnName("UpdateFilesDownloadedJob_MangaId");
|
|
||||||
});
|
|
||||||
|
|
||||||
b.HasDiscriminator().HasValue((byte)6);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.Jobs.UpdateMetadataJob", b =>
|
|
||||||
{
|
|
||||||
b.HasBaseType("API.Schema.Jobs.Job");
|
|
||||||
|
|
||||||
b.Property<string>("MangaId")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(64)
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.HasIndex("MangaId");
|
|
||||||
|
|
||||||
b.ToTable("Jobs", t =>
|
|
||||||
{
|
|
||||||
t.Property("MangaId")
|
|
||||||
.HasColumnName("UpdateMetadataJob_MangaId");
|
|
||||||
});
|
|
||||||
|
|
||||||
b.HasDiscriminator().HasValue((byte)2);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.LibraryConnectors.Kavita", b =>
|
|
||||||
{
|
|
||||||
b.HasBaseType("API.Schema.LibraryConnectors.LibraryConnector");
|
|
||||||
|
|
||||||
b.HasDiscriminator().HasValue((byte)1);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.LibraryConnectors.Komga", b =>
|
|
||||||
{
|
|
||||||
b.HasBaseType("API.Schema.LibraryConnectors.LibraryConnector");
|
|
||||||
|
|
||||||
b.HasDiscriminator().HasValue((byte)0);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.MangaConnectors.AsuraToon", b =>
|
|
||||||
{
|
|
||||||
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
|
|
||||||
|
|
||||||
b.HasDiscriminator().HasValue("AsuraToon");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.MangaConnectors.Bato", b =>
|
|
||||||
{
|
|
||||||
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
|
|
||||||
|
|
||||||
b.HasDiscriminator().HasValue("Bato");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.MangaConnectors.Global", b =>
|
|
||||||
{
|
|
||||||
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
|
|
||||||
|
|
||||||
b.HasDiscriminator().HasValue("Global");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.MangaConnectors.MangaDex", b =>
|
|
||||||
{
|
|
||||||
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
|
|
||||||
|
|
||||||
b.HasDiscriminator().HasValue("MangaDex");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.MangaConnectors.MangaHere", b =>
|
|
||||||
{
|
|
||||||
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
|
|
||||||
|
|
||||||
b.HasDiscriminator().HasValue("MangaHere");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.MangaConnectors.MangaKatana", b =>
|
|
||||||
{
|
|
||||||
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
|
|
||||||
|
|
||||||
b.HasDiscriminator().HasValue("MangaKatana");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.MangaConnectors.Manganato", b =>
|
|
||||||
{
|
|
||||||
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
|
|
||||||
|
|
||||||
b.HasDiscriminator().HasValue("Manganato");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.MangaConnectors.Mangaworld", b =>
|
|
||||||
{
|
|
||||||
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
|
|
||||||
|
|
||||||
b.HasDiscriminator().HasValue("Mangaworld");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.MangaConnectors.ManhuaPlus", b =>
|
|
||||||
{
|
|
||||||
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
|
|
||||||
|
|
||||||
b.HasDiscriminator().HasValue("ManhuaPlus");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.MangaConnectors.Weebcentral", b =>
|
|
||||||
{
|
|
||||||
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
|
|
||||||
|
|
||||||
b.HasDiscriminator().HasValue("Weebcentral");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.Chapter", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("API.Schema.Manga", "ParentManga")
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("ParentMangaId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.Navigation("ParentManga");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.Jobs.Job", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("API.Schema.Jobs.Job", "ParentJob")
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("ParentJobId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade);
|
|
||||||
|
|
||||||
b.Navigation("ParentJob");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.Link", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("API.Schema.Manga", null)
|
|
||||||
.WithMany("Links")
|
|
||||||
.HasForeignKey("MangaId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.Manga", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("API.Schema.LocalLibrary", "Library")
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("LibraryLocalLibraryId")
|
|
||||||
.OnDelete(DeleteBehavior.Restrict);
|
|
||||||
|
|
||||||
b.HasOne("API.Schema.MangaConnectors.MangaConnector", "MangaConnector")
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("MangaConnectorId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.Navigation("Library");
|
|
||||||
|
|
||||||
b.Navigation("MangaConnector");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.MangaAltTitle", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("API.Schema.Manga", null)
|
|
||||||
.WithMany("AltTitles")
|
|
||||||
.HasForeignKey("MangaId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("AuthorManga", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("API.Schema.Author", null)
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("AuthorsAuthorId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.HasOne("API.Schema.Manga", null)
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("MangaId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("JobJob", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("API.Schema.Jobs.Job", null)
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("DependsOnJobsJobId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.HasOne("API.Schema.Jobs.Job", null)
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("JobId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("MangaMangaTag", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("API.Schema.Manga", null)
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("MangaId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.HasOne("API.Schema.MangaTag", null)
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("MangaTagsTag")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.Jobs.DownloadAvailableChaptersJob", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("API.Schema.Manga", "Manga")
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("MangaId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.Navigation("Manga");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.Jobs.DownloadMangaCoverJob", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("API.Schema.Manga", "Manga")
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("MangaId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.Navigation("Manga");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.Jobs.DownloadSingleChapterJob", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("API.Schema.Chapter", "Chapter")
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("ChapterId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.Navigation("Chapter");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.Jobs.RetrieveChaptersJob", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("API.Schema.Manga", "Manga")
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("MangaId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.Navigation("Manga");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.Jobs.UpdateFilesDownloadedJob", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("API.Schema.Manga", "Manga")
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("MangaId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.Navigation("Manga");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.Jobs.UpdateMetadataJob", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("API.Schema.Manga", "Manga")
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("MangaId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.Navigation("Manga");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.Manga", b =>
|
|
||||||
{
|
|
||||||
b.Navigation("AltTitles");
|
|
||||||
|
|
||||||
b.Navigation("Links");
|
|
||||||
});
|
|
||||||
#pragma warning restore 612, 618
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,46 +0,0 @@
|
|||||||
using Asp.Versioning.ApiExplorer;
|
|
||||||
using Microsoft.Extensions.Options;
|
|
||||||
using Microsoft.OpenApi.Models;
|
|
||||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
|
||||||
|
|
||||||
namespace API;
|
|
||||||
|
|
||||||
public class NamedSwaggerGenOptions : IConfigureNamedOptions<SwaggerGenOptions>
|
|
||||||
{
|
|
||||||
private readonly IApiVersionDescriptionProvider provider;
|
|
||||||
public NamedSwaggerGenOptions(IApiVersionDescriptionProvider provider)
|
|
||||||
{
|
|
||||||
this.provider = provider;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Configure(string? name, SwaggerGenOptions options)
|
|
||||||
{
|
|
||||||
Configure(options);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Configure(SwaggerGenOptions options)
|
|
||||||
{
|
|
||||||
// add swagger document for every API version discovered
|
|
||||||
foreach (var description in provider.ApiVersionDescriptions)
|
|
||||||
{
|
|
||||||
options.SwaggerDoc(
|
|
||||||
description.GroupName,
|
|
||||||
CreateVersionInfo(description));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private OpenApiInfo CreateVersionInfo(
|
|
||||||
ApiVersionDescription description)
|
|
||||||
{
|
|
||||||
var info = new OpenApiInfo()
|
|
||||||
{
|
|
||||||
Title = "Test API " + description.GroupName,
|
|
||||||
Version = description.ApiVersion.ToString()
|
|
||||||
};
|
|
||||||
if (description.IsDeprecated)
|
|
||||||
{
|
|
||||||
info.Description += " This API version has been deprecated.";
|
|
||||||
}
|
|
||||||
return info;
|
|
||||||
}
|
|
||||||
}
|
|
147
API/Program.cs
147
API/Program.cs
@ -1,147 +0,0 @@
|
|||||||
using System.Reflection;
|
|
||||||
using System.Text.Json.Serialization;
|
|
||||||
using API;
|
|
||||||
using API.Schema;
|
|
||||||
using API.Schema.Jobs;
|
|
||||||
using API.Schema.MangaConnectors;
|
|
||||||
using Asp.Versioning;
|
|
||||||
using Asp.Versioning.Builder;
|
|
||||||
using Asp.Versioning.Conventions;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Newtonsoft.Json;
|
|
||||||
using Newtonsoft.Json.Converters;
|
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
|
||||||
|
|
||||||
builder.Services.AddCors(options =>
|
|
||||||
{
|
|
||||||
options.AddPolicy("AllowAll",
|
|
||||||
policy =>
|
|
||||||
{
|
|
||||||
policy
|
|
||||||
.AllowAnyOrigin()
|
|
||||||
.AllowAnyMethod()
|
|
||||||
.AllowAnyHeader();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
builder.Services.AddApiVersioning(option =>
|
|
||||||
{
|
|
||||||
option.AssumeDefaultVersionWhenUnspecified = true;
|
|
||||||
option.DefaultApiVersion = new ApiVersion(2);
|
|
||||||
option.ReportApiVersions = true;
|
|
||||||
option.ApiVersionReader = ApiVersionReader.Combine(
|
|
||||||
new UrlSegmentApiVersionReader(),
|
|
||||||
new QueryStringApiVersionReader("api-version"),
|
|
||||||
new HeaderApiVersionReader("X-Version"),
|
|
||||||
new MediaTypeApiVersionReader("x-version"));
|
|
||||||
})
|
|
||||||
.AddMvc(options =>
|
|
||||||
{
|
|
||||||
options.Conventions.Add(new VersionByNamespaceConvention());
|
|
||||||
})
|
|
||||||
.AddApiExplorer(options =>
|
|
||||||
{
|
|
||||||
options.GroupNameFormat = "'v'V";
|
|
||||||
options.SubstituteApiVersionInUrl = true;
|
|
||||||
});
|
|
||||||
builder.Services.AddEndpointsApiExplorer();
|
|
||||||
builder.Services.AddSwaggerGenNewtonsoftSupport();
|
|
||||||
builder.Services.AddSwaggerGen(opt =>
|
|
||||||
{
|
|
||||||
var xmlFilename = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
|
|
||||||
opt.IncludeXmlComments(Path.Combine(AppContext.BaseDirectory, xmlFilename));
|
|
||||||
});
|
|
||||||
builder.Services.ConfigureOptions<NamedSwaggerGenOptions>();
|
|
||||||
|
|
||||||
builder.Services.AddDbContext<PgsqlContext>(options =>
|
|
||||||
options.UseNpgsql($"Host={Environment.GetEnvironmentVariable("POSTGRES_HOST")??"localhost:5432"}; " +
|
|
||||||
$"Database={Environment.GetEnvironmentVariable("POSTGRES_DB")??"postgres"}; " +
|
|
||||||
$"Username={Environment.GetEnvironmentVariable("POSTGRES_USER")??"postgres"}; " +
|
|
||||||
$"Password={Environment.GetEnvironmentVariable("POSTGRES_PASSWORD")??"postgres"}"));
|
|
||||||
|
|
||||||
builder.Services.AddControllers(options =>
|
|
||||||
{
|
|
||||||
options.AllowEmptyInputInBodyModelBinding = true;
|
|
||||||
});
|
|
||||||
builder.Services.AddControllers().AddNewtonsoftJson(opts =>
|
|
||||||
{
|
|
||||||
opts.SerializerSettings.Converters.Add(new StringEnumConverter());
|
|
||||||
opts.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore;
|
|
||||||
});
|
|
||||||
|
|
||||||
builder.WebHost.UseUrls("http://*:6531");
|
|
||||||
|
|
||||||
var app = builder.Build();
|
|
||||||
|
|
||||||
ApiVersionSet apiVersionSet = app.NewApiVersionSet()
|
|
||||||
.HasApiVersion(new ApiVersion(2))
|
|
||||||
.ReportApiVersions()
|
|
||||||
.Build();
|
|
||||||
|
|
||||||
|
|
||||||
app.UseCors("AllowAll");
|
|
||||||
|
|
||||||
app.MapControllers()
|
|
||||||
.WithApiVersionSet(apiVersionSet)
|
|
||||||
.MapToApiVersion(2);
|
|
||||||
|
|
||||||
app.UseSwagger();
|
|
||||||
app.UseSwaggerUI(options =>
|
|
||||||
{
|
|
||||||
options.SwaggerEndpoint(
|
|
||||||
$"/swagger/v2/swagger.json", "v2");
|
|
||||||
});
|
|
||||||
|
|
||||||
app.UseHttpsRedirection();
|
|
||||||
|
|
||||||
app.UseMiddleware<RequestTimeMiddleware>();
|
|
||||||
|
|
||||||
using (var scope = app.Services.CreateScope())
|
|
||||||
{
|
|
||||||
var db = scope.ServiceProvider.GetRequiredService<PgsqlContext>();
|
|
||||||
db.Database.Migrate();
|
|
||||||
}
|
|
||||||
|
|
||||||
using (var scope = app.Services.CreateScope())
|
|
||||||
{
|
|
||||||
PgsqlContext context = scope.ServiceProvider.GetService<PgsqlContext>()!;
|
|
||||||
|
|
||||||
MangaConnector[] connectors =
|
|
||||||
[
|
|
||||||
new AsuraToon(),
|
|
||||||
new Bato(),
|
|
||||||
new MangaDex(),
|
|
||||||
new MangaHere(),
|
|
||||||
new MangaKatana(),
|
|
||||||
new Mangaworld(),
|
|
||||||
new ManhuaPlus(),
|
|
||||||
new Weebcentral(),
|
|
||||||
new Manganato(),
|
|
||||||
new Global(scope.ServiceProvider.GetService<PgsqlContext>()!)
|
|
||||||
];
|
|
||||||
MangaConnector[] newConnectors = connectors.Where(c => !context.MangaConnectors.Contains(c)).ToArray();
|
|
||||||
context.MangaConnectors.AddRange(newConnectors);
|
|
||||||
|
|
||||||
context.Jobs.AddRange(context.Mangas.AsEnumerable().Select(m => new UpdateFilesDownloadedJob(0, m.MangaId)));
|
|
||||||
|
|
||||||
context.Jobs.RemoveRange(context.Jobs.Where(j => j.state == JobState.Completed && j.RecurrenceMs < 1));
|
|
||||||
|
|
||||||
if (!context.LocalLibraries.Any())
|
|
||||||
context.LocalLibraries.Add(new LocalLibrary(TrangaSettings.downloadLocation, "Default Library"));
|
|
||||||
|
|
||||||
string[] emojis = { "(•‿•)", "(づ \u25d5‿\u25d5 )づ", "( \u02d8\u25bd\u02d8)っ\u2668", "=\uff3e\u25cf \u22cf \u25cf\uff3e=", "(ΦωΦ)", "(\u272a\u3268\u272a)", "( ノ・o・ )ノ", "(〜^\u2207^ )〜", "~(\u2267ω\u2266)~","૮ \u00b4• ﻌ \u00b4• ა", "(\u02c3ᆺ\u02c2)", "(=\ud83d\udf66 \u0f1d \ud83d\udf66=)"};
|
|
||||||
context.Notifications.Add(new Notification("Tranga Started", emojis[Random.Shared.Next(0, emojis.Length - 1)], NotificationUrgency.High));
|
|
||||||
|
|
||||||
context.SaveChanges();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
TrangaSettings.Load();
|
|
||||||
Tranga.StartLogger();
|
|
||||||
Tranga.JobStarterThread.Start(app.Services);
|
|
||||||
Tranga.NotificationSenderThread.Start(app.Services);
|
|
||||||
|
|
||||||
app.UseCors("AllowAll");
|
|
||||||
|
|
||||||
app.Run();
|
|
@ -1,47 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "http://json.schemastore.org/launchsettings.json",
|
|
||||||
"iisSettings": {
|
|
||||||
"windowsAuthentication": false,
|
|
||||||
"anonymousAuthentication": true,
|
|
||||||
"iisExpress": {
|
|
||||||
"applicationUrl": "http://localhost:5976",
|
|
||||||
"sslPort": 44332,
|
|
||||||
"environmentVariables": {
|
|
||||||
"POSTGRES_Host": "localhost:5432"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"profiles": {
|
|
||||||
"http": {
|
|
||||||
"commandName": "Project",
|
|
||||||
"dotnetRunMessages": true,
|
|
||||||
"launchBrowser": true,
|
|
||||||
"launchUrl": "swagger",
|
|
||||||
"applicationUrl": "http://localhost:5287",
|
|
||||||
"environmentVariables": {
|
|
||||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
|
||||||
"POSTGRES_Host": "localhost:5432"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"https": {
|
|
||||||
"commandName": "Project",
|
|
||||||
"dotnetRunMessages": true,
|
|
||||||
"launchBrowser": true,
|
|
||||||
"launchUrl": "swagger",
|
|
||||||
"applicationUrl": "https://localhost:7206;http://localhost:5287",
|
|
||||||
"environmentVariables": {
|
|
||||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
|
||||||
"POSTGRES_Host": "localhost:5432"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"IIS Express": {
|
|
||||||
"commandName": "IISExpress",
|
|
||||||
"launchBrowser": true,
|
|
||||||
"launchUrl": "swagger",
|
|
||||||
"environmentVariables": {
|
|
||||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
|
||||||
"POSTGRES_Host": "localhost:5432"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,15 +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;
|
|
||||||
}
|
|
@ -1,218 +0,0 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
|
||||||
using System.ComponentModel.DataAnnotations.Schema;
|
|
||||||
using System.Text;
|
|
||||||
using System.Text.RegularExpressions;
|
|
||||||
using System.Xml.Linq;
|
|
||||||
using API.Schema.Jobs;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Newtonsoft.Json;
|
|
||||||
|
|
||||||
namespace API.Schema;
|
|
||||||
|
|
||||||
[PrimaryKey("ChapterId")]
|
|
||||||
public class Chapter : IComparable<Chapter>
|
|
||||||
{
|
|
||||||
public Chapter(Manga parentManga, string url, string chapterNumber, int? volumeNumber = null, string? title = null)
|
|
||||||
: this(parentManga.MangaId, url, chapterNumber, volumeNumber, title)
|
|
||||||
{
|
|
||||||
ParentManga = parentManga;
|
|
||||||
FileName = GetArchiveFilePath(parentManga.Name);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Chapter(string parentMangaId, string url, string chapterNumber,
|
|
||||||
int? volumeNumber = null, string? title = null)
|
|
||||||
{
|
|
||||||
ChapterId = TokenGen.CreateToken(typeof(Chapter), parentMangaId, (volumeNumber ?? 0).ToString(), chapterNumber);
|
|
||||||
ParentMangaId = parentMangaId;
|
|
||||||
Url = url;
|
|
||||||
ChapterNumber = chapterNumber;
|
|
||||||
VolumeNumber = volumeNumber;
|
|
||||||
Title = title;
|
|
||||||
}
|
|
||||||
|
|
||||||
[StringLength(64)]
|
|
||||||
[Required]
|
|
||||||
public string ChapterId { get; init; }
|
|
||||||
public int? VolumeNumber { get; private set; }
|
|
||||||
[StringLength(10)]
|
|
||||||
[Required]
|
|
||||||
public string ChapterNumber { get; private set; }
|
|
||||||
|
|
||||||
[StringLength(2048)]
|
|
||||||
[Required]
|
|
||||||
[Url]
|
|
||||||
public string Url { get; internal set; }
|
|
||||||
[StringLength(256)]
|
|
||||||
public string? Title { get; private set; }
|
|
||||||
[StringLength(256)]
|
|
||||||
[Required]
|
|
||||||
public string FileName { get; private set; }
|
|
||||||
[JsonIgnore]
|
|
||||||
[NotMapped]
|
|
||||||
public string? FullArchiveFilePath => ParentManga is { } m ? Path.Join(m.FullDirectoryPath, FileName) : null;
|
|
||||||
[Required]
|
|
||||||
public bool Downloaded { get; internal set; } = false;
|
|
||||||
[Required]
|
|
||||||
[StringLength(64)]
|
|
||||||
public string ParentMangaId { get; internal set; }
|
|
||||||
[JsonIgnore]
|
|
||||||
public Manga? ParentManga { get; init; }
|
|
||||||
|
|
||||||
public int CompareTo(Chapter? other)
|
|
||||||
{
|
|
||||||
if (other is not { } otherChapter)
|
|
||||||
throw new ArgumentException($"{other} can not be compared to {this}");
|
|
||||||
return VolumeNumber?.CompareTo(otherChapter.VolumeNumber) switch
|
|
||||||
{
|
|
||||||
< 0 => -1,
|
|
||||||
> 0 => 1,
|
|
||||||
_ => CompareChapterNumbers(ChapterNumber, otherChapter.ChapterNumber)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public MoveFileOrFolderJob? UpdateChapterNumber(string chapterNumber)
|
|
||||||
{
|
|
||||||
ChapterNumber = chapterNumber;
|
|
||||||
return UpdateArchiveFileName();
|
|
||||||
}
|
|
||||||
|
|
||||||
public MoveFileOrFolderJob? UpdateVolumeNumber(int? volumeNumber)
|
|
||||||
{
|
|
||||||
VolumeNumber = volumeNumber;
|
|
||||||
return UpdateArchiveFileName();
|
|
||||||
}
|
|
||||||
|
|
||||||
public MoveFileOrFolderJob? UpdateTitle(string? title)
|
|
||||||
{
|
|
||||||
Title = title;
|
|
||||||
return UpdateArchiveFileName();
|
|
||||||
}
|
|
||||||
|
|
||||||
internal MoveFileOrFolderJob? UpdateArchiveFileName()
|
|
||||||
{
|
|
||||||
string? oldPath = FullArchiveFilePath;
|
|
||||||
if (oldPath is null)
|
|
||||||
return null;
|
|
||||||
string newPath = GetArchiveFilePath();
|
|
||||||
FileName = newPath;
|
|
||||||
return Downloaded ? new MoveFileOrFolderJob(oldPath, newPath) : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Checks the filesystem if an archive at the ArchiveFilePath exists
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>True if archive exists on disk</returns>
|
|
||||||
public bool IsDownloaded()
|
|
||||||
{
|
|
||||||
string path = GetArchiveFilePath();
|
|
||||||
return File.Exists(path);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Placeholders:
|
|
||||||
/// %M Manga Name
|
|
||||||
/// %V Volume
|
|
||||||
/// %C Chapter
|
|
||||||
/// %T Title
|
|
||||||
/// %A Author (first in list)
|
|
||||||
/// %I Chapter Internal ID
|
|
||||||
/// %i Manga Internal ID
|
|
||||||
/// %Y Year (Manga)
|
|
||||||
private static readonly Regex NullableRex = new(@"\?([a-zA-Z])\(([^\)]*)\)|(.+?)");
|
|
||||||
private static readonly Regex ReplaceRexx = new(@"%([a-zA-Z])|(.+?)");
|
|
||||||
private string GetArchiveFilePath(string? parentMangaName = null)
|
|
||||||
{
|
|
||||||
string archiveNamingScheme = TrangaSettings.chapterNamingScheme;
|
|
||||||
StringBuilder stringBuilder = new();
|
|
||||||
foreach (Match nullable in NullableRex.Matches(archiveNamingScheme))
|
|
||||||
{
|
|
||||||
if (nullable.Groups[3].Success)
|
|
||||||
{
|
|
||||||
stringBuilder.Append(nullable.Groups[3].Value);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
char placeholder = nullable.Groups[1].Value[0];
|
|
||||||
bool isNull = placeholder switch
|
|
||||||
{
|
|
||||||
'M' => ParentManga?.Name is null && parentMangaName is null,
|
|
||||||
'V' => VolumeNumber is null,
|
|
||||||
'C' => ChapterNumber is null,
|
|
||||||
'T' => Title is null,
|
|
||||||
'A' => ParentManga?.Authors?.FirstOrDefault()?.AuthorName is null,
|
|
||||||
'I' => ChapterId is null,
|
|
||||||
'i' => ParentMangaId is null,
|
|
||||||
'Y' => ParentManga?.Year is null,
|
|
||||||
_ => true
|
|
||||||
};
|
|
||||||
if(!isNull)
|
|
||||||
stringBuilder.Append(nullable.Groups[2].Value);
|
|
||||||
}
|
|
||||||
|
|
||||||
string checkedString = stringBuilder.ToString();
|
|
||||||
stringBuilder = new();
|
|
||||||
|
|
||||||
foreach (Match replace in ReplaceRexx.Matches(checkedString))
|
|
||||||
{
|
|
||||||
if (replace.Groups[2].Success)
|
|
||||||
{
|
|
||||||
stringBuilder.Append(replace.Groups[2].Value);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
char placeholder = replace.Groups[1].Value[0];
|
|
||||||
string? value = placeholder switch
|
|
||||||
{
|
|
||||||
'M' => ParentManga?.Name ?? parentMangaName,
|
|
||||||
'V' => VolumeNumber?.ToString(),
|
|
||||||
'C' => ChapterNumber,
|
|
||||||
'T' => Title,
|
|
||||||
'A' => ParentManga?.Authors?.FirstOrDefault()?.AuthorName,
|
|
||||||
'I' => ChapterId,
|
|
||||||
'i' => ParentMangaId,
|
|
||||||
'Y' => ParentManga?.Year.ToString(),
|
|
||||||
_ => null
|
|
||||||
};
|
|
||||||
stringBuilder.Append(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
stringBuilder.Append(".cbz");
|
|
||||||
|
|
||||||
return stringBuilder.ToString();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static int CompareChapterNumbers(string ch1, string ch2)
|
|
||||||
{
|
|
||||||
int[] ch1Arr = ch1.Split('.').Select(c => int.TryParse(c, out int result) ? result : -1).ToArray();
|
|
||||||
int[] ch2Arr = ch2.Split('.').Select(c => int.TryParse(c, out int result) ? result : -1).ToArray();
|
|
||||||
|
|
||||||
if (ch1Arr.Contains(-1) || ch2Arr.Contains(-1))
|
|
||||||
throw new ArgumentException("Chapter number is not in correct format");
|
|
||||||
|
|
||||||
int i = 0, j = 0;
|
|
||||||
|
|
||||||
while (i < ch1Arr.Length && j < ch2Arr.Length)
|
|
||||||
{
|
|
||||||
if (ch1Arr[i] < ch2Arr[j])
|
|
||||||
return -1;
|
|
||||||
if (ch1Arr[i] > ch2Arr[j])
|
|
||||||
return 1;
|
|
||||||
i++;
|
|
||||||
j++;
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
);
|
|
||||||
return comicInfo.ToString();
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,21 +0,0 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
|
||||||
using Newtonsoft.Json;
|
|
||||||
|
|
||||||
namespace API.Schema.Jobs;
|
|
||||||
|
|
||||||
public class DownloadAvailableChaptersJob(ulong recurrenceMs, string mangaId, string? parentJobId = null, ICollection<string>? dependsOnJobsIds = null)
|
|
||||||
: Job(TokenGen.CreateToken(typeof(DownloadAvailableChaptersJob)), JobType.DownloadAvailableChaptersJob, recurrenceMs, parentJobId, dependsOnJobsIds)
|
|
||||||
{
|
|
||||||
[StringLength(64)]
|
|
||||||
[Required]
|
|
||||||
public string MangaId { get; init; } = mangaId;
|
|
||||||
|
|
||||||
[JsonIgnore]
|
|
||||||
public Manga? Manga { get; init; }
|
|
||||||
|
|
||||||
protected override IEnumerable<Job> RunInternal(PgsqlContext context)
|
|
||||||
{
|
|
||||||
return context.Chapters.Where(c => c.ParentMangaId == MangaId).AsEnumerable()
|
|
||||||
.Select(chapter => new DownloadSingleChapterJob(chapter.ChapterId, this.JobId));
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,29 +0,0 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
|
||||||
using Newtonsoft.Json;
|
|
||||||
|
|
||||||
namespace API.Schema.Jobs;
|
|
||||||
|
|
||||||
public class DownloadMangaCoverJob(string mangaId, string? parentJobId = null, ICollection<string>? dependsOnJobsIds = null)
|
|
||||||
: Job(TokenGen.CreateToken(typeof(DownloadMangaCoverJob)), JobType.DownloadMangaCoverJob, 0, parentJobId, dependsOnJobsIds)
|
|
||||||
{
|
|
||||||
[StringLength(64)]
|
|
||||||
[Required]
|
|
||||||
public string MangaId { get; init; } = mangaId;
|
|
||||||
[JsonIgnore]
|
|
||||||
public Manga? Manga { get; init; }
|
|
||||||
|
|
||||||
protected override IEnumerable<Job> RunInternal(PgsqlContext context)
|
|
||||||
{
|
|
||||||
Manga? manga = Manga ?? context.Mangas.Find(this.MangaId);
|
|
||||||
if (manga is null)
|
|
||||||
{
|
|
||||||
Log.Error($"Manga {this.MangaId} not found.");
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
manga.CoverFileNameInCache = manga.SaveCoverImageToCache();
|
|
||||||
context.SaveChanges();
|
|
||||||
Log.Info($"Saved cover for Manga {this.MangaId} to cache at {manga.CoverFileNameInCache}.");
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,175 +0,0 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
|
||||||
using System.IO.Compression;
|
|
||||||
using System.Runtime.InteropServices;
|
|
||||||
using API.MangaDownloadClients;
|
|
||||||
using API.Schema.MangaConnectors;
|
|
||||||
using Newtonsoft.Json;
|
|
||||||
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;
|
|
||||||
|
|
||||||
public class DownloadSingleChapterJob(string chapterId, string? parentJobId = null, ICollection<string>? dependsOnJobsIds = null)
|
|
||||||
: Job(TokenGen.CreateToken(typeof(DownloadSingleChapterJob)), JobType.DownloadSingleChapterJob, 0, parentJobId, dependsOnJobsIds)
|
|
||||||
{
|
|
||||||
[StringLength(64)]
|
|
||||||
[Required]
|
|
||||||
public string ChapterId { get; init; } = chapterId;
|
|
||||||
|
|
||||||
[JsonIgnore]
|
|
||||||
public Chapter? Chapter { get; init; }
|
|
||||||
|
|
||||||
protected override IEnumerable<Job> RunInternal(PgsqlContext context)
|
|
||||||
{
|
|
||||||
Chapter? chapter = Chapter ?? context.Chapters.Find(ChapterId);
|
|
||||||
if (chapter is null)
|
|
||||||
{
|
|
||||||
Log.Error("Chapter is null.");
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
Manga? manga = chapter.ParentManga ?? context.Mangas.Find(chapter.ParentMangaId);
|
|
||||||
if (manga is null)
|
|
||||||
{
|
|
||||||
Log.Error("Manga is null.");
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
MangaConnector? connector = manga.MangaConnector ?? context.MangaConnectors.Find(manga.MangaConnectorId);
|
|
||||||
if (connector is null)
|
|
||||||
{
|
|
||||||
Log.Error("Connector is null.");
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
string[] imageUrls = connector.GetChapterImageUrls(chapter);
|
|
||||||
if (imageUrls.Length < 1)
|
|
||||||
{
|
|
||||||
Log.Info($"No imageUrls for chapter {chapterId}");
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
string? saveArchiveFilePath = chapter.FullArchiveFilePath;
|
|
||||||
if (saveArchiveFilePath is null)
|
|
||||||
{
|
|
||||||
Log.Error("saveArchiveFilePath is null.");
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
//Check if Publication Directory already exists
|
|
||||||
string directoryPath = Path.GetDirectoryName(saveArchiveFilePath)!;
|
|
||||||
if (!Directory.Exists(directoryPath))
|
|
||||||
{
|
|
||||||
Log.Info($"Creating publication Directory: {directoryPath}");
|
|
||||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
|
||||||
Directory.CreateDirectory(directoryPath,
|
|
||||||
UserRead | UserWrite | UserExecute | GroupRead | GroupWrite | GroupExecute );
|
|
||||||
else
|
|
||||||
Directory.CreateDirectory(directoryPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (File.Exists(saveArchiveFilePath)) //Don't download twice. Redownload
|
|
||||||
{
|
|
||||||
Log.Info($"Archive {saveArchiveFilePath} already existed, but deleting and re-downloading.");
|
|
||||||
File.Delete(saveArchiveFilePath);
|
|
||||||
}
|
|
||||||
|
|
||||||
//Create a temporary folder to store images
|
|
||||||
string tempFolder = Directory.CreateTempSubdirectory("trangatemp").FullName;
|
|
||||||
Log.Debug($"Created temp folder: {tempFolder}");
|
|
||||||
|
|
||||||
Log.Info($"Downloading images: {ChapterId}");
|
|
||||||
int chapterNum = 0;
|
|
||||||
//Download all Images to temporary Folder
|
|
||||||
foreach (string imageUrl in imageUrls)
|
|
||||||
{
|
|
||||||
string extension = imageUrl.Split('.')[^1].Split('?')[0];
|
|
||||||
string imagePath = Path.Join(tempFolder, $"{chapterNum++}.{extension}");
|
|
||||||
bool status = DownloadImage(imageUrl, imagePath);
|
|
||||||
if (status is false)
|
|
||||||
{
|
|
||||||
Log.Error($"Failed to download image: {imageUrl}");
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
CopyCoverFromCacheToDownloadLocation(manga);
|
|
||||||
|
|
||||||
Log.Debug($"Creating ComicInfo.xml {ChapterId}");
|
|
||||||
File.WriteAllText(Path.Join(tempFolder, "ComicInfo.xml"), chapter.GetComicInfoXmlString());
|
|
||||||
|
|
||||||
Log.Debug($"Packaging images to archive {ChapterId}");
|
|
||||||
//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();
|
|
||||||
|
|
||||||
return [new UpdateFilesDownloadedJob(0, manga.MangaId, this.JobId)];
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ProcessImage(string imagePath)
|
|
||||||
{
|
|
||||||
if (!TrangaSettings.bwImages && TrangaSettings.compression == 100)
|
|
||||||
{
|
|
||||||
Log.Debug($"No processing requested for image");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Log.Debug($"Processing image: {imagePath}");
|
|
||||||
|
|
||||||
using Image image = Image.Load(imagePath);
|
|
||||||
File.Delete(imagePath);
|
|
||||||
if(TrangaSettings.bwImages)
|
|
||||||
image.Mutate(i => i.ApplyProcessor(new AdaptiveThresholdProcessor()));
|
|
||||||
image.SaveAsJpeg(imagePath, new JpegEncoder()
|
|
||||||
{
|
|
||||||
Quality = TrangaSettings.compression
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void CopyCoverFromCacheToDownloadLocation(Manga manga)
|
|
||||||
{
|
|
||||||
//Check if Publication already has a Folder and cover
|
|
||||||
string publicationFolder = manga.CreatePublicationFolder();
|
|
||||||
DirectoryInfo dirInfo = new (publicationFolder);
|
|
||||||
if (dirInfo.EnumerateFiles().Any(info => info.Name.Contains("cover", StringComparison.InvariantCultureIgnoreCase)))
|
|
||||||
{
|
|
||||||
Log.Debug($"Cover already exists at {publicationFolder}");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Log.Info($"Copying cover to {publicationFolder}");
|
|
||||||
string? fileInCache = manga.CoverFileNameInCache ?? manga.SaveCoverImageToCache();
|
|
||||||
if (fileInCache is null)
|
|
||||||
{
|
|
||||||
Log.Error($"File {fileInCache} does not exist");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
Log.Debug($"Copied cover from {fileInCache} to {newFilePath}");
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool DownloadImage(string imageUrl, string savePath)
|
|
||||||
{
|
|
||||||
HttpDownloadClient downloadClient = new();
|
|
||||||
RequestResult requestResult = downloadClient.MakeRequest(imageUrl, RequestType.MangaImage);
|
|
||||||
|
|
||||||
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
|
|
||||||
return false;
|
|
||||||
if (requestResult.result == Stream.Null)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
FileStream fs = new (savePath, FileMode.Create, FileAccess.Write, FileShare.None);
|
|
||||||
requestResult.result.CopyTo(fs);
|
|
||||||
fs.Close();
|
|
||||||
ProcessImage(savePath);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,86 +0,0 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
|
||||||
using System.ComponentModel.DataAnnotations.Schema;
|
|
||||||
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; }
|
|
||||||
[StringLength(64)]
|
|
||||||
public ICollection<string>? DependsOnJobsIds { 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.Waiting;
|
|
||||||
[Required]
|
|
||||||
public bool Enabled { get; internal set; } = true;
|
|
||||||
|
|
||||||
[NotMapped]
|
|
||||||
[JsonIgnore]
|
|
||||||
protected ILog Log { get; init; }
|
|
||||||
|
|
||||||
public Job(string jobId, JobType jobType, ulong recurrenceMs, Job? parentJob = null, ICollection<Job>? dependsOnJobs = null)
|
|
||||||
: this(jobId, jobType, recurrenceMs, parentJob?.JobId, dependsOnJobs?.Select(j => j.JobId).ToList())
|
|
||||||
{
|
|
||||||
this.ParentJob = parentJob;
|
|
||||||
this.DependsOnJobs = dependsOnJobs;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Job(string jobId, JobType jobType, ulong recurrenceMs, string? parentJobId = null, ICollection<string>? dependsOnJobsIds = null)
|
|
||||||
{
|
|
||||||
Log = LogManager.GetLogger(GetType());
|
|
||||||
JobId = jobId;
|
|
||||||
ParentJobId = parentJobId;
|
|
||||||
DependsOnJobsIds = dependsOnJobsIds;
|
|
||||||
JobType = jobType;
|
|
||||||
RecurrenceMs = recurrenceMs;
|
|
||||||
}
|
|
||||||
|
|
||||||
public IEnumerable<Job> Run(IServiceProvider serviceProvider)
|
|
||||||
{
|
|
||||||
Log.Debug($"Running job {JobId}");
|
|
||||||
using IServiceScope scope = serviceProvider.CreateScope();
|
|
||||||
PgsqlContext context = scope.ServiceProvider.GetRequiredService<PgsqlContext>();
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
this.state = JobState.Running;
|
|
||||||
context.SaveChanges();
|
|
||||||
Job[] newJobs = RunInternal(context).ToArray();
|
|
||||||
this.state = JobState.Completed;
|
|
||||||
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);
|
|
||||||
}
|
|
@ -1,13 +0,0 @@
|
|||||||
namespace API.Schema.Jobs;
|
|
||||||
|
|
||||||
public enum JobState : byte
|
|
||||||
{
|
|
||||||
//Values 0-63 Preparation Stages
|
|
||||||
Waiting = 0,
|
|
||||||
//64-127 Running Stages
|
|
||||||
Running = 64,
|
|
||||||
//128-191 Completion Stages
|
|
||||||
Completed = 128,
|
|
||||||
//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,53 +0,0 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
|
||||||
|
|
||||||
namespace API.Schema.Jobs;
|
|
||||||
|
|
||||||
public class MoveFileOrFolderJob(string fromLocation, string toLocation, string? parentJobId = null, ICollection<string>? dependsOnJobsIds = null)
|
|
||||||
: Job(TokenGen.CreateToken(typeof(MoveFileOrFolderJob)), JobType.MoveFileOrFolderJob, 0, parentJobId, dependsOnJobsIds)
|
|
||||||
{
|
|
||||||
[StringLength(256)]
|
|
||||||
[Required]
|
|
||||||
public string FromLocation { get; init; } = fromLocation;
|
|
||||||
[StringLength(256)]
|
|
||||||
[Required]
|
|
||||||
public string ToLocation { get; init; } = 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,45 +0,0 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
|
|
||||||
namespace API.Schema.Jobs;
|
|
||||||
|
|
||||||
public class MoveMangaLibraryJob(string mangaId, string toLibraryId, string? parentJobId = null, ICollection<string>? dependsOnJobsIds = null)
|
|
||||||
: Job(TokenGen.CreateToken(typeof(MoveMangaLibraryJob)), JobType.MoveMangaLibraryJob, 0, parentJobId, dependsOnJobsIds)
|
|
||||||
{
|
|
||||||
[StringLength(64)]
|
|
||||||
[Required]
|
|
||||||
public string MangaId { get; init; } = mangaId;
|
|
||||||
[StringLength(64)]
|
|
||||||
[Required]
|
|
||||||
public string ToLibraryId { get; init; } = toLibraryId;
|
|
||||||
|
|
||||||
protected override IEnumerable<Job> RunInternal(PgsqlContext context)
|
|
||||||
{
|
|
||||||
Manga? manga = context.Mangas.Find(MangaId);
|
|
||||||
if (manga is null)
|
|
||||||
{
|
|
||||||
Log.Error("Manga not found");
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
LocalLibrary? library = context.LocalLibraries.Find(ToLibraryId);
|
|
||||||
if (library is null)
|
|
||||||
{
|
|
||||||
Log.Error("LocalLibrary not found");
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
Chapter[] chapters = context.Chapters.Where(c => c.ParentMangaId == MangaId).ToArray();
|
|
||||||
Dictionary<Chapter, string> oldPath = chapters.ToDictionary(c => c, c => c.FullArchiveFilePath!);
|
|
||||||
manga.Library = library;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
context.SaveChanges();
|
|
||||||
}
|
|
||||||
catch (DbUpdateException e)
|
|
||||||
{
|
|
||||||
Log.Error(e);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
return chapters.Select(c => new MoveFileOrFolderJob(oldPath[c], c.FullArchiveFilePath!));
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,52 +0,0 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
|
||||||
using API.Schema.MangaConnectors;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Newtonsoft.Json;
|
|
||||||
|
|
||||||
namespace API.Schema.Jobs;
|
|
||||||
|
|
||||||
public class RetrieveChaptersJob(ulong recurrenceMs, string mangaId, string? parentJobId = null, ICollection<string>? dependsOnJobsIds = null)
|
|
||||||
: Job(TokenGen.CreateToken(typeof(RetrieveChaptersJob)), JobType.RetrieveChaptersJob, recurrenceMs, parentJobId, dependsOnJobsIds)
|
|
||||||
{
|
|
||||||
[StringLength(64)]
|
|
||||||
[Required]
|
|
||||||
public string MangaId { get; init; } = mangaId;
|
|
||||||
|
|
||||||
[JsonIgnore]
|
|
||||||
public Manga? Manga { get; init; }
|
|
||||||
|
|
||||||
protected override IEnumerable<Job> RunInternal(PgsqlContext context)
|
|
||||||
{
|
|
||||||
Manga? manga = Manga ?? context.Mangas.Find(MangaId);
|
|
||||||
if (manga is null)
|
|
||||||
{
|
|
||||||
Log.Error("Manga is null.");
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
MangaConnector? connector = manga.MangaConnector ?? context.MangaConnectors.Find(manga.MangaConnectorId);
|
|
||||||
if (connector is null)
|
|
||||||
{
|
|
||||||
Log.Error("Connector is null.");
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
// This gets all chapters that are not downloaded
|
|
||||||
Chapter[] allNewChapters = connector.GetNewChapters(manga).DistinctBy(c => c.ChapterId).ToArray();
|
|
||||||
Log.Info($"{allNewChapters.Length} new chapters.");
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// This filters out chapters that are not downloaded but already exist in the DB
|
|
||||||
string[] chapterIds = context.Chapters.Where(chapter => chapter.ParentMangaId == manga.MangaId)
|
|
||||||
.Select(chapter => chapter.ChapterId).ToArray();
|
|
||||||
Chapter[] newChapters = allNewChapters.Where(chapter => !chapterIds.Contains(chapter.ChapterId)).ToArray();
|
|
||||||
context.Chapters.AddRange(newChapters);
|
|
||||||
context.SaveChanges();
|
|
||||||
}
|
|
||||||
catch (DbUpdateException e)
|
|
||||||
{
|
|
||||||
Log.Error(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,25 +0,0 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
|
||||||
using Newtonsoft.Json;
|
|
||||||
|
|
||||||
namespace API.Schema.Jobs;
|
|
||||||
|
|
||||||
public class UpdateFilesDownloadedJob(ulong recurrenceMs, string mangaId, string? parentJobId = null, ICollection<string>? dependsOnJobsIds = null)
|
|
||||||
: Job(TokenGen.CreateToken(typeof(UpdateFilesDownloadedJob)), JobType.UpdateFilesDownloadedJob, recurrenceMs, parentJobId, dependsOnJobsIds)
|
|
||||||
{
|
|
||||||
[StringLength(64)]
|
|
||||||
[Required]
|
|
||||||
public string MangaId { get; init; } = mangaId;
|
|
||||||
|
|
||||||
[JsonIgnore]
|
|
||||||
public virtual Manga? Manga { get; init; }
|
|
||||||
|
|
||||||
protected override IEnumerable<Job> RunInternal(PgsqlContext context)
|
|
||||||
{
|
|
||||||
IQueryable<Chapter> chapters = context.Chapters.Where(c => c.ParentMangaId == MangaId);
|
|
||||||
foreach (Chapter chapter in chapters)
|
|
||||||
chapter.Downloaded = chapter.IsDownloaded();
|
|
||||||
|
|
||||||
context.SaveChanges();
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,27 +0,0 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
|
||||||
using Newtonsoft.Json;
|
|
||||||
|
|
||||||
namespace API.Schema.Jobs;
|
|
||||||
|
|
||||||
public class UpdateMetadataJob(ulong recurrenceMs, string mangaId, string? parentJobId = null, ICollection<string>? dependsOnJobsIds = null)
|
|
||||||
: Job(TokenGen.CreateToken(typeof(UpdateMetadataJob)), JobType.UpdateMetaDataJob, recurrenceMs, parentJobId, dependsOnJobsIds)
|
|
||||||
{
|
|
||||||
[StringLength(64)]
|
|
||||||
[Required]
|
|
||||||
public string MangaId { get; init; } = mangaId;
|
|
||||||
|
|
||||||
[JsonIgnore]
|
|
||||||
public virtual Manga? Manga { get; init; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Updates all data related to Manga.
|
|
||||||
/// Retrieves data from Mangaconnector
|
|
||||||
/// Updates Chapter-info
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="context"></param>
|
|
||||||
protected override IEnumerable<Job> RunInternal(PgsqlContext context)
|
|
||||||
{
|
|
||||||
Log.Warn("NOT IMPLEMENTED.");
|
|
||||||
return [];//TODO
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,25 +0,0 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
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,69 +0,0 @@
|
|||||||
using System.Net;
|
|
||||||
using System.Net.Http.Headers;
|
|
||||||
|
|
||||||
namespace API.Schema.LibraryConnectors;
|
|
||||||
|
|
||||||
public class NetClient
|
|
||||||
{
|
|
||||||
public static Stream MakeRequest(string url, string authScheme, string auth)
|
|
||||||
{
|
|
||||||
HttpClient client = new();
|
|
||||||
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(authScheme, auth);
|
|
||||||
|
|
||||||
HttpRequestMessage requestMessage = new ()
|
|
||||||
{
|
|
||||||
Method = HttpMethod.Get,
|
|
||||||
RequestUri = new Uri(url)
|
|
||||||
};
|
|
||||||
try
|
|
||||||
{
|
|
||||||
|
|
||||||
HttpResponseMessage response = client.Send(requestMessage);
|
|
||||||
|
|
||||||
if (response.StatusCode is HttpStatusCode.Unauthorized &&
|
|
||||||
response.RequestMessage!.RequestUri!.AbsoluteUri != url)
|
|
||||||
return MakeRequest(response.RequestMessage!.RequestUri!.AbsoluteUri, authScheme, auth);
|
|
||||||
else if (response.IsSuccessStatusCode)
|
|
||||||
return response.Content.ReadAsStream();
|
|
||||||
else
|
|
||||||
return Stream.Null;
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
switch (e)
|
|
||||||
{
|
|
||||||
case HttpRequestException:
|
|
||||||
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
return Stream.Null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static bool MakePost(string url, string authScheme, string auth)
|
|
||||||
{
|
|
||||||
HttpClient client = new()
|
|
||||||
{
|
|
||||||
DefaultRequestHeaders =
|
|
||||||
{
|
|
||||||
{ "Accept", "application/json" },
|
|
||||||
{ "Authorization", new AuthenticationHeaderValue(authScheme, auth).ToString() }
|
|
||||||
}
|
|
||||||
};
|
|
||||||
HttpRequestMessage requestMessage = new ()
|
|
||||||
{
|
|
||||||
Method = HttpMethod.Post,
|
|
||||||
RequestUri = new Uri(url)
|
|
||||||
};
|
|
||||||
HttpResponseMessage response = client.Send(requestMessage);
|
|
||||||
|
|
||||||
if(response.StatusCode is HttpStatusCode.Unauthorized && response.RequestMessage!.RequestUri!.AbsoluteUri != url)
|
|
||||||
return MakePost(response.RequestMessage!.RequestUri!.AbsoluteUri, authScheme, auth);
|
|
||||||
else if (response.IsSuccessStatusCode)
|
|
||||||
return true;
|
|
||||||
else
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,19 +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;
|
|
||||||
}
|
|
@ -1,17 +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;
|
|
||||||
}
|
|
@ -1,185 +0,0 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
|
||||||
using System.ComponentModel.DataAnnotations.Schema;
|
|
||||||
using System.Diagnostics.CodeAnalysis;
|
|
||||||
using System.Net;
|
|
||||||
using System.Runtime.CompilerServices;
|
|
||||||
using System.Runtime.InteropServices;
|
|
||||||
using System.Text.RegularExpressions;
|
|
||||||
using API.MangaDownloadClients;
|
|
||||||
using API.Schema.Jobs;
|
|
||||||
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 set; }
|
|
||||||
[JsonIgnore]
|
|
||||||
[Url]
|
|
||||||
public string CoverUrl { get; internal set; }
|
|
||||||
[JsonIgnore]
|
|
||||||
public string? CoverFileNameInCache { get; internal set; }
|
|
||||||
[Required]
|
|
||||||
public uint Year { get; internal set; }
|
|
||||||
[StringLength(8)]
|
|
||||||
public string? OriginalLanguage { get; internal set; }
|
|
||||||
[Required]
|
|
||||||
public MangaReleaseStatus ReleaseStatus { get; internal set; }
|
|
||||||
[StringLength(1024)]
|
|
||||||
[Required]
|
|
||||||
public string DirectoryName { get; private set; }
|
|
||||||
public LocalLibrary? Library { get; internal set; }
|
|
||||||
[JsonIgnore]
|
|
||||||
[NotMapped]
|
|
||||||
public string LibraryPath => Library is null ? TrangaSettings.downloadLocation : Library.BasePath;
|
|
||||||
[JsonIgnore]
|
|
||||||
[NotMapped]
|
|
||||||
public string FullDirectoryPath => Path.Join(LibraryPath, DirectoryName);
|
|
||||||
[Required]
|
|
||||||
public float IgnoreChapterBefore { get; internal set; }
|
|
||||||
[StringLength(64)]
|
|
||||||
[Required]
|
|
||||||
public string MangaConnectorId { get; private set; }
|
|
||||||
[JsonIgnore] public MangaConnector? MangaConnector { get; private set; }
|
|
||||||
|
|
||||||
[JsonIgnore] public ICollection<Author>? Authors { get; internal set; }
|
|
||||||
[NotMapped]
|
|
||||||
[StringLength(64)]
|
|
||||||
[Required]
|
|
||||||
public IEnumerable<string> AuthorIds => Authors?.Select(a => a.AuthorId) ?? [];
|
|
||||||
|
|
||||||
[JsonIgnore] public ICollection<MangaTag>? MangaTags { get; internal set; }
|
|
||||||
[NotMapped]
|
|
||||||
[StringLength(64)]
|
|
||||||
[Required]
|
|
||||||
public IEnumerable<string> Tags => MangaTags?.Select(t => t.Tag) ?? [];
|
|
||||||
|
|
||||||
|
|
||||||
[JsonIgnore] public ICollection<Link>? Links { get; internal set; }
|
|
||||||
[NotMapped]
|
|
||||||
[StringLength(64)]
|
|
||||||
[Required]
|
|
||||||
public IEnumerable<string> LinkIds => Links?.Select(l => l.LinkId) ?? [];
|
|
||||||
|
|
||||||
[JsonIgnore] public ICollection<MangaAltTitle>? AltTitles { get; internal set; }
|
|
||||||
[NotMapped]
|
|
||||||
[StringLength(64)]
|
|
||||||
[Required]
|
|
||||||
public IEnumerable<string> AltTitleIds => AltTitles?.Select(a => a.AltTitleId) ?? [];
|
|
||||||
|
|
||||||
public Manga(string idOnConnectorSite, string name, string description, string websiteUrl, string coverUrl,
|
|
||||||
string? coverFileNameInCache, uint year, string? originalLanguage, MangaReleaseStatus releaseStatus,
|
|
||||||
float ignoreChapterBefore, MangaConnector mangaConnector, ICollection<Author> authors,
|
|
||||||
ICollection<MangaTag> mangaTags, ICollection<Link> links, ICollection<MangaAltTitle> altTitles,
|
|
||||||
LocalLibrary? library = null)
|
|
||||||
: this(idOnConnectorSite, name, description, websiteUrl, coverUrl, coverFileNameInCache, year, originalLanguage,
|
|
||||||
releaseStatus, ignoreChapterBefore, mangaConnector.Name)
|
|
||||||
{
|
|
||||||
this.Authors = authors;
|
|
||||||
this.MangaTags = mangaTags;
|
|
||||||
this.Links = links;
|
|
||||||
this.AltTitles = altTitles;
|
|
||||||
this.Library = library;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Manga(string idOnConnectorSite, string name, string description, string websiteUrl, string coverUrl,
|
|
||||||
string? coverFileNameInCache, uint year, string? originalLanguage, MangaReleaseStatus releaseStatus,
|
|
||||||
float ignoreChapterBefore, string mangaConnectorId)
|
|
||||||
{
|
|
||||||
MangaId = TokenGen.CreateToken(typeof(Manga), mangaConnectorId, idOnConnectorSite);
|
|
||||||
IdOnConnectorSite = idOnConnectorSite;
|
|
||||||
Name = name;
|
|
||||||
Description = description;
|
|
||||||
WebsiteUrl = websiteUrl;
|
|
||||||
CoverUrl = coverUrl;
|
|
||||||
CoverFileNameInCache = coverFileNameInCache;
|
|
||||||
Year = year;
|
|
||||||
OriginalLanguage = originalLanguage;
|
|
||||||
ReleaseStatus = releaseStatus;
|
|
||||||
IgnoreChapterBefore = ignoreChapterBefore;
|
|
||||||
MangaConnectorId = mangaConnectorId;
|
|
||||||
DirectoryName = BuildFolderName(name);
|
|
||||||
}
|
|
||||||
|
|
||||||
public MoveFileOrFolderJob UpdateFolderName(string downloadLocation, string newName)
|
|
||||||
{
|
|
||||||
string oldName = this.DirectoryName;
|
|
||||||
this.DirectoryName = newName;
|
|
||||||
return new MoveFileOrFolderJob(Path.Join(downloadLocation, oldName), Path.Join(downloadLocation, this.DirectoryName));
|
|
||||||
}
|
|
||||||
|
|
||||||
internal void UpdateWithInfo(Manga other)
|
|
||||||
{
|
|
||||||
this.Name = other.Name;
|
|
||||||
this.Year = other.Year;
|
|
||||||
this.Description = other.Description;
|
|
||||||
this.CoverUrl = other.CoverUrl;
|
|
||||||
this.OriginalLanguage = other.OriginalLanguage;
|
|
||||||
this.Authors = other.Authors;
|
|
||||||
this.Links = other.Links;
|
|
||||||
this.MangaTags = other.MangaTags;
|
|
||||||
this.AltTitles = other.AltTitles;
|
|
||||||
this.ReleaseStatus = other.ReleaseStatus;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string BuildFolderName(string mangaName)
|
|
||||||
{
|
|
||||||
return mangaName;
|
|
||||||
}
|
|
||||||
|
|
||||||
internal string? SaveCoverImageToCache(int retries = 3)
|
|
||||||
{
|
|
||||||
if(retries < 0)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
Regex urlRex = new (@"https?:\/\/((?:[a-zA-Z0-9-]+\.)+[a-zA-Z0-9]+)\/(?:.+\/)*(.+\.([a-zA-Z]+))");
|
|
||||||
//https?:\/\/[a-zA-Z0-9-]+\.([a-zA-Z0-9-]+\.[a-zA-Z0-9]+)\/(?:.+\/)*(.+\.([a-zA-Z]+)) for only second level domains
|
|
||||||
Match match = urlRex.Match(CoverUrl);
|
|
||||||
string filename = $"{match.Groups[1].Value}-{MangaId}.{match.Groups[3].Value}";
|
|
||||||
string saveImagePath = Path.Join(TrangaSettings.coverImageCache, filename);
|
|
||||||
|
|
||||||
if (File.Exists(saveImagePath))
|
|
||||||
return saveImagePath;
|
|
||||||
|
|
||||||
RequestResult coverResult = new HttpDownloadClient().MakeRequest(CoverUrl, RequestType.MangaCover, $"https://{match.Groups[1].Value}");
|
|
||||||
if (coverResult.statusCode is < HttpStatusCode.OK or >= HttpStatusCode.Ambiguous)
|
|
||||||
return SaveCoverImageToCache(--retries);
|
|
||||||
|
|
||||||
using MemoryStream ms = new();
|
|
||||||
coverResult.result.CopyTo(ms);
|
|
||||||
Directory.CreateDirectory(TrangaSettings.coverImageCache);
|
|
||||||
File.WriteAllBytes(saveImagePath, ms.ToArray());
|
|
||||||
|
|
||||||
return saveImagePath;
|
|
||||||
}
|
|
||||||
|
|
||||||
public string CreatePublicationFolder()
|
|
||||||
{
|
|
||||||
string publicationFolder = Path.Join(LibraryPath, this.DirectoryName);
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
//TODO onchanges create job to update metadata files in archives, etc.
|
|
||||||
}
|
|
@ -1,19 +0,0 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
|
||||||
using System.ComponentModel.DataAnnotations.Schema;
|
|
||||||
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;
|
|
||||||
}
|
|
@ -1,55 +0,0 @@
|
|||||||
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, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)[] GetManga(string publicationTitle = "")
|
|
||||||
{
|
|
||||||
//Get all enabled Connectors
|
|
||||||
MangaConnector[] enabledConnectors = context.MangaConnectors.Where(c => c.Enabled && c.Name != "Global").ToArray();
|
|
||||||
|
|
||||||
//Create Task for each MangaConnector to search simulatneously
|
|
||||||
Task<(Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)[]>[] tasks =
|
|
||||||
enabledConnectors.Select(c =>
|
|
||||||
new Task<(Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)[]>(() => c.GetManga(publicationTitle))).ToArray();
|
|
||||||
foreach (var task in tasks)
|
|
||||||
task.Start();
|
|
||||||
|
|
||||||
//Wait for all tasks to finish
|
|
||||||
do
|
|
||||||
{
|
|
||||||
Thread.Sleep(50);
|
|
||||||
}while(tasks.Any(t => t.Status < TaskStatus.RanToCompletion));
|
|
||||||
|
|
||||||
//Concatenate all results into one
|
|
||||||
(Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)[] ret =
|
|
||||||
tasks.Select(t => t.IsCompletedSuccessfully ? t.Result : []).ToArray().SelectMany(i => i).ToArray();
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)? GetMangaFromUrl(string url)
|
|
||||||
{
|
|
||||||
MangaConnector? mc = context.MangaConnectors.ToArray().FirstOrDefault(c => c.ValidateUrl(url));
|
|
||||||
return mc?.GetMangaFromUrl(url) ?? null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)? GetMangaFromId(string publicationId)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override Chapter[] GetChapters(Manga manga, string language = "en")
|
|
||||||
{
|
|
||||||
return manga.MangaConnector?.GetChapters(manga) ?? [];
|
|
||||||
}
|
|
||||||
|
|
||||||
internal override string[] GetChapterImageUrls(Chapter chapter)
|
|
||||||
{
|
|
||||||
return chapter.ParentManga?.MangaConnector?.GetChapterImageUrls(chapter) ?? [];
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,57 +0,0 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
|
||||||
using System.ComponentModel.DataAnnotations.Schema;
|
|
||||||
using System.Text.RegularExpressions;
|
|
||||||
using API.MangaDownloadClients;
|
|
||||||
using log4net;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Newtonsoft.Json;
|
|
||||||
|
|
||||||
namespace API.Schema.MangaConnectors;
|
|
||||||
|
|
||||||
[PrimaryKey("Name")]
|
|
||||||
public abstract class MangaConnector(string name, string[] supportedLanguages, string[] baseUris, string iconUrl)
|
|
||||||
{
|
|
||||||
[StringLength(32)]
|
|
||||||
[Required]
|
|
||||||
public string Name { get; init; } = name;
|
|
||||||
[StringLength(8)]
|
|
||||||
[Required]
|
|
||||||
public string[] SupportedLanguages { get; init; } = supportedLanguages;
|
|
||||||
[StringLength(2048)]
|
|
||||||
[Required]
|
|
||||||
public string IconUrl { get; init; } = iconUrl;
|
|
||||||
[StringLength(256)]
|
|
||||||
[Required]
|
|
||||||
public string[] BaseUris { get; init; } = baseUris;
|
|
||||||
[Required]
|
|
||||||
public bool Enabled { get; internal set; } = true;
|
|
||||||
|
|
||||||
public abstract (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)[] GetManga(string publicationTitle = "");
|
|
||||||
|
|
||||||
public abstract (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)? GetMangaFromUrl(string url);
|
|
||||||
|
|
||||||
public abstract (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)? GetMangaFromId(string publicationId);
|
|
||||||
|
|
||||||
public abstract Chapter[] GetChapters(Manga manga, string language="en");
|
|
||||||
|
|
||||||
[JsonIgnore]
|
|
||||||
[NotMapped]
|
|
||||||
internal DownloadClient downloadClient { get; init; } = null!;
|
|
||||||
|
|
||||||
[JsonIgnore]
|
|
||||||
[NotMapped]
|
|
||||||
protected ILog Log { get; init; } = LogManager.GetLogger(name);
|
|
||||||
|
|
||||||
public Chapter[] GetNewChapters(Manga manga)
|
|
||||||
{
|
|
||||||
Chapter[] allChapters = GetChapters(manga);
|
|
||||||
if (allChapters.Length < 1)
|
|
||||||
return [];
|
|
||||||
|
|
||||||
return allChapters.Where(chapter => !chapter.IsDownloaded()).ToArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
internal abstract string[] GetChapterImageUrls(Chapter chapter);
|
|
||||||
|
|
||||||
public bool ValidateUrl(string url) => BaseUris.Any(baseUri => Regex.IsMatch(url, "https?://" + baseUri + "/.*"));
|
|
||||||
}
|
|
@ -1,175 +0,0 @@
|
|||||||
using System.Net;
|
|
||||||
using System.Text.RegularExpressions;
|
|
||||||
using API.MangaDownloadClients;
|
|
||||||
using HtmlAgilityPack;
|
|
||||||
|
|
||||||
namespace API.Schema.MangaConnectors;
|
|
||||||
|
|
||||||
public class Weebcentral : MangaConnector
|
|
||||||
{
|
|
||||||
private readonly string[] _filterWords =
|
|
||||||
{ "a", "the", "of", "as", "to", "no", "for", "on", "with", "be", "and", "in", "wa", "at", "be", "ni" };
|
|
||||||
|
|
||||||
public Weebcentral() : base("Weebcentral", ["en"], ["weebcentral.com"], "https://weebcentral.com/favicon.ico")
|
|
||||||
{
|
|
||||||
downloadClient = new ChromiumDownloadClient();
|
|
||||||
}
|
|
||||||
|
|
||||||
public override (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)[] GetManga(string publicationTitle = "")
|
|
||||||
{
|
|
||||||
const int limit = 32; //How many values we want returned at once
|
|
||||||
var offset = 0; //"Page"
|
|
||||||
var requestUrl =
|
|
||||||
$"https://{BaseUris[0]}/search/data?limit={limit}&offset={offset}&text={publicationTitle}&sort=Best+Match&order=Ascending&official=Any&display_mode=Minimal%20Display";
|
|
||||||
var requestResult =
|
|
||||||
downloadClient.MakeRequest(requestUrl, RequestType.Default);
|
|
||||||
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300 ||
|
|
||||||
requestResult.htmlDocument == null)
|
|
||||||
{
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
var publications = ParsePublicationsFromHtml(requestResult.htmlDocument);
|
|
||||||
|
|
||||||
return publications;
|
|
||||||
}
|
|
||||||
|
|
||||||
private (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)[] ParsePublicationsFromHtml(HtmlDocument document)
|
|
||||||
{
|
|
||||||
if (document.DocumentNode.SelectNodes("//article").Count < 1)
|
|
||||||
return [];
|
|
||||||
|
|
||||||
var urls = document.DocumentNode.SelectNodes("/html/body/article/a[contains(concat(' ',normalize-space(@class),' '),' link ')]")
|
|
||||||
.Select(elem => elem.GetAttributeValue("href", "")).ToList();
|
|
||||||
|
|
||||||
List<(Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)> ret = new();
|
|
||||||
foreach (var url in urls)
|
|
||||||
{
|
|
||||||
(Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)? manga = GetMangaFromUrl(url);
|
|
||||||
if (manga is { })
|
|
||||||
ret.Add(((Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?))manga);
|
|
||||||
}
|
|
||||||
|
|
||||||
return ret.ToArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
public override (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)? GetMangaFromUrl(string url)
|
|
||||||
{
|
|
||||||
Regex publicationIdRex = new(@"https:\/\/weebcentral\.com\/series\/(\w*)\/(.*)");
|
|
||||||
var publicationId = publicationIdRex.Match(url).Groups[1].Value;
|
|
||||||
|
|
||||||
var requestResult = downloadClient.MakeRequest(url, RequestType.MangaInfo);
|
|
||||||
if ((int)requestResult.statusCode < 300 && (int)requestResult.statusCode >= 200 &&
|
|
||||||
requestResult.htmlDocument is not null)
|
|
||||||
return ParseSinglePublicationFromHtml(requestResult.htmlDocument, publicationId, url);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?) ParseSinglePublicationFromHtml(HtmlDocument document, string publicationId, string websiteUrl)
|
|
||||||
{
|
|
||||||
HtmlNode posterNode =
|
|
||||||
document.DocumentNode.SelectSingleNode("//section[@class='flex items-center justify-center']/picture/img");
|
|
||||||
string posterUrl = posterNode?.GetAttributeValue("src", "") ?? "";
|
|
||||||
|
|
||||||
HtmlNode titleNode = document.DocumentNode.SelectSingleNode("//section/h1");
|
|
||||||
string sortName = titleNode?.InnerText ?? "Undefined";
|
|
||||||
|
|
||||||
HtmlNode[] authorsNodes =
|
|
||||||
document.DocumentNode.SelectNodes("//ul/li[strong/text() = 'Author(s): ']/span").ToArray();
|
|
||||||
List<Author> authors = authorsNodes.Select(n => new Author(n.InnerText)).ToList();
|
|
||||||
|
|
||||||
HtmlNode[] genreNodes =
|
|
||||||
document.DocumentNode.SelectNodes("//ul/li[strong/text() = 'Tags(s): ']/span").ToArray();
|
|
||||||
List<MangaTag> tags = genreNodes.Select(n => new MangaTag(n.InnerText.EndsWith(',') ? n.InnerText.Substring(0,n.InnerText.Length-1) : n.InnerText)).ToList();
|
|
||||||
|
|
||||||
HtmlNode statusNode = document.DocumentNode.SelectSingleNode("//ul/li[strong/text() = 'Status: ']/a");
|
|
||||||
string statusText = statusNode?.InnerText ?? "";
|
|
||||||
MangaReleaseStatus releaseStatus = statusText.ToLower() switch
|
|
||||||
{
|
|
||||||
"cancelled" => MangaReleaseStatus.Cancelled,
|
|
||||||
"hiatus" => MangaReleaseStatus.OnHiatus,
|
|
||||||
"complete" => MangaReleaseStatus.Completed,
|
|
||||||
"ongoing" => MangaReleaseStatus.Continuing,
|
|
||||||
_ => MangaReleaseStatus.Unreleased
|
|
||||||
};
|
|
||||||
|
|
||||||
HtmlNode yearNode = document.DocumentNode.SelectSingleNode("//ul/li[strong/text() = 'Released: ']/span");
|
|
||||||
uint year = Convert.ToUInt32(yearNode?.InnerText ?? "0");
|
|
||||||
|
|
||||||
HtmlNode descriptionNode = document.DocumentNode.SelectSingleNode("//ul/li[strong/text() = 'Description']/p");
|
|
||||||
string description = descriptionNode?.InnerText ?? "Undefined";
|
|
||||||
|
|
||||||
HtmlNode[] altTitleNodes = document.DocumentNode
|
|
||||||
.SelectNodes("//ul/li[strong/text() = 'Associated Name(s)']/ul/li")?.ToArray() ?? [];
|
|
||||||
List<MangaAltTitle> altTitles = altTitleNodes.Select(n => new MangaAltTitle("", n.InnerText)).ToList();
|
|
||||||
|
|
||||||
Manga m = new(publicationId, sortName, description, websiteUrl, posterUrl, null, year, null, releaseStatus, -1,
|
|
||||||
this, authors, tags, [], altTitles);
|
|
||||||
return (m, authors, tags, [], altTitles);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)? GetMangaFromId(string publicationId)
|
|
||||||
{
|
|
||||||
return GetMangaFromUrl($"https://{BaseUris[0]}/series/{publicationId}");
|
|
||||||
}
|
|
||||||
|
|
||||||
public override Chapter[] GetChapters(Manga manga, string language = "en")
|
|
||||||
{
|
|
||||||
var requestUrl = $"https://{BaseUris[0]}/series/{manga.MangaConnectorId}/full-chapter-list";
|
|
||||||
var requestResult =
|
|
||||||
downloadClient.MakeRequest(requestUrl, RequestType.Default);
|
|
||||||
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
|
|
||||||
return [];
|
|
||||||
|
|
||||||
//Return Chapters ordered by Chapter-Number
|
|
||||||
if (requestResult.htmlDocument is null)
|
|
||||||
return [];
|
|
||||||
var chapters = ParseChaptersFromHtml(manga, requestResult.htmlDocument);
|
|
||||||
return chapters.Order().ToArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
internal override string[] GetChapterImageUrls(Chapter chapter)
|
|
||||||
{
|
|
||||||
var requestResult = downloadClient.MakeRequest(chapter.Url, RequestType.Default);
|
|
||||||
if (requestResult.htmlDocument is null)
|
|
||||||
return [];
|
|
||||||
|
|
||||||
var document = requestResult.htmlDocument;
|
|
||||||
|
|
||||||
var imageNodes =
|
|
||||||
document.DocumentNode.SelectNodes($"//section[@hx-get='{chapter.Url}/images']/img")?.ToArray() ?? [];
|
|
||||||
var urls = imageNodes.Select(imgNode => imgNode.GetAttributeValue("src", "")).ToArray();
|
|
||||||
|
|
||||||
return urls;
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<Chapter> ParseChaptersFromHtml(Manga manga, HtmlDocument document)
|
|
||||||
{
|
|
||||||
var chaptersWrapper = document.DocumentNode.SelectSingleNode("/html/body");
|
|
||||||
|
|
||||||
Regex chapterRex = new(@"(\d+(?:\.\d+)*)");
|
|
||||||
Regex idRex = new(@"https:\/\/weebcentral\.com\/chapters\/(\w*)");
|
|
||||||
|
|
||||||
var ret = chaptersWrapper.Descendants("a").Select(elem =>
|
|
||||||
{
|
|
||||||
var url = elem.GetAttributeValue("href", "") ?? "Undefined";
|
|
||||||
|
|
||||||
if (!url.StartsWith("https://") && !url.StartsWith("http://"))
|
|
||||||
return new Chapter(manga, "", "");
|
|
||||||
|
|
||||||
var idMatch = idRex.Match(url);
|
|
||||||
var id = idMatch.Success ? idMatch.Groups[1].Value : null;
|
|
||||||
|
|
||||||
var chapterNode = elem.SelectSingleNode("span[@class='grow flex items-center gap-2']/span")?.InnerText ??
|
|
||||||
"Undefined";
|
|
||||||
|
|
||||||
var chapterNumberMatch = chapterRex.Match(chapterNode);
|
|
||||||
var chapterNumber = chapterNumberMatch.Success ? chapterNumberMatch.Groups[1].Value : "-1";
|
|
||||||
|
|
||||||
return new Chapter(manga, url, chapterNumber);
|
|
||||||
}).Where(elem => elem.ChapterNumber != String.Empty && elem.Url != string.Empty).ToList();
|
|
||||||
|
|
||||||
ret.Reverse();
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,10 +0,0 @@
|
|||||||
namespace API.Schema;
|
|
||||||
|
|
||||||
public enum MangaReleaseStatus : byte
|
|
||||||
{
|
|
||||||
Continuing = 0,
|
|
||||||
Completed = 1,
|
|
||||||
OnHiatus = 2,
|
|
||||||
Cancelled = 3,
|
|
||||||
Unreleased = 4
|
|
||||||
}
|
|
@ -1,12 +0,0 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
|
|
||||||
namespace API.Schema;
|
|
||||||
|
|
||||||
[PrimaryKey("Tag")]
|
|
||||||
public class MangaTag(string tag)
|
|
||||||
{
|
|
||||||
[StringLength(64)]
|
|
||||||
[Required]
|
|
||||||
public string Tag { get; init; } = tag;
|
|
||||||
}
|
|
@ -1,28 +0,0 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
|
|
||||||
namespace API.Schema;
|
|
||||||
|
|
||||||
[PrimaryKey("NotificationId")]
|
|
||||||
public class Notification(string title, string message = "", NotificationUrgency urgency = NotificationUrgency.Normal, DateTime? date = null)
|
|
||||||
{
|
|
||||||
[StringLength(64)]
|
|
||||||
[Required]
|
|
||||||
public string NotificationId { get; init; } = TokenGen.CreateToken("Notification");
|
|
||||||
|
|
||||||
[Required]
|
|
||||||
public NotificationUrgency Urgency { get; init; } = urgency;
|
|
||||||
|
|
||||||
[StringLength(128)]
|
|
||||||
[Required]
|
|
||||||
public string Title { get; init; } = title;
|
|
||||||
|
|
||||||
[StringLength(512)]
|
|
||||||
[Required]
|
|
||||||
public string Message { get; init; } = message;
|
|
||||||
|
|
||||||
[Required]
|
|
||||||
public DateTime Date { get; init; } = date ?? DateTime.UtcNow;
|
|
||||||
|
|
||||||
public Notification() : this("") { }
|
|
||||||
}
|
|
@ -1,82 +0,0 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
|
||||||
using System.ComponentModel.DataAnnotations.Schema;
|
|
||||||
using System.Text;
|
|
||||||
using log4net;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Newtonsoft.Json;
|
|
||||||
|
|
||||||
namespace API.Schema.NotificationConnectors;
|
|
||||||
|
|
||||||
[PrimaryKey("Name")]
|
|
||||||
public class NotificationConnector(string name, string url, Dictionary<string, string> headers, string httpMethod, string body)
|
|
||||||
{
|
|
||||||
[StringLength(64)]
|
|
||||||
[Required]
|
|
||||||
public string Name { get; init; } = name;
|
|
||||||
|
|
||||||
[StringLength(2048)]
|
|
||||||
[Required]
|
|
||||||
[Url]
|
|
||||||
public string Url { get; internal set; } = url;
|
|
||||||
|
|
||||||
[Required]
|
|
||||||
public Dictionary<string, string> Headers { get; internal set; } = headers;
|
|
||||||
|
|
||||||
[StringLength(8)]
|
|
||||||
[Required]
|
|
||||||
public string HttpMethod { get; internal set; } = httpMethod;
|
|
||||||
|
|
||||||
[StringLength(4096)]
|
|
||||||
[Required]
|
|
||||||
public string Body { get; internal set; } = body;
|
|
||||||
|
|
||||||
[JsonIgnore]
|
|
||||||
[NotMapped]
|
|
||||||
private readonly HttpClient Client = new()
|
|
||||||
{
|
|
||||||
DefaultRequestHeaders = { { "User-Agent", TrangaSettings.userAgent } }
|
|
||||||
};
|
|
||||||
|
|
||||||
[JsonIgnore]
|
|
||||||
[NotMapped]
|
|
||||||
protected ILog Log = LogManager.GetLogger(name);
|
|
||||||
|
|
||||||
public void SendNotification(string title, string notificationText)
|
|
||||||
{
|
|
||||||
Log.Info($"Sending notification: {title} - {notificationText}");
|
|
||||||
CustomWebhookFormatProvider formatProvider = new (title, notificationText);
|
|
||||||
string formattedUrl = string.Format(formatProvider, Url);
|
|
||||||
string formattedBody = string.Format(formatProvider, Body, title, notificationText);
|
|
||||||
Dictionary<string, string> formattedHeaders = Headers.ToDictionary(h => h.Key,
|
|
||||||
h => string.Format(formatProvider, h.Value, title, notificationText));
|
|
||||||
|
|
||||||
HttpRequestMessage request = new(System.Net.Http.HttpMethod.Parse(HttpMethod), formattedUrl);
|
|
||||||
foreach (var (key, value) in formattedHeaders)
|
|
||||||
request.Headers.Add(key, value);
|
|
||||||
request.Content = new StringContent(formattedBody);
|
|
||||||
Log.Debug($"Request: {request}");
|
|
||||||
|
|
||||||
HttpResponseMessage response = Client.Send(request);
|
|
||||||
Log.Debug($"Response status code: {response.StatusCode}");
|
|
||||||
}
|
|
||||||
|
|
||||||
private class CustomWebhookFormatProvider(string title, string text) : IFormatProvider
|
|
||||||
{
|
|
||||||
public object? GetFormat(Type? formatType)
|
|
||||||
{
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public string Format(string fmt, object arg, IFormatProvider provider)
|
|
||||||
{
|
|
||||||
if(arg.GetType() != typeof(string))
|
|
||||||
return arg.ToString() ?? string.Empty;
|
|
||||||
|
|
||||||
StringBuilder sb = new StringBuilder(fmt);
|
|
||||||
sb.Replace("%title", title);
|
|
||||||
sb.Replace("%text", text);
|
|
||||||
|
|
||||||
return sb.ToString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,8 +0,0 @@
|
|||||||
namespace API.Schema;
|
|
||||||
|
|
||||||
public enum NotificationUrgency : byte
|
|
||||||
{
|
|
||||||
Low = 1,
|
|
||||||
Normal = 3,
|
|
||||||
High = 5
|
|
||||||
}
|
|
@ -1,120 +0,0 @@
|
|||||||
using API.Schema.Jobs;
|
|
||||||
using API.Schema.LibraryConnectors;
|
|
||||||
using API.Schema.MangaConnectors;
|
|
||||||
using API.Schema.NotificationConnectors;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
|
|
||||||
namespace API.Schema;
|
|
||||||
|
|
||||||
public class PgsqlContext(DbContextOptions<PgsqlContext> options) : DbContext(options)
|
|
||||||
{
|
|
||||||
public DbSet<Job> Jobs { get; set; }
|
|
||||||
public DbSet<MangaConnector> MangaConnectors { get; set; }
|
|
||||||
public DbSet<Manga> Mangas { get; set; }
|
|
||||||
public DbSet<LocalLibrary> LocalLibraries { get; set; }
|
|
||||||
public DbSet<Chapter> Chapters { get; set; }
|
|
||||||
public DbSet<Author> Authors { get; set; }
|
|
||||||
public DbSet<Link> Links { get; set; }
|
|
||||||
public DbSet<MangaTag> Tags { get; set; }
|
|
||||||
public DbSet<MangaAltTitle> AltTitles { get; set; }
|
|
||||||
public DbSet<LibraryConnector> LibraryConnectors { get; set; }
|
|
||||||
public DbSet<NotificationConnector> NotificationConnectors { get; set; }
|
|
||||||
public DbSet<Notification> Notifications { get; set; }
|
|
||||||
|
|
||||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
|
||||||
{
|
|
||||||
modelBuilder.Entity<MangaConnector>()
|
|
||||||
.HasDiscriminator(c => c.Name)
|
|
||||||
.HasValue<Global>("Global")
|
|
||||||
.HasValue<AsuraToon>("AsuraToon")
|
|
||||||
.HasValue<Bato>("Bato")
|
|
||||||
.HasValue<MangaHere>("MangaHere")
|
|
||||||
.HasValue<MangaKatana>("MangaKatana")
|
|
||||||
.HasValue<Mangaworld>("Mangaworld")
|
|
||||||
.HasValue<ManhuaPlus>("ManhuaPlus")
|
|
||||||
.HasValue<Weebcentral>("Weebcentral")
|
|
||||||
.HasValue<Manganato>("Manganato")
|
|
||||||
.HasValue<MangaDex>("MangaDex");
|
|
||||||
modelBuilder.Entity<LibraryConnector>()
|
|
||||||
.HasDiscriminator<LibraryType>(l => l.LibraryType)
|
|
||||||
.HasValue<Komga>(LibraryType.Komga)
|
|
||||||
.HasValue<Kavita>(LibraryType.Kavita);
|
|
||||||
|
|
||||||
modelBuilder.Entity<Job>()
|
|
||||||
.HasDiscriminator<JobType>(j => j.JobType)
|
|
||||||
.HasValue<MoveFileOrFolderJob>(JobType.MoveFileOrFolderJob)
|
|
||||||
.HasValue<DownloadAvailableChaptersJob>(JobType.DownloadAvailableChaptersJob)
|
|
||||||
.HasValue<DownloadSingleChapterJob>(JobType.DownloadSingleChapterJob)
|
|
||||||
.HasValue<DownloadMangaCoverJob>(JobType.DownloadMangaCoverJob)
|
|
||||||
.HasValue<UpdateMetadataJob>(JobType.UpdateMetaDataJob)
|
|
||||||
.HasValue<RetrieveChaptersJob>(JobType.RetrieveChaptersJob)
|
|
||||||
.HasValue<UpdateFilesDownloadedJob>(JobType.UpdateFilesDownloadedJob);
|
|
||||||
modelBuilder.Entity<Job>()
|
|
||||||
.HasOne<Job>(j => j.ParentJob)
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey(j => j.ParentJobId)
|
|
||||||
.OnDelete(DeleteBehavior.Cascade);
|
|
||||||
modelBuilder.Entity<Job>()
|
|
||||||
.HasMany<Job>(j => j.DependsOnJobs)
|
|
||||||
.WithMany();
|
|
||||||
modelBuilder.Entity<DownloadAvailableChaptersJob>()
|
|
||||||
.Navigation(dncj => dncj.Manga)
|
|
||||||
.AutoInclude();
|
|
||||||
modelBuilder.Entity<DownloadSingleChapterJob>()
|
|
||||||
.Navigation(dscj => dscj.Chapter)
|
|
||||||
.AutoInclude();
|
|
||||||
modelBuilder.Entity<UpdateMetadataJob>()
|
|
||||||
.Navigation(umj => umj.Manga)
|
|
||||||
.AutoInclude();
|
|
||||||
|
|
||||||
modelBuilder.Entity<Manga>()
|
|
||||||
.HasOne<MangaConnector>(m => m.MangaConnector)
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey(m => m.MangaConnectorId)
|
|
||||||
.OnDelete(DeleteBehavior.Cascade);
|
|
||||||
modelBuilder.Entity<Manga>()
|
|
||||||
.Navigation(m => m.MangaConnector)
|
|
||||||
.AutoInclude();
|
|
||||||
modelBuilder.Entity<Manga>()
|
|
||||||
.HasOne<LocalLibrary>(m => m.Library)
|
|
||||||
.WithMany()
|
|
||||||
.OnDelete(DeleteBehavior.Restrict);
|
|
||||||
modelBuilder.Entity<Manga>()
|
|
||||||
.Navigation(m => m.Library)
|
|
||||||
.AutoInclude();
|
|
||||||
modelBuilder.Entity<Manga>()
|
|
||||||
.HasMany<Author>(m => m.Authors)
|
|
||||||
.WithMany();
|
|
||||||
modelBuilder.Entity<Manga>()
|
|
||||||
.Navigation(m => m.Authors)
|
|
||||||
.AutoInclude();
|
|
||||||
modelBuilder.Entity<Manga>()
|
|
||||||
.HasMany<MangaTag>(m => m.MangaTags)
|
|
||||||
.WithMany();
|
|
||||||
modelBuilder.Entity<Manga>()
|
|
||||||
.Navigation(m => m.MangaTags)
|
|
||||||
.AutoInclude();
|
|
||||||
modelBuilder.Entity<Manga>()
|
|
||||||
.HasMany<Link>(m => m.Links)
|
|
||||||
.WithOne()
|
|
||||||
.OnDelete(DeleteBehavior.Cascade);
|
|
||||||
modelBuilder.Entity<Manga>()
|
|
||||||
.Navigation(m => m.Links)
|
|
||||||
.AutoInclude();
|
|
||||||
modelBuilder.Entity<Manga>()
|
|
||||||
.HasMany<MangaAltTitle>(m => m.AltTitles)
|
|
||||||
.WithOne()
|
|
||||||
.OnDelete(DeleteBehavior.Cascade);
|
|
||||||
modelBuilder.Entity<Manga>()
|
|
||||||
.Navigation(m => m.AltTitles)
|
|
||||||
.AutoInclude();
|
|
||||||
modelBuilder.Entity<Chapter>()
|
|
||||||
.HasOne<Manga>(c => c.ParentManga)
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey(c => c.ParentMangaId)
|
|
||||||
.OnDelete(DeleteBehavior.Cascade);
|
|
||||||
modelBuilder.Entity<Chapter>()
|
|
||||||
.Navigation(c => c.ParentManga)
|
|
||||||
.AutoInclude();
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,37 +0,0 @@
|
|||||||
using System.Security.Cryptography;
|
|
||||||
using System.Text;
|
|
||||||
|
|
||||||
namespace API;
|
|
||||||
|
|
||||||
public static class TokenGen
|
|
||||||
{
|
|
||||||
private const int MinimumLength = 32;
|
|
||||||
private const int MaximumLength = 64;
|
|
||||||
private const string Chars = "abcdefghijklmnopqrstuvwxyz0123456789";
|
|
||||||
|
|
||||||
public static string CreateToken(Type t, params string[] identifiers) => CreateToken(t.Name, identifiers);
|
|
||||||
|
|
||||||
public static string CreateToken(string prefix, params string[] identifiers)
|
|
||||||
{
|
|
||||||
if (prefix.Length + 1 >= MaximumLength - MinimumLength)
|
|
||||||
throw new ArgumentException("Prefix to long to create Token of meaningful length.");
|
|
||||||
|
|
||||||
int tokenLength = MaximumLength - prefix.Length - 1;
|
|
||||||
|
|
||||||
if (identifiers.Length == 0)
|
|
||||||
{
|
|
||||||
// No identifier, just create a random token
|
|
||||||
byte[] rng = new byte[tokenLength];
|
|
||||||
RandomNumberGenerator.Create().GetBytes(rng);
|
|
||||||
string key = new(rng.Select(b => Chars[b % Chars.Length]).ToArray());
|
|
||||||
key = string.Join('-', prefix, key);
|
|
||||||
return key;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Identifier provided, create a token based on the identifier hashed
|
|
||||||
byte[] hash = MD5.HashData(Encoding.UTF8.GetBytes(string.Join("", identifiers)));
|
|
||||||
string token = Convert.ToHexStringLower(hash);
|
|
||||||
|
|
||||||
return string.Join('-', prefix, token);
|
|
||||||
}
|
|
||||||
}
|
|
271
API/Tranga.cs
271
API/Tranga.cs
@ -1,271 +0,0 @@
|
|||||||
using API.Schema;
|
|
||||||
using API.Schema.Jobs;
|
|
||||||
using API.Schema.MangaConnectors;
|
|
||||||
using API.Schema.NotificationConnectors;
|
|
||||||
using log4net;
|
|
||||||
using log4net.Config;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
|
|
||||||
namespace API;
|
|
||||||
|
|
||||||
public static class Tranga
|
|
||||||
{
|
|
||||||
public static Thread NotificationSenderThread { get; } = new (NotificationSender);
|
|
||||||
public static Thread JobStarterThread { get; } = new (JobStarter);
|
|
||||||
private static readonly ILog Log = LogManager.GetLogger(typeof(Tranga));
|
|
||||||
|
|
||||||
internal static void StartLogger()
|
|
||||||
{
|
|
||||||
BasicConfigurator.Configure();
|
|
||||||
Log.Info("Logger Configured.");
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void NotificationSender(object? serviceProviderObj)
|
|
||||||
{
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void SendNotifications(IServiceProvider serviceProvider, NotificationUrgency urgency)
|
|
||||||
{
|
|
||||||
Log.Info($"Sending notifications for {urgency}");
|
|
||||||
using IServiceScope scope = serviceProvider.CreateScope();
|
|
||||||
PgsqlContext? context = scope.ServiceProvider.GetService<PgsqlContext>();
|
|
||||||
if (context is null)
|
|
||||||
{
|
|
||||||
Log.Error("PgsqlContext is null");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
List<Notification> notifications = context.Notifications.Where(n => n.Urgency == urgency).ToList();
|
|
||||||
if (!notifications.Any())
|
|
||||||
return;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
{
|
|
||||||
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("JobStarter Thread running.");
|
|
||||||
while (true)
|
|
||||||
{
|
|
||||||
List<Job> completedJobs = context.Jobs.Where(j => j.state >= JobState.Completed).ToList();
|
|
||||||
Log.Debug($"Completed jobs: {completedJobs.Count}");
|
|
||||||
foreach (Job job in completedJobs)
|
|
||||||
if (job.RecurrenceMs <= 0)
|
|
||||||
context.Jobs.Remove(job);
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (job.state >= JobState.Failed)
|
|
||||||
job.Enabled = false;
|
|
||||||
else
|
|
||||||
job.state = JobState.Waiting;
|
|
||||||
job.LastExecution = DateTime.UtcNow;
|
|
||||||
}
|
|
||||||
|
|
||||||
List<Job> runJobs = context.Jobs.Where(j => j.state <= JobState.Running && j.Enabled == true).ToList()
|
|
||||||
.Where(j => j.NextExecution < DateTime.UtcNow).ToList();
|
|
||||||
Log.Debug($"Due jobs: {runJobs.Count}");
|
|
||||||
Log.Debug($"Running jobs: {RunningJobs.Count}");
|
|
||||||
IEnumerable<Job> orderedJobs = OrderJobs(runJobs, context).ToList();
|
|
||||||
Log.Debug($"Ordered jobs: {orderedJobs.Count()}");
|
|
||||||
foreach (Job job in orderedJobs)
|
|
||||||
{
|
|
||||||
// If the job is already running, skip it
|
|
||||||
if (RunningJobs.Values.Any(j => j.JobId == job.JobId)) continue;
|
|
||||||
|
|
||||||
//If a Job for that connector is already running, skip it
|
|
||||||
if (job is DownloadAvailableChaptersJob dncj)
|
|
||||||
{
|
|
||||||
if (RunningJobs.Values.Any(j =>
|
|
||||||
j is DownloadAvailableChaptersJob rdncj &&
|
|
||||||
rdncj.Manga?.MangaConnector == dncj.Manga?.MangaConnector))
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (job is DownloadSingleChapterJob dscj)
|
|
||||||
{
|
|
||||||
if (RunningJobs.Values.Any(j =>
|
|
||||||
j is DownloadSingleChapterJob rdscj && rdscj.Chapter?.ParentManga?.MangaConnector ==
|
|
||||||
dscj.Chapter?.ParentManga?.MangaConnector))
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Thread t = new(() =>
|
|
||||||
{
|
|
||||||
job.Run(serviceProvider);
|
|
||||||
});
|
|
||||||
RunningJobs.Add(t, job);
|
|
||||||
t.Start();
|
|
||||||
}
|
|
||||||
|
|
||||||
(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);
|
|
||||||
if(context.Jobs.Find(thread.job.JobId) is not null)
|
|
||||||
context.Jobs.Update(thread.job);
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
context.SaveChanges();
|
|
||||||
}
|
|
||||||
catch (DbUpdateException e)
|
|
||||||
{
|
|
||||||
Log.Error("Failed saving Job changes.", e);
|
|
||||||
}
|
|
||||||
Thread.Sleep(TrangaSettings.startNewJobTimeoutMs);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static IEnumerable<Job> OrderJobs(List<Job> jobs, PgsqlContext context)
|
|
||||||
{
|
|
||||||
Dictionary<JobType, List<Job>> jobsByType = new();
|
|
||||||
foreach (Job job in jobs)
|
|
||||||
if(!jobsByType.TryAdd(job.JobType, [job]))
|
|
||||||
jobsByType[job.JobType].Add(job);
|
|
||||||
|
|
||||||
IEnumerable<Job> ret = new List<Job>();
|
|
||||||
if(jobsByType.ContainsKey(JobType.MoveMangaLibraryJob))
|
|
||||||
ret = ret.Concat(jobsByType[JobType.MoveMangaLibraryJob]);
|
|
||||||
if(jobsByType.ContainsKey(JobType.MoveFileOrFolderJob))
|
|
||||||
ret = ret.Concat(jobsByType[JobType.MoveFileOrFolderJob]);
|
|
||||||
if(jobsByType.ContainsKey(JobType.DownloadMangaCoverJob))
|
|
||||||
ret = ret.Concat(jobsByType[JobType.DownloadMangaCoverJob]);
|
|
||||||
if(jobsByType.ContainsKey(JobType.UpdateFilesDownloadedJob))
|
|
||||||
ret = ret.Concat(jobsByType[JobType.UpdateFilesDownloadedJob]);
|
|
||||||
|
|
||||||
Dictionary<MangaConnector, List<Job>> metadataJobsByConnector = new();
|
|
||||||
if (jobsByType.ContainsKey(JobType.DownloadAvailableChaptersJob))
|
|
||||||
{
|
|
||||||
foreach (DownloadAvailableChaptersJob job in jobsByType[JobType.DownloadAvailableChaptersJob])
|
|
||||||
{
|
|
||||||
Manga manga = job.Manga ?? context.Mangas.Find(job.MangaId)!;
|
|
||||||
MangaConnector connector = manga.MangaConnector ?? context.MangaConnectors.Find(manga.MangaConnectorId)!;
|
|
||||||
if(!metadataJobsByConnector.TryAdd(connector, [job]))
|
|
||||||
metadataJobsByConnector[connector].Add(job);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (jobsByType.ContainsKey(JobType.UpdateMetaDataJob))
|
|
||||||
{
|
|
||||||
foreach (UpdateMetadataJob job in jobsByType[JobType.UpdateMetaDataJob])
|
|
||||||
{
|
|
||||||
Manga manga = job.Manga ?? context.Mangas.Find(job.MangaId)!;
|
|
||||||
MangaConnector connector = manga.MangaConnector ?? context.MangaConnectors.Find(manga.MangaConnectorId)!;
|
|
||||||
if(!metadataJobsByConnector.TryAdd(connector, [job]))
|
|
||||||
metadataJobsByConnector[connector].Add(job);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (jobsByType.ContainsKey(JobType.RetrieveChaptersJob))
|
|
||||||
{
|
|
||||||
foreach (RetrieveChaptersJob job in jobsByType[JobType.RetrieveChaptersJob])
|
|
||||||
{
|
|
||||||
Manga manga = job.Manga ?? context.Mangas.Find(job.MangaId)!;
|
|
||||||
MangaConnector connector = manga.MangaConnector ?? context.MangaConnectors.Find(manga.MangaConnectorId)!;
|
|
||||||
if(!metadataJobsByConnector.TryAdd(connector, [job]))
|
|
||||||
metadataJobsByConnector[connector].Add(job);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
foreach (List<Job> metadataJobs in metadataJobsByConnector.Values)
|
|
||||||
ret = ret.Append(metadataJobs.MinBy(j => j.NextExecution))!;
|
|
||||||
|
|
||||||
if (jobsByType.ContainsKey(JobType.DownloadSingleChapterJob))
|
|
||||||
{
|
|
||||||
|
|
||||||
Dictionary<MangaConnector, List<DownloadSingleChapterJob>> downloadJobsByConnector = new();
|
|
||||||
foreach (DownloadSingleChapterJob job in jobsByType[JobType.DownloadSingleChapterJob])
|
|
||||||
{
|
|
||||||
Chapter chapter = job.Chapter ?? context.Chapters.Find(job.ChapterId)!;
|
|
||||||
Manga manga = chapter.ParentManga ?? context.Mangas.Find(chapter.ParentMangaId)!;
|
|
||||||
MangaConnector connector = manga.MangaConnector ?? context.MangaConnectors.Find(manga.MangaConnectorId)!;
|
|
||||||
|
|
||||||
if(!downloadJobsByConnector.TryAdd(connector, [job]))
|
|
||||||
downloadJobsByConnector[connector].Add(job);
|
|
||||||
}
|
|
||||||
//From all jobs select those that are supposed to be executed soonest, then select the minimum chapternumber
|
|
||||||
foreach (List<DownloadSingleChapterJob> downloadJobs in downloadJobsByConnector.Values)
|
|
||||||
ret = ret.Append(
|
|
||||||
downloadJobs.Where(j => j.NextExecution == downloadJobs
|
|
||||||
.MinBy(mj => mj.NextExecution)!.NextExecution)
|
|
||||||
.MinBy(j => j.Chapter ?? context.Chapters.Find(j.ChapterId)!))!;
|
|
||||||
}
|
|
||||||
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,178 +0,0 @@
|
|||||||
using System.Runtime.InteropServices;
|
|
||||||
using API.MangaDownloadClients;
|
|
||||||
using API.Schema;
|
|
||||||
using Newtonsoft.Json;
|
|
||||||
using Newtonsoft.Json.Linq;
|
|
||||||
|
|
||||||
namespace API;
|
|
||||||
|
|
||||||
public static class TrangaSettings
|
|
||||||
{
|
|
||||||
public static string downloadLocation { get; private set; } = (RuntimeInformation.IsOSPlatform(OSPlatform.Linux) ? "/Manga" : Path.Join(Directory.GetCurrentDirectory(), "Downloads"));
|
|
||||||
public static string 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;
|
|
||||||
/// <summary>
|
|
||||||
/// Placeholders:
|
|
||||||
/// %M Manga Name
|
|
||||||
/// %V Volume
|
|
||||||
/// %C Chapter
|
|
||||||
/// %T Title
|
|
||||||
/// %A Author (first in list)
|
|
||||||
/// %I Chapter Internal ID
|
|
||||||
/// %i Manga Internal ID
|
|
||||||
/// %Y Year (Manga)
|
|
||||||
///
|
|
||||||
/// ?_(...) 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;
|
|
||||||
[JsonIgnore]
|
|
||||||
internal static readonly Dictionary<RequestType, int> DefaultRequestLimits = new ()
|
|
||||||
{
|
|
||||||
{RequestType.MangaInfo, 250},
|
|
||||||
{RequestType.MangaDexFeed, 250},
|
|
||||||
{RequestType.MangaDexImage, 40},
|
|
||||||
{RequestType.MangaImage, 60},
|
|
||||||
{RequestType.MangaCover, 250},
|
|
||||||
{RequestType.Default, 60}
|
|
||||||
};
|
|
||||||
public static Dictionary<RequestType, int> requestLimits { get; private set; } = DefaultRequestLimits;
|
|
||||||
|
|
||||||
public static TimeSpan NotificationUrgencyDelay(NotificationUrgency urgency) => urgency switch
|
|
||||||
{
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void UpdateAprilFoolsMode(bool enabled)
|
|
||||||
{
|
|
||||||
aprilFoolsMode = enabled;
|
|
||||||
ExportSettings();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void UpdateCompressImages(int value)
|
|
||||||
{
|
|
||||||
compression = int.Clamp(value, 1, 100);
|
|
||||||
ExportSettings();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void UpdateBwImages(bool enabled)
|
|
||||||
{
|
|
||||||
bwImages = enabled;
|
|
||||||
ExportSettings();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void UpdateUserAgent(string? customUserAgent)
|
|
||||||
{
|
|
||||||
userAgent = customUserAgent ?? DefaultUserAgent;
|
|
||||||
ExportSettings();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void UpdateRequestLimit(RequestType requestType, int newLimit)
|
|
||||||
{
|
|
||||||
requestLimits[requestType] = newLimit;
|
|
||||||
ExportSettings();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void UpdateChapterNamingScheme(string namingScheme)
|
|
||||||
{
|
|
||||||
chapterNamingScheme = namingScheme;
|
|
||||||
ExportSettings();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void ResetRequestLimits()
|
|
||||||
{
|
|
||||||
requestLimits = DefaultRequestLimits;
|
|
||||||
ExportSettings();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void ExportSettings()
|
|
||||||
{
|
|
||||||
if (File.Exists(settingsFilePath))
|
|
||||||
{
|
|
||||||
while(IsFileInUse(settingsFilePath))
|
|
||||||
Thread.Sleep(100);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
Directory.CreateDirectory(new FileInfo(settingsFilePath).DirectoryName!);
|
|
||||||
File.WriteAllText(settingsFilePath, Serialize());
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static bool IsFileInUse(string filePath)
|
|
||||||
{
|
|
||||||
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>()!;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,8 +0,0 @@
|
|||||||
{
|
|
||||||
"Logging": {
|
|
||||||
"LogLevel": {
|
|
||||||
"Default": "Error",
|
|
||||||
"Microsoft.AspNetCore": "Warning"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,9 +0,0 @@
|
|||||||
{
|
|
||||||
"Logging": {
|
|
||||||
"LogLevel": {
|
|
||||||
"Default": "Error",
|
|
||||||
"Microsoft.AspNetCore": "Warning"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"AllowedHosts": "*"
|
|
||||||
}
|
|
19
CLI/CLI.csproj
Normal file
19
CLI/CLI.csproj
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<LangVersion>12</LangVersion>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Spectre.Console.Cli" Version="0.49.1" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\Tranga\Tranga.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
157
CLI/Program.cs
Normal file
157
CLI/Program.cs
Normal file
@ -0,0 +1,157 @@
|
|||||||
|
using System.ComponentModel;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using Logging;
|
||||||
|
using Spectre.Console;
|
||||||
|
using Spectre.Console.Cli;
|
||||||
|
using Tranga;
|
||||||
|
|
||||||
|
var app = new CommandApp<TrangaCli>();
|
||||||
|
return app.Run(args);
|
||||||
|
|
||||||
|
internal sealed class TrangaCli : Command<TrangaCli.Settings>
|
||||||
|
{
|
||||||
|
public sealed class Settings : CommandSettings
|
||||||
|
{
|
||||||
|
[Description("Directory to which downloaded Manga are saved")]
|
||||||
|
[CommandOption("-d|--downloadLocation")]
|
||||||
|
[DefaultValue(null)]
|
||||||
|
public string? downloadLocation { get; init; }
|
||||||
|
|
||||||
|
[Description("Directory in which application-data is saved")]
|
||||||
|
[CommandOption("-w|--workingDirectory")]
|
||||||
|
[DefaultValue(null)]
|
||||||
|
public string? workingDirectory { get; init; }
|
||||||
|
|
||||||
|
[Description("Enables the file-logger")]
|
||||||
|
[CommandOption("-f")]
|
||||||
|
[DefaultValue(null)]
|
||||||
|
public bool? fileLogger { get; init; }
|
||||||
|
|
||||||
|
[Description("Path to save logfile to")]
|
||||||
|
[CommandOption("-l|--fPath")]
|
||||||
|
[DefaultValue(null)]
|
||||||
|
public string? fileLoggerPath { get; init; }
|
||||||
|
|
||||||
|
[Description("Port on which to run API on")]
|
||||||
|
[CommandOption("-p|--port")]
|
||||||
|
[DefaultValue(null)]
|
||||||
|
public int? apiPort { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public override int Execute([NotNull] CommandContext context, [NotNull] Settings settings)
|
||||||
|
{
|
||||||
|
List<Logger.LoggerType> enabledLoggers = new();
|
||||||
|
if(settings.fileLogger is true)
|
||||||
|
enabledLoggers.Add(Logger.LoggerType.FileLogger);
|
||||||
|
|
||||||
|
string? logFolderPath = settings.fileLoggerPath ?? "";
|
||||||
|
Logger logger = new(enabledLoggers.ToArray(), Console.Out, Console.OutputEncoding, logFolderPath);
|
||||||
|
|
||||||
|
if(settings.workingDirectory is not null)
|
||||||
|
TrangaSettings.LoadFromWorkingDirectory(settings.workingDirectory);
|
||||||
|
else
|
||||||
|
TrangaSettings.CreateOrUpdate();
|
||||||
|
if(settings.downloadLocation is not null)
|
||||||
|
TrangaSettings.CreateOrUpdate(downloadDirectory: settings.downloadLocation);
|
||||||
|
|
||||||
|
Tranga.Tranga? api = null;
|
||||||
|
|
||||||
|
Thread trangaApi = new Thread(() =>
|
||||||
|
{
|
||||||
|
api = new(logger);
|
||||||
|
});
|
||||||
|
trangaApi.Start();
|
||||||
|
|
||||||
|
HttpClient client = new();
|
||||||
|
|
||||||
|
bool exit = false;
|
||||||
|
while (!exit)
|
||||||
|
{
|
||||||
|
string menuSelect = AnsiConsole.Prompt(
|
||||||
|
new SelectionPrompt<string>()
|
||||||
|
.Title("Menu")
|
||||||
|
.PageSize(10)
|
||||||
|
.MoreChoicesText("Up/Down")
|
||||||
|
.AddChoices(new[]
|
||||||
|
{
|
||||||
|
"CustomRequest",
|
||||||
|
"Log",
|
||||||
|
"Exit"
|
||||||
|
}));
|
||||||
|
|
||||||
|
switch (menuSelect)
|
||||||
|
{
|
||||||
|
case "CustomRequest":
|
||||||
|
HttpMethod requestMethod = AnsiConsole.Prompt(
|
||||||
|
new SelectionPrompt<HttpMethod>()
|
||||||
|
.Title("Request Type")
|
||||||
|
.AddChoices(new[]
|
||||||
|
{
|
||||||
|
HttpMethod.Get,
|
||||||
|
HttpMethod.Delete,
|
||||||
|
HttpMethod.Post
|
||||||
|
}));
|
||||||
|
string requestPath = AnsiConsole.Prompt(
|
||||||
|
new TextPrompt<string>("Request Path:"));
|
||||||
|
List<ValueTuple<string, string>> parameters = new();
|
||||||
|
while (AnsiConsole.Confirm("Add Parameter?"))
|
||||||
|
{
|
||||||
|
string name = AnsiConsole.Ask<string>("Parameter Name:");
|
||||||
|
string value = AnsiConsole.Ask<string>("Parameter Value:");
|
||||||
|
parameters.Add(new ValueTuple<string, string>(name, value));
|
||||||
|
}
|
||||||
|
|
||||||
|
string requestString = $"http://localhost:{TrangaSettings.apiPortNumber}/{requestPath}";
|
||||||
|
if (parameters.Any())
|
||||||
|
{
|
||||||
|
requestString += "?";
|
||||||
|
foreach (ValueTuple<string, string> parameter in parameters)
|
||||||
|
requestString += $"{parameter.Item1}={parameter.Item2}&";
|
||||||
|
}
|
||||||
|
|
||||||
|
HttpRequestMessage request = new (requestMethod, requestString);
|
||||||
|
AnsiConsole.WriteLine($"Request: {request.Method} {request.RequestUri}");
|
||||||
|
HttpResponseMessage response;
|
||||||
|
if (AnsiConsole.Confirm("Send Request?"))
|
||||||
|
response = client.Send(request);
|
||||||
|
else break;
|
||||||
|
AnsiConsole.WriteLine($"Response: {(int)response.StatusCode} {response.StatusCode}");
|
||||||
|
AnsiConsole.WriteLine(response.Content.ReadAsStringAsync().Result);
|
||||||
|
break;
|
||||||
|
case "Log":
|
||||||
|
List<string> lines = logger.Tail(10).ToList();
|
||||||
|
Rows rows = new Rows(lines.Select(line => new Text(line)));
|
||||||
|
|
||||||
|
AnsiConsole.Live(rows).Start(context =>
|
||||||
|
{
|
||||||
|
bool running = true;
|
||||||
|
while (running)
|
||||||
|
{
|
||||||
|
string[] newLines = logger.GetNewLines();
|
||||||
|
if (newLines.Length > 0)
|
||||||
|
{
|
||||||
|
lines.AddRange(newLines);
|
||||||
|
rows = new Rows(lines.Select(line => new Text(line)));
|
||||||
|
context.UpdateTarget(rows);
|
||||||
|
}
|
||||||
|
Thread.Sleep(100);
|
||||||
|
if (AnsiConsole.Console.Input.IsKeyAvailable())
|
||||||
|
{
|
||||||
|
AnsiConsole.Console.Input.ReadKey(true); //Do not process input
|
||||||
|
running = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case "Exit":
|
||||||
|
exit = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (api is not null)
|
||||||
|
api.keepRunning = false;
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
10
Dockerfile
10
Dockerfile
@ -1,7 +1,7 @@
|
|||||||
# syntax=docker/dockerfile:1
|
# syntax=docker/dockerfile:1
|
||||||
ARG DOTNET=9.0
|
ARG DOTNET=8.0
|
||||||
|
|
||||||
FROM --platform=$TARGETPLATFORM mcr.microsoft.com/dotnet/aspnet:$DOTNET AS base
|
FROM --platform=$TARGETPLATFORM mcr.microsoft.com/dotnet/runtime:$DOTNET AS base
|
||||||
WORKDIR /publish
|
WORKDIR /publish
|
||||||
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true
|
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true
|
||||||
ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium
|
ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium
|
||||||
@ -16,7 +16,9 @@ FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:$DOTNET AS build-env
|
|||||||
WORKDIR /src
|
WORKDIR /src
|
||||||
|
|
||||||
COPY Tranga.sln /src
|
COPY Tranga.sln /src
|
||||||
COPY API/API.csproj /src/API/API.csproj
|
COPY CLI/CLI.csproj /src/CLI/CLI.csproj
|
||||||
|
COPY Logging/Logging.csproj /src/Logging/Logging.csproj
|
||||||
|
COPY Tranga/Tranga.csproj /src/Tranga/Tranga.csproj
|
||||||
RUN dotnet restore /src/Tranga.sln
|
RUN dotnet restore /src/Tranga.sln
|
||||||
|
|
||||||
COPY . /src/
|
COPY . /src/
|
||||||
@ -38,5 +40,5 @@ USER $UNAME
|
|||||||
WORKDIR /publish
|
WORKDIR /publish
|
||||||
COPY --chown=1000:1000 --from=build-env /publish .
|
COPY --chown=1000:1000 --from=build-env /publish .
|
||||||
USER 0
|
USER 0
|
||||||
ENTRYPOINT ["dotnet", "/publish/API.dll"]
|
ENTRYPOINT ["dotnet", "/publish/Tranga.dll"]
|
||||||
CMD ["-f", "-c", "-l", "/usr/share/tranga-api/logs"]
|
CMD ["-f", "-c", "-l", "/usr/share/tranga-api/logs"]
|
32
Logging/FileLogger.cs
Normal file
32
Logging/FileLogger.cs
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace Logging;
|
||||||
|
|
||||||
|
public class FileLogger : LoggerBase
|
||||||
|
{
|
||||||
|
internal string logFilePath { get; }
|
||||||
|
private const int MaxNumberOfLogFiles = 5;
|
||||||
|
|
||||||
|
public FileLogger(string logFilePath, Encoding? encoding = null) : base (encoding)
|
||||||
|
{
|
||||||
|
this.logFilePath = logFilePath;
|
||||||
|
|
||||||
|
DirectoryInfo dir = Directory.CreateDirectory(new FileInfo(logFilePath).DirectoryName!);
|
||||||
|
|
||||||
|
//Remove oldest logfile if more than MaxNumberOfLogFiles
|
||||||
|
for (int fileCount = dir.EnumerateFiles().Count(); fileCount > MaxNumberOfLogFiles - 1; fileCount--) //-1 because we create own logfile later
|
||||||
|
File.Delete(dir.EnumerateFiles().MinBy(file => file.LastWriteTime)!.FullName);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Write(LogMessage logMessage)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
File.AppendAllText(logFilePath, logMessage.formattedMessage);
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
// ignored
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
17
Logging/FormattedConsoleLogger.cs
Normal file
17
Logging/FormattedConsoleLogger.cs
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace Logging;
|
||||||
|
|
||||||
|
public class FormattedConsoleLogger : LoggerBase
|
||||||
|
{
|
||||||
|
private readonly TextWriter _stdOut;
|
||||||
|
public FormattedConsoleLogger(TextWriter stdOut, Encoding? encoding = null) : base(encoding)
|
||||||
|
{
|
||||||
|
this._stdOut = stdOut;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Write(LogMessage message)
|
||||||
|
{
|
||||||
|
this._stdOut.Write(message.formattedMessage);
|
||||||
|
}
|
||||||
|
}
|
23
Logging/LogMessage.cs
Normal file
23
Logging/LogMessage.cs
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
namespace Logging;
|
||||||
|
|
||||||
|
public readonly struct LogMessage
|
||||||
|
{
|
||||||
|
public DateTime logTime { get; }
|
||||||
|
public string caller { get; }
|
||||||
|
public string value { get; }
|
||||||
|
public string formattedMessage => ToString();
|
||||||
|
|
||||||
|
public LogMessage(DateTime messageTime, string caller, string value)
|
||||||
|
{
|
||||||
|
this.logTime = messageTime;
|
||||||
|
this.caller = caller;
|
||||||
|
this.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
string dateTimeString = $"{logTime.ToShortDateString()} {logTime.ToLongTimeString()}.{logTime.Millisecond,-3}";
|
||||||
|
string name = caller.Split(new char[] { '.', '+' }).Last();
|
||||||
|
return $"[{dateTimeString}] {name.Substring(0, name.Length >= 13 ? 13 : name.Length),13} | {value}";
|
||||||
|
}
|
||||||
|
}
|
80
Logging/Logger.cs
Normal file
80
Logging/Logger.cs
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace Logging;
|
||||||
|
|
||||||
|
public class Logger : TextWriter
|
||||||
|
{
|
||||||
|
private static readonly string LogDirectoryPath = RuntimeInformation.IsOSPlatform(OSPlatform.Linux)
|
||||||
|
? "/var/log/tranga-api"
|
||||||
|
: Path.Join(Directory.GetCurrentDirectory(), "logs");
|
||||||
|
public string? logFilePath => _fileLogger?.logFilePath;
|
||||||
|
public override Encoding Encoding { get; }
|
||||||
|
public enum LoggerType
|
||||||
|
{
|
||||||
|
FileLogger,
|
||||||
|
ConsoleLogger
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly FileLogger? _fileLogger;
|
||||||
|
private readonly FormattedConsoleLogger? _formattedConsoleLogger;
|
||||||
|
private readonly MemoryLogger _memoryLogger;
|
||||||
|
|
||||||
|
public Logger(LoggerType[] enabledLoggers, TextWriter? stdOut, Encoding? encoding, string? logFolderPath)
|
||||||
|
{
|
||||||
|
this.Encoding = encoding ?? Encoding.UTF8;
|
||||||
|
DateTime now = DateTime.Now;
|
||||||
|
if(enabledLoggers.Contains(LoggerType.FileLogger) && (logFolderPath is null || logFolderPath == ""))
|
||||||
|
{
|
||||||
|
string filePath = Path.Join(LogDirectoryPath,
|
||||||
|
$"{now.ToShortDateString()}_{now.Hour}-{now.Minute}-{now.Second}.log");
|
||||||
|
_fileLogger = new FileLogger(filePath, encoding);
|
||||||
|
}else if (enabledLoggers.Contains(LoggerType.FileLogger) && logFolderPath is not null)
|
||||||
|
_fileLogger = new FileLogger(Path.Join(logFolderPath, $"{now.ToShortDateString()}_{now.Hour}-{now.Minute}-{now.Second}.log") , encoding);
|
||||||
|
|
||||||
|
|
||||||
|
if (enabledLoggers.Contains(LoggerType.ConsoleLogger) && stdOut is not null)
|
||||||
|
{
|
||||||
|
_formattedConsoleLogger = new FormattedConsoleLogger(stdOut, encoding);
|
||||||
|
}
|
||||||
|
else if (enabledLoggers.Contains(LoggerType.ConsoleLogger) && stdOut is null)
|
||||||
|
{
|
||||||
|
_formattedConsoleLogger = null;
|
||||||
|
throw new ArgumentException($"stdOut can not be null for LoggerType {LoggerType.ConsoleLogger}");
|
||||||
|
}
|
||||||
|
_memoryLogger = new MemoryLogger(encoding);
|
||||||
|
WriteLine(GetType().ToString(), $"Logfile: {logFilePath}");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void WriteLine(string caller, string? value)
|
||||||
|
{
|
||||||
|
value = value is null ? Environment.NewLine : string.Concat(value, Environment.NewLine);
|
||||||
|
|
||||||
|
Write(caller, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Write(string caller, string? value)
|
||||||
|
{
|
||||||
|
if (value is null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_fileLogger?.Write(caller, value);
|
||||||
|
_formattedConsoleLogger?.Write(caller, value);
|
||||||
|
_memoryLogger.Write(caller, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string[] Tail(uint? lines)
|
||||||
|
{
|
||||||
|
return _memoryLogger.Tail(lines);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string[] GetNewLines()
|
||||||
|
{
|
||||||
|
return _memoryLogger.GetNewLines();
|
||||||
|
}
|
||||||
|
|
||||||
|
public string[] GetLog()
|
||||||
|
{
|
||||||
|
return _memoryLogger.GetLogMessages();
|
||||||
|
}
|
||||||
|
}
|
25
Logging/LoggerBase.cs
Normal file
25
Logging/LoggerBase.cs
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace Logging;
|
||||||
|
|
||||||
|
public abstract class LoggerBase : TextWriter
|
||||||
|
{
|
||||||
|
public override Encoding Encoding { get; }
|
||||||
|
|
||||||
|
public LoggerBase(Encoding? encoding = null)
|
||||||
|
{
|
||||||
|
this.Encoding = encoding ?? Encoding.ASCII;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Write(string caller, string? value)
|
||||||
|
{
|
||||||
|
if (value is null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
LogMessage message = new (DateTime.Now, caller, value);
|
||||||
|
|
||||||
|
Write(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract void Write(LogMessage message);
|
||||||
|
}
|
10
Logging/Logging.csproj
Normal file
10
Logging/Logging.csproj
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<LangVersion>12</LangVersion>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
</Project>
|
74
Logging/MemoryLogger.cs
Normal file
74
Logging/MemoryLogger.cs
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace Logging;
|
||||||
|
|
||||||
|
public class MemoryLogger : LoggerBase
|
||||||
|
{
|
||||||
|
private readonly SortedList<DateTime, LogMessage> _logMessages = new();
|
||||||
|
private int _lastLogMessageIndex = 0;
|
||||||
|
|
||||||
|
public MemoryLogger(Encoding? encoding = null) : base(encoding)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Write(LogMessage value)
|
||||||
|
{
|
||||||
|
lock (_logMessages)
|
||||||
|
{
|
||||||
|
_logMessages.Add(DateTime.Now, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public string[] GetLogMessages()
|
||||||
|
{
|
||||||
|
return Tail(Convert.ToUInt32(_logMessages.Count));
|
||||||
|
}
|
||||||
|
|
||||||
|
public string[] Tail(uint? length)
|
||||||
|
{
|
||||||
|
int retLength;
|
||||||
|
if (length is null || length > _logMessages.Count)
|
||||||
|
retLength = _logMessages.Count;
|
||||||
|
else
|
||||||
|
retLength = (int)length;
|
||||||
|
|
||||||
|
string[] ret = new string[retLength];
|
||||||
|
|
||||||
|
for (int retIndex = 0; retIndex < ret.Length; retIndex++)
|
||||||
|
{
|
||||||
|
lock (_logMessages)
|
||||||
|
{
|
||||||
|
ret[retIndex] = _logMessages.GetValueAtIndex(_logMessages.Count - retLength + retIndex).ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_lastLogMessageIndex = _logMessages.Count - 1;
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string[] GetNewLines()
|
||||||
|
{
|
||||||
|
int logMessageCount = _logMessages.Count;
|
||||||
|
List<string> ret = new();
|
||||||
|
|
||||||
|
int retIndex = 0;
|
||||||
|
for (; retIndex < logMessageCount - _lastLogMessageIndex; retIndex++)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
lock(_logMessages)
|
||||||
|
{
|
||||||
|
ret.Add(_logMessages.GetValueAtIndex(_lastLogMessageIndex + retIndex).ToString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (NullReferenceException)//Called when LogMessage has not finished writing
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_lastLogMessageIndex = _lastLogMessageIndex + retIndex;
|
||||||
|
return ret.ToArray();
|
||||||
|
}
|
||||||
|
}
|
189
README.md
189
README.md
@ -1,77 +1,82 @@
|
|||||||
<span id="readme-top"></span>
|
# Testers for V2 wanted!
|
||||||
|
|
||||||
|
[Details](https://github.com/C9Glax/tranga/pull/355#issuecomment-2764217944)
|
||||||
|
|
||||||
|
<!-- PROJECT LOGO -->
|
||||||
|
<br />
|
||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
||||||
<h1 align="center">Tranga v2</h1>
|
<h3 align="center">Tranga</h3>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
Automatic Manga and Metadata downloader
|
Automatic Manga and Metadata downloader
|
||||||
</p>
|
</p>
|
||||||
|
<p align="center">
|
||||||

|
This is the API for <a href="https://github.com/C9Glax/tranga-website">Tranga-Website</a>
|
||||||
|
</p>
|
||||||
<table>
|
|
||||||
<tr>
|
|
||||||
<th><img alt="GitHub branch check runs" src="https://img.shields.io/github/check-runs/c9glax/tranga/master?label=master"></th>
|
|
||||||
<td><img alt="Last Run" src="https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fapi.github.com%2Frepos%2Fc9glax%2Ftranga%2Factions%2Fworkflows%2Fdocker-image-master.yml%2Fruns%3Fper_page%3D1&query=workflow_runs%5B0%5D.created_at&label=Last%20Run"></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th><img alt="GitHub branch check runs" src="https://img.shields.io/github/check-runs/c9glax/tranga/cuttingedge?label=cuttingedge"></th>
|
|
||||||
<td><img alt="Last Run" src="https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fapi.github.com%2Frepos%2Fc9glax%2Ftranga%2Factions%2Fworkflows%2Fdocker-image-cuttingedge.yml%2Fruns%3Fper_page%3D1&query=workflow_runs%5B0%5D.created_at&label=Last%20Run"></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th><img alt="GitHub branch check runs" src="https://img.shields.io/github/check-runs/c9glax/tranga/postgres-Server-V2?label=postgres-Server-V2"></th>
|
|
||||||
<td><img alt="Last Run" src="https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fapi.github.com%2Frepos%2Fc9glax%2Ftranga%2Factions%2Fworkflows%2Fdocker-image-serverv2.yml%2Fruns%3Fper_page%3D1&query=workflow_runs%5B0%5D.created_at&label=Last%20Run"></td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- TABLE OF CONTENTS -->
|
||||||
|
<details>
|
||||||
|
<summary>Table of Contents</summary>
|
||||||
|
<ol>
|
||||||
|
<li>
|
||||||
|
<a href="#about-the-project">About The Project</a>
|
||||||
|
<ul>
|
||||||
|
<li><a href="#built-with">Built With</a></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="#getting-started">Getting Started</a>
|
||||||
|
<ul>
|
||||||
|
<li><a href="#prerequisites">Usage</a></li>
|
||||||
|
<li><a href="#prerequisites">Prerequisites</a></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li><a href="#roadmap">Roadmap</a></li>
|
||||||
|
<li><a href="#contributing">Contributing</a></li>
|
||||||
|
<li><a href="#license">License</a></li>
|
||||||
|
<li><a href="#acknowledgments">Acknowledgments</a></li>
|
||||||
|
</ol>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<!-- ABOUT THE PROJECT -->
|
<!-- ABOUT THE PROJECT -->
|
||||||
## About The Project
|
## About The Project
|
||||||
|
|
||||||
Tranga can download Chapters and Metadata from "Scanlation" sites such as
|
Tranga can download Chapters and Metadata from "Scanlation" sites such as
|
||||||
|
|
||||||
- [MangaDex.org](https://mangadex.org/) (Multilingual)
|
- [MangaDex.org](https://mangadex.org/) (Multilingual)
|
||||||
- [Manganato.gg](https://manganato.com/) (en) (or natomanga.com, mangakakalot, nelomanga, ...)
|
- [Manganato.com](https://manganato.com/) (en)
|
||||||
- [MangaKatana.com](https://mangakatana.com) (en)
|
- [MangaKatana.com](https://mangakatana.com) (en)
|
||||||
- [Mangaworld.bz](https://www.mangaworld.bz/) (it)
|
- [Mangaworld.bz](https://www.mangaworld.bz/) (it)
|
||||||
- [Bato.to](https://bato.to/v3x) (en)
|
- [Bato.to](https://bato.to/v3x) (en)
|
||||||
- [ManhuaPlus](https://manhuaplus.org/) (en)
|
- [ManhuaPlus](https://manhuaplus.org/) (en)
|
||||||
- [MangaHere](https://www.mangahere.cc/) (en)
|
- [MangaHere](https://www.mangahere.cc/) (en) (Their covers aren't scrapeable.)
|
||||||
- [Weebcentral](https://weebcentral.com) (en)
|
- [Weebcentral](https://weebcentral.com) (en)
|
||||||
- [Webtoons](https://www.webtoons.com/en/) (en)
|
- [Webtoons](https://www.webtoons.com/en/)
|
||||||
- ❓ Open an [issue](https://github.com/C9Glax/tranga/issues/new?assignees=&labels=New+Connector&projects=&template=new_connector.yml&title=%5BNew+Connector%5D%3A+)
|
- ❓ Open an [issue](https://github.com/C9Glax/tranga/issues/new?assignees=&labels=New+Connector&projects=&template=new_connector.yml&title=%5BNew+Connector%5D%3A+)
|
||||||
|
|
||||||
and trigger a library-scan with [Komga](https://komga.org/) and [Kavita](https://www.kavitareader.com/).
|
and trigger a library-scan with [Komga](https://komga.org/) and [Kavita](https://www.kavitareader.com/).
|
||||||
Notifications can be sent to your devices using [Gotify](https://gotify.net/), [LunaSea](https://www.lunasea.app/) or [Ntfy](https://ntfy.sh/
|
Notifications can be sent to your devices using [Gotify](https://gotify.net/), [LunaSea](https://www.lunasea.app/) or [Ntfy](https://ntfy.sh/
|
||||||
), or any other service that can use REST Webhooks.
|
|
||||||
|
|
||||||
## What this program does and does *not* do
|
|
||||||
|
|
||||||
Tranga (the program in this repository) is a REST-API and worker in one. Meaning it will open a network-port
|
|
||||||
to listen for requests, and then work through these. Requests include searches for Manga, starting "Jobs" such
|
|
||||||
as downloading available chapters, creating a monitoring job (that will periodically do the aforementioned),
|
|
||||||
update metadata, and more.
|
|
||||||
|
|
||||||
This repository *does not* include a frontend. A frontend can take many forms, such as a website:
|
|
||||||
|
|
||||||
[tranga-website](https://github.com/C9Glax/tranga-website)
|
|
||||||
|
|
||||||
When downloading a chapter (meaning the images that make-up the manga) from a Scanlation-Website, Tranga will
|
|
||||||
additionally try and scrape Metadata from the same website ~~or enhance it from third-party sources~~
|
|
||||||
(tbd https://github.com/C9Glax/tranga/issues/280).
|
|
||||||
Downloaded images can be jpeg-compressed and/or made black and white to save on diskspace
|
|
||||||
(measured at least a 50% reduction in size, without a significant loss of quality).
|
|
||||||
|
|
||||||
Tranga will then package the contents of each chapter in a `.cbz`-archive and place it in a common folder per Manga.
|
|
||||||
If specified, Tranga will then notify library-Managers such as [Komga](https://komga.org/) and [Kavita](https://www.kavitareader.com/) to trigger a scan for new
|
|
||||||
chapters. Tranga can also send notifications to your devices via third-party services such as [Gotify](https://gotify.net/), [LunaSea](https://www.lunasea.app/) or [Ntfy](https://ntfy.sh/
|
|
||||||
).
|
).
|
||||||
|
|
||||||
## Screenshots
|
### What this does and doesn't do
|
||||||
|
|
||||||
This repository has no frontend, however checkout [tranga-website](https://github.com/C9Glax/tranga-website) for a default!
|
Tranga (this git-repo) will open a port (standard 6531) and listen for requests to add Jobs to Monitor and/or download specific Manga.
|
||||||
|
The configuration is all done through HTTP-Requests.
|
||||||
|
_**For a web-frontend use [tranga-website](https://github.com/C9Glax/tranga-website).**_
|
||||||
|
|
||||||
## Inspiration:
|
This project downloads the images for a Manga from the specified Scanlation-Website and packages them with some metadata - from that same website - in a .cbz-archive (per chapter).
|
||||||
|
It does this on an interval, and checks for any Chapters (.cbz-Archive) not already existing in your specified Download-Location. (If you rename or move files, it will download those again)
|
||||||
|
Tranga can (if configured) trigger a scan in Komga or Kavita, however the directory in which the Manga reside has to be available to both Tranga and Komga/Kavita.
|
||||||
|
|
||||||
|
The project doesn't manage metadata, and doesn't curate, change or enhance any information that isn't available on the selected Scanlation-Site.
|
||||||
|
It will blindly use whatever is scrapes (yes this is a glorified Web-scraper).
|
||||||
|
|
||||||
|
|
||||||
|
### Inspiration:
|
||||||
|
|
||||||
Because [Kaizoku](https://github.com/oae/kaizoku) was relying on [mangal](https://github.com/metafates/mangal) and mangal
|
Because [Kaizoku](https://github.com/oae/kaizoku) was relying on [mangal](https://github.com/metafates/mangal) and mangal
|
||||||
hasn't received bugfixes for its issues with Titles not showing up, or throwing errors because of illegal characters,
|
hasn't received bugfixes for its issues with Titles not showing up, or throwing errors because of illegal characters,
|
||||||
@ -81,23 +86,13 @@ That is why I wanted to create my own project, in a language I understand, and t
|
|||||||
|
|
||||||
<p align="right">(<a href="#readme-top">back to top</a>)</p>
|
<p align="right">(<a href="#readme-top">back to top</a>)</p>
|
||||||
|
|
||||||
## Endpoint Documentation
|
### Built With
|
||||||
|
|
||||||
Endpoints are documented in Swagger. Just spin up an instance, and go to `http://<url>/swagger`.
|
- .NET-Core
|
||||||
|
- Newtonsoft.JSON
|
||||||
## Built With
|
- [PuppeteerSharp](https://www.puppeteersharp.com/)
|
||||||
|
- [Html Agility Pack (HAP)](https://html-agility-pack.net/)
|
||||||
- .NET
|
- [Soenneker.Utils.String.NeedlemanWunsch](https://github.com/soenneker/soenneker.utils.string.needlemanwunsch)
|
||||||
- ASP.NET
|
|
||||||
- Entity Framework
|
|
||||||
- [PostgreSQL](https://www.postgresql.org/about/licence/)
|
|
||||||
- [Swagger](https://github.com/domaindrivendev/Swashbuckle.AspNetCore/blob/master/LICENSE)
|
|
||||||
- [Ngpsql](https://github.com/npgsql/npgsql/blob/main/LICENSE)
|
|
||||||
- [Newtonsoft.Json](https://github.com/JamesNK/Newtonsoft.Json/blob/master/LICENSE.md)
|
|
||||||
- [PuppeteerSharp](https://github.com/hardkoded/puppeteer-sharp/blob/master/LICENSE)
|
|
||||||
- [Html Agility Pack (HAP)](https://github.com/zzzprojects/html-agility-pack/blob/master/LICENSE)
|
|
||||||
- [Soenneker.Utils.String.NeedlemanWunsch](https://github.com/soenneker/soenneker.utils.string.needlemanwunsch/blob/main/LICENSE)
|
|
||||||
- [Sixlabors.ImageSharp](https://docs-v2.sixlabors.com/articles/imagesharp/index.html#license)
|
|
||||||
- 💙 Blåhaj 🦈
|
- 💙 Blåhaj 🦈
|
||||||
|
|
||||||
<p align="right">(<a href="#readme-top">back to top</a>)</p>
|
<p align="right">(<a href="#readme-top">back to top</a>)</p>
|
||||||
@ -117,72 +112,60 @@ Endpoints are documented in Swagger. Just spin up an instance, and go to `http:/
|
|||||||
|
|
||||||
### Docker
|
### Docker
|
||||||
|
|
||||||
An example `docker-compose.yaml` is provided. Mount `/Manga` to wherever you want your chapters (`.cbz`-Archives)
|
Download [docker-compose.yaml](https://git.bernloehr.eu/glax/Tranga/src/branch/master/docker-compose.yaml) and configure to your needs.
|
||||||
downloaded (where Komga/Kavita can access them for example).
|
Mount `/Manga` to wherever you want your chapters (`.cbz`-Archives) downloaded (where Komga/Kavita can access them).
|
||||||
The file also includes [tranga-website](https://github.com/C9Glax/tranga-website) as frontend. For its configuration refer to the
|
The `docker-compose` also includes [tranga-website](https://github.com/C9Glax/tranga-website) as frontend. For its configuration refer to the repo README.
|
||||||
[Tranga-Website Repository](https://github.com/C9Glax/tranga-website) README.
|
|
||||||
|
|
||||||
For compatibility do not execute the compose as root (which you should not do anyways...) but as user that can
|
For compatibility do not execute the compose as root (which you should not do anyways...) but as user that can
|
||||||
access the folder. Permission conflicts with Komga and Kavita should thus be limited.
|
access the folder.
|
||||||
|
|
||||||
### Bare-Metal
|
### Prerequisites
|
||||||
|
|
||||||
While not supported/currently built, Tranga will also run Bare-Metal without issue.
|
#### To Build
|
||||||
|
[.NET-Core 8.0 SDK](https://dotnet.microsoft.com/en-us/download/dotnet/8.0)
|
||||||
|
#### To Run
|
||||||
|
[.NET-Core 8.0 Runtime](https://dotnet.microsoft.com/en-us/download/dotnet/8.0) scroll down a bit, should be on the right the second item.
|
||||||
|
|
||||||
Configuration-Files will be stored per OS:
|
See the [open issues](https://github.com/C9Glax/tranga/issues) for a full list of proposed features (and known issues).
|
||||||
- Linux `/usr/share/tranga-api`
|
|
||||||
- Windows `%appdata%/tranga-api`
|
|
||||||
|
|
||||||
Downloads (default) are stored in - but this can be configured in `settings.json`:
|
<p align="right">(<a href="#readme-top">back to top</a>)</p>
|
||||||
- Linux `/Manga`
|
|
||||||
- Windows `%currentDirectory%/Downloads`
|
|
||||||
|
|
||||||
#### Prerequisits
|
|
||||||
|
|
||||||
[.NET-Core 9.0](https://dotnet.microsoft.com/en-us/download/dotnet/9.0)
|
|
||||||
|
|
||||||
<!-- CONTRIBUTING -->
|
<!-- CONTRIBUTING -->
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
If you want to contribute, please feel free to fork and create a Pull-Request!
|
The following is copy & pasted:
|
||||||
|
|
||||||
General rules:
|
Contributions are what make the open source community such an amazing place to learn, inspire, and create. Any contributions you make are **greatly appreciated**.
|
||||||
- Strongly-type your variables. This improves readability.
|
|
||||||
```csharp
|
|
||||||
var xyz = Object.GetSomething(); //Do not do this. What type is xyz?
|
|
||||||
Manga[] zyx = Object.GetAnotherThing(); //I can now easily see that zyx is an Array.
|
|
||||||
```
|
|
||||||
|
|
||||||
**A broad overview of where is what:**<br />
|
If you have a suggestion that would make this better, please fork the repo and create a pull request. You can also simply open an issue with the tag "enhancement".
|
||||||
|
Don't forget to give the project a star! Thanks again!
|
||||||
|
|
||||||
|
1. Fork the Project
|
||||||
|
2. Create your Feature Branch (`git checkout -b feature/AmazingFeature`)
|
||||||
|
3. Commit your Changes (`git commit -m 'Add some AmazingFeature'`)
|
||||||
|
4. Push to the Branch (`git push origin feature/AmazingFeature`)
|
||||||
|
5. Open a Pull Request
|
||||||
|
|
||||||
|
<p align="right">(<a href="#readme-top">back to top</a>)</p>
|
||||||
|
|
||||||
- `Program.cs` Configuration for ASP.NET, Swagger (also in `NamedSwaggerGenOptions.cs`, Npgsql
|
|
||||||
- `Tranga.cs` Job(worker)-Logic
|
|
||||||
- `Schema/` Entity-Framework
|
|
||||||
- `Schema/Jobs/` + Logic for Jobs
|
|
||||||
- `Schema/**/` + Logic for **
|
|
||||||
- `Schema/PgsqlContext.cs` EF configuration
|
|
||||||
- `MangaDownloadClients/` Networking-Clients for Scraping
|
|
||||||
- `Controllers/` ASP.NET Controllers (Endpoints)
|
|
||||||
- `APIEndpointRecords/` Records for API-Requests with specific Request-Types (Body)
|
|
||||||
|
|
||||||
If you want to add a new Scanlationsite-Connector: <br />
|
|
||||||
1. Copy one of the existing connectors, or start from scratch and inherit from `API.Schema.MangaConnectors.MangaConnector`.
|
|
||||||
2. Add the new Connector as Object-Instance in `Program.cs` to the MangaConnector-Array `connectors`.
|
|
||||||
3. In `Schema/PgsqlContext.cs` add the Discriminator for the Connector (the value is the name of the connector, as defined
|
|
||||||
in the constructor).
|
|
||||||
|
|
||||||
<!-- LICENSE -->
|
<!-- LICENSE -->
|
||||||
## License
|
## License
|
||||||
|
|
||||||
Distributed under the GNU GPLv3 License. See [LICENSE.txt](https://github.com/C9Glax/tranga/blob/master/LICENSE.txt) for more information.
|
Distributed under the GNU GPLv3 License. See `LICENSE.txt` for more information.
|
||||||
|
|
||||||
<p align="right">(<a href="#readme-top">back to top</a>)</p>
|
<p align="right">(<a href="#readme-top">back to top</a>)</p>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<!-- ACKNOWLEDGMENTS -->
|
<!-- ACKNOWLEDGMENTS -->
|
||||||
## Acknowledgments
|
## Acknowledgments
|
||||||
|
|
||||||
* [Choose an Open Source License](https://choosealicense.com)
|
* [Choose an Open Source License](https://choosealicense.com)
|
||||||
|
* [Font Awesome](https://fontawesome.com)
|
||||||
* [Best-README-Template](https://github.com/othneildrew/Best-README-Template/tree/master)
|
* [Best-README-Template](https://github.com/othneildrew/Best-README-Template/tree/master)
|
||||||
* [Shields.io](https://shields.io/)
|
|
||||||
|
|
||||||
<p align="right">(<a href="#readme-top">back to top</a>)</p>
|
<p align="right">(<a href="#readme-top">back to top</a>)</p>
|
||||||
|
22
Tranga.sln
22
Tranga.sln
@ -1,6 +1,10 @@
|
|||||||
|
|
||||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "API", "API\API.csproj", "{EDB07E7B-351F-4FCC-9AEF-777838E5551E}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tranga", ".\Tranga\Tranga.csproj", "{545E81B9-D96B-4C8F-A97F-2C02414DE566}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Logging", "Logging\Logging.csproj", "{415BE889-BB7D-426F-976F-8D977876A462}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CLI", "CLI\CLI.csproj", "{4324C816-F9D2-468F-8ED6-397FE2F0DCB3}"
|
||||||
EndProject
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
@ -8,9 +12,17 @@ Global
|
|||||||
Release|Any CPU = Release|Any CPU
|
Release|Any CPU = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||||
{EDB07E7B-351F-4FCC-9AEF-777838E5551E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{545E81B9-D96B-4C8F-A97F-2C02414DE566}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{EDB07E7B-351F-4FCC-9AEF-777838E5551E}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{545E81B9-D96B-4C8F-A97F-2C02414DE566}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{EDB07E7B-351F-4FCC-9AEF-777838E5551E}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{545E81B9-D96B-4C8F-A97F-2C02414DE566}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{EDB07E7B-351F-4FCC-9AEF-777838E5551E}.Release|Any CPU.Build.0 = Release|Any CPU
|
{545E81B9-D96B-4C8F-A97F-2C02414DE566}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{415BE889-BB7D-426F-976F-8D977876A462}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{415BE889-BB7D-426F-976F-8D977876A462}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{415BE889-BB7D-426F-976F-8D977876A462}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{415BE889-BB7D-426F-976F-8D977876A462}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{4324C816-F9D2-468F-8ED6-397FE2F0DCB3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{4324C816-F9D2-468F-8ED6-397FE2F0DCB3}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{4324C816-F9D2-468F-8ED6-397FE2F0DCB3}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{4324C816-F9D2-468F-8ED6-397FE2F0DCB3}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
EndGlobal
|
EndGlobal
|
||||||
|
159
Tranga/Chapter.cs
Normal file
159
Tranga/Chapter.cs
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using System.Xml.Linq;
|
||||||
|
using static System.IO.UnixFileMode;
|
||||||
|
|
||||||
|
namespace Tranga;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Has to be Part of a publication
|
||||||
|
/// Includes the Chapter-Name, -VolumeNumber, -ChapterNumber, the location of the chapter on the internet and the saveName of the local file.
|
||||||
|
/// </summary>
|
||||||
|
public readonly struct Chapter : IComparable
|
||||||
|
{
|
||||||
|
// ReSharper disable once MemberCanBePrivate.Global
|
||||||
|
public Manga parentManga { get; }
|
||||||
|
public string? name { get; }
|
||||||
|
public float volumeNumber { get; }
|
||||||
|
public float chapterNumber { get; }
|
||||||
|
public string url { get; }
|
||||||
|
// ReSharper disable once MemberCanBePrivate.Global
|
||||||
|
public string fileName { get; }
|
||||||
|
public string? id { get; }
|
||||||
|
|
||||||
|
private static readonly Regex LegalCharacters = new (@"([A-z]*[0-9]* *\.*-*,*\]*\[*'*\'*\)*\(*~*!*)*");
|
||||||
|
private static readonly Regex IllegalStrings = new(@"(Vol(ume)?|Ch(apter)?)\.?", RegexOptions.IgnoreCase);
|
||||||
|
|
||||||
|
public Chapter(Manga parentManga, string? name, string? volumeNumber, string chapterNumber, string url, string? id = null)
|
||||||
|
: this(parentManga, name, float.Parse(volumeNumber??"0", GlobalBase.numberFormatDecimalPoint),
|
||||||
|
float.Parse(chapterNumber, GlobalBase.numberFormatDecimalPoint), url, id)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public Chapter(Manga parentManga, string? name, float? volumeNumber, float chapterNumber, string url, string? id = null)
|
||||||
|
{
|
||||||
|
this.parentManga = parentManga;
|
||||||
|
this.name = name;
|
||||||
|
this.volumeNumber = volumeNumber??0;
|
||||||
|
this.chapterNumber = chapterNumber;
|
||||||
|
this.url = url;
|
||||||
|
this.id = id;
|
||||||
|
|
||||||
|
string chapterVolNumStr = $"Vol.{this.volumeNumber} Ch.{chapterNumber}";
|
||||||
|
|
||||||
|
if (name is not null && name.Length > 0)
|
||||||
|
{
|
||||||
|
string chapterName = IllegalStrings.Replace(string.Concat(LegalCharacters.Matches(name)), "");
|
||||||
|
this.fileName = chapterName.Length > 0 ? $"{chapterVolNumStr} - {chapterName}" : chapterVolNumStr;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
this.fileName = chapterVolNumStr;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
return $"Chapter {parentManga.sortName} {parentManga.internalId} {chapterNumber} {name}";
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool Equals(object? obj)
|
||||||
|
{
|
||||||
|
if (obj is not Chapter)
|
||||||
|
return false;
|
||||||
|
return CompareTo(obj) == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int CompareTo(object? obj)
|
||||||
|
{
|
||||||
|
if(obj is not Chapter otherChapter)
|
||||||
|
throw new ArgumentException($"{obj} can not be compared to {this}");
|
||||||
|
return volumeNumber.CompareTo(otherChapter.volumeNumber) switch
|
||||||
|
{
|
||||||
|
<0 => -1,
|
||||||
|
>0 => 1,
|
||||||
|
_ => chapterNumber.CompareTo(otherChapter.chapterNumber)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks if a chapter-archive is already present
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>true if chapter is present</returns>
|
||||||
|
internal bool CheckChapterIsDownloaded()
|
||||||
|
{
|
||||||
|
string mangaDirectory = Path.Join(TrangaSettings.downloadLocation, parentManga.folderName);
|
||||||
|
if (!Directory.Exists(mangaDirectory))
|
||||||
|
return false;
|
||||||
|
FileInfo? mangaArchive = null;
|
||||||
|
string markerPath = Path.Join(mangaDirectory, $".{id}");
|
||||||
|
if (this.id is not null && File.Exists(markerPath))
|
||||||
|
{
|
||||||
|
if(File.Exists(File.ReadAllText(markerPath)))
|
||||||
|
mangaArchive = new FileInfo(File.ReadAllText(markerPath));
|
||||||
|
else
|
||||||
|
File.Delete(markerPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(mangaArchive is null)
|
||||||
|
{
|
||||||
|
FileInfo[] archives = new DirectoryInfo(mangaDirectory).GetFiles("*.cbz");
|
||||||
|
Regex volChRex = new(@"(?:Vol(?:ume)?\.([0-9]+)\D*)?Ch(?:apter)?\.([0-9]+(?:\.[0-9]+)*)(?: - (.*))?.cbz");
|
||||||
|
|
||||||
|
Chapter t = this;
|
||||||
|
mangaArchive = archives.FirstOrDefault(archive =>
|
||||||
|
{
|
||||||
|
Match m = volChRex.Match(archive.Name);
|
||||||
|
/*
|
||||||
|
* 1. If the volumeNumber is not present in the filename, it is not checked.
|
||||||
|
* 2. Check the chapterNumber in the chapter against the one in the filename.
|
||||||
|
* 3. The chpaterName has to either be absent both in the chapter and the filename or match.
|
||||||
|
*/
|
||||||
|
return (!m.Groups[1].Success || m.Groups[1].Value == t.volumeNumber.ToString(GlobalBase.numberFormatDecimalPoint)) &&
|
||||||
|
m.Groups[2].Value == t.chapterNumber.ToString(GlobalBase.numberFormatDecimalPoint) &&
|
||||||
|
((!m.Groups[3].Success && string.IsNullOrEmpty(t.name)) || m.Groups[3].Value == t.name);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
string correctPath = GetArchiveFilePath();
|
||||||
|
if(mangaArchive is not null && mangaArchive.FullName != correctPath)
|
||||||
|
mangaArchive.MoveTo(correctPath, true);
|
||||||
|
return (mangaArchive is not null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void CreateChapterMarker()
|
||||||
|
{
|
||||||
|
if (this.id is null)
|
||||||
|
return;
|
||||||
|
string path = Path.Join(TrangaSettings.downloadLocation, parentManga.folderName, $".{id}");
|
||||||
|
File.WriteAllText(path, GetArchiveFilePath());
|
||||||
|
File.SetAttributes(path, FileAttributes.Hidden);
|
||||||
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
||||||
|
File.SetUnixFileMode(path, UserRead | UserWrite | UserExecute | GroupRead | GroupWrite | GroupExecute | OtherRead | OtherExecute);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates full file path of chapter-archive
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>Filepath</returns>
|
||||||
|
internal string GetArchiveFilePath()
|
||||||
|
{
|
||||||
|
return Path.Join(TrangaSettings.downloadLocation, parentManga.folderName, $"{parentManga.folderName} - {this.fileName}.cbz");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a string containing XML of publication and chapter.
|
||||||
|
/// See ComicInfo.xml
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>XML-string</returns>
|
||||||
|
internal string GetComicInfoXmlString()
|
||||||
|
{
|
||||||
|
XElement comicInfo = new XElement("ComicInfo",
|
||||||
|
new XElement("Tags", string.Join(',', parentManga.tags)),
|
||||||
|
new XElement("LanguageISO", parentManga.originalLanguage),
|
||||||
|
new XElement("Title", this.name),
|
||||||
|
new XElement("Writer", string.Join(',', parentManga.authors)),
|
||||||
|
new XElement("Volume", this.volumeNumber),
|
||||||
|
new XElement("Number", this.chapterNumber)
|
||||||
|
);
|
||||||
|
return comicInfo.ToString();
|
||||||
|
}
|
||||||
|
}
|
143
Tranga/GlobalBase.cs
Normal file
143
Tranga/GlobalBase.cs
Normal file
@ -0,0 +1,143 @@
|
|||||||
|
using System.Globalization;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using Logging;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using Tranga.LibraryConnectors;
|
||||||
|
using Tranga.NotificationConnectors;
|
||||||
|
|
||||||
|
namespace Tranga;
|
||||||
|
|
||||||
|
public abstract class GlobalBase
|
||||||
|
{
|
||||||
|
[JsonIgnore]
|
||||||
|
public Logger? logger { get; init; }
|
||||||
|
protected HashSet<NotificationConnector> notificationConnectors { get; init; }
|
||||||
|
protected HashSet<LibraryConnector> libraryConnectors { get; init; }
|
||||||
|
private Dictionary<string, Manga> cachedPublications { get; init; }
|
||||||
|
public static readonly NumberFormatInfo numberFormatDecimalPoint = new (){ NumberDecimalSeparator = "." };
|
||||||
|
protected static readonly Regex baseUrlRex = new(@"https?:\/\/[0-9A-z\.-]+(:[0-9]+)?");
|
||||||
|
|
||||||
|
protected GlobalBase(GlobalBase clone)
|
||||||
|
{
|
||||||
|
this.logger = clone.logger;
|
||||||
|
this.notificationConnectors = clone.notificationConnectors;
|
||||||
|
this.libraryConnectors = clone.libraryConnectors;
|
||||||
|
this.cachedPublications = clone.cachedPublications;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected GlobalBase(Logger? logger)
|
||||||
|
{
|
||||||
|
this.logger = logger;
|
||||||
|
this.notificationConnectors = TrangaSettings.LoadNotificationConnectors(this);
|
||||||
|
this.libraryConnectors = TrangaSettings.LoadLibraryConnectors(this);
|
||||||
|
this.cachedPublications = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void AddMangaToCache(Manga manga)
|
||||||
|
{
|
||||||
|
if (!this.cachedPublications.TryAdd(manga.internalId, manga))
|
||||||
|
{
|
||||||
|
Log($"Overwriting Manga {manga.internalId}");
|
||||||
|
this.cachedPublications[manga.internalId] = manga;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected Manga? GetCachedManga(string internalId)
|
||||||
|
{
|
||||||
|
return cachedPublications.TryGetValue(internalId, out Manga manga) switch
|
||||||
|
{
|
||||||
|
true => manga,
|
||||||
|
_ => null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
protected IEnumerable<Manga> GetAllCachedManga()
|
||||||
|
{
|
||||||
|
return cachedPublications.Values;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void Log(string message)
|
||||||
|
{
|
||||||
|
logger?.WriteLine(this.GetType().Name, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void Log(string fStr, params object?[] replace)
|
||||||
|
{
|
||||||
|
Log(string.Format(fStr, replace));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void SendNotifications(string title, string text, bool buffer = false)
|
||||||
|
{
|
||||||
|
foreach (NotificationConnector nc in notificationConnectors)
|
||||||
|
nc.SendNotification(title, text, buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void AddNotificationConnector(NotificationConnector notificationConnector)
|
||||||
|
{
|
||||||
|
Log($"Adding {notificationConnector}");
|
||||||
|
notificationConnectors.RemoveWhere(nc => nc.notificationConnectorType == notificationConnector.notificationConnectorType);
|
||||||
|
notificationConnectors.Add(notificationConnector);
|
||||||
|
|
||||||
|
while(IsFileInUse(TrangaSettings.notificationConnectorsFilePath))
|
||||||
|
Thread.Sleep(100);
|
||||||
|
Log("Exporting notificationConnectors");
|
||||||
|
File.WriteAllText(TrangaSettings.notificationConnectorsFilePath, JsonConvert.SerializeObject(notificationConnectors));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void DeleteNotificationConnector(NotificationConnector.NotificationConnectorType notificationConnectorType)
|
||||||
|
{
|
||||||
|
Log($"Removing {notificationConnectorType}");
|
||||||
|
notificationConnectors.RemoveWhere(nc => nc.notificationConnectorType == notificationConnectorType);
|
||||||
|
while(IsFileInUse(TrangaSettings.notificationConnectorsFilePath))
|
||||||
|
Thread.Sleep(100);
|
||||||
|
Log("Exporting notificationConnectors");
|
||||||
|
File.WriteAllText(TrangaSettings.notificationConnectorsFilePath, JsonConvert.SerializeObject(notificationConnectors));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void UpdateLibraries()
|
||||||
|
{
|
||||||
|
foreach(LibraryConnector lc in libraryConnectors)
|
||||||
|
lc.UpdateLibrary();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void AddLibraryConnector(LibraryConnector libraryConnector)
|
||||||
|
{
|
||||||
|
Log($"Adding {libraryConnector}");
|
||||||
|
libraryConnectors.RemoveWhere(lc => lc.libraryType == libraryConnector.libraryType);
|
||||||
|
libraryConnectors.Add(libraryConnector);
|
||||||
|
|
||||||
|
while(IsFileInUse(TrangaSettings.libraryConnectorsFilePath))
|
||||||
|
Thread.Sleep(100);
|
||||||
|
Log("Exporting libraryConnectors");
|
||||||
|
File.WriteAllText(TrangaSettings.libraryConnectorsFilePath, JsonConvert.SerializeObject(libraryConnectors, Formatting.Indented));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void DeleteLibraryConnector(LibraryConnector.LibraryType libraryType)
|
||||||
|
{
|
||||||
|
Log($"Removing {libraryType}");
|
||||||
|
libraryConnectors.RemoveWhere(lc => lc.libraryType == libraryType);
|
||||||
|
while(IsFileInUse(TrangaSettings.libraryConnectorsFilePath))
|
||||||
|
Thread.Sleep(100);
|
||||||
|
Log("Exporting libraryConnectors");
|
||||||
|
File.WriteAllText(TrangaSettings.libraryConnectorsFilePath, JsonConvert.SerializeObject(libraryConnectors, Formatting.Indented));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected bool IsFileInUse(string filePath) => IsFileInUse(filePath, this.logger);
|
||||||
|
|
||||||
|
public static bool IsFileInUse(string filePath, Logger? logger)
|
||||||
|
{
|
||||||
|
if (!File.Exists(filePath))
|
||||||
|
return false;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using FileStream stream = new (filePath, FileMode.Open, FileAccess.Read, FileShare.None);
|
||||||
|
stream.Close();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
catch (IOException)
|
||||||
|
{
|
||||||
|
logger?.WriteLine($"File is in use {filePath}");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
54
Tranga/Jobs/DownloadChapter.cs
Normal file
54
Tranga/Jobs/DownloadChapter.cs
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
using System.Net;
|
||||||
|
using Tranga.MangaConnectors;
|
||||||
|
|
||||||
|
namespace Tranga.Jobs;
|
||||||
|
|
||||||
|
public class DownloadChapter : Job
|
||||||
|
{
|
||||||
|
public Chapter chapter { get; init; }
|
||||||
|
|
||||||
|
public DownloadChapter(GlobalBase clone, MangaConnector connector, Chapter chapter, DateTime lastExecution, string? parentJobId = null) : base(clone, JobType.DownloadChapterJob, connector, lastExecution, parentJobId: parentJobId)
|
||||||
|
{
|
||||||
|
this.chapter = chapter;
|
||||||
|
}
|
||||||
|
|
||||||
|
public DownloadChapter(GlobalBase clone, MangaConnector connector, Chapter chapter, string? parentJobId = null) : base(clone, JobType.DownloadChapterJob, connector, parentJobId: parentJobId)
|
||||||
|
{
|
||||||
|
this.chapter = chapter;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override string GetId()
|
||||||
|
{
|
||||||
|
return $"{GetType()}-{chapter.parentManga.internalId}-{chapter.chapterNumber}";
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
return $"{id} Chapter: {chapter}";
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override IEnumerable<Job> ExecuteReturnSubTasksInternal(JobBoss jobBoss)
|
||||||
|
{
|
||||||
|
Task downloadTask = new(delegate
|
||||||
|
{
|
||||||
|
mangaConnector.CopyCoverFromCacheToDownloadLocation(chapter.parentManga);
|
||||||
|
HttpStatusCode success = mangaConnector.DownloadChapter(chapter, this.progressToken);
|
||||||
|
chapter.parentManga.UpdateLatestDownloadedChapter(chapter);
|
||||||
|
if (success == HttpStatusCode.OK)
|
||||||
|
{
|
||||||
|
UpdateLibraries();
|
||||||
|
SendNotifications("Chapter downloaded", $"{chapter.parentManga.sortName} - {chapter.chapterNumber}", true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
downloadTask.Start();
|
||||||
|
return Array.Empty<Job>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool Equals(object? obj)
|
||||||
|
{
|
||||||
|
if (obj is not DownloadChapter otherJob)
|
||||||
|
return false;
|
||||||
|
return otherJob.mangaConnector == this.mangaConnector &&
|
||||||
|
otherJob.chapter.Equals(this.chapter);
|
||||||
|
}
|
||||||
|
}
|
59
Tranga/Jobs/DownloadNewChapters.cs
Normal file
59
Tranga/Jobs/DownloadNewChapters.cs
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
using Tranga.MangaConnectors;
|
||||||
|
|
||||||
|
namespace Tranga.Jobs;
|
||||||
|
|
||||||
|
public class DownloadNewChapters : Job
|
||||||
|
{
|
||||||
|
public Manga manga { get; set; }
|
||||||
|
public string translatedLanguage { get; init; }
|
||||||
|
|
||||||
|
public DownloadNewChapters(GlobalBase clone, MangaConnector connector, Manga manga, DateTime lastExecution,
|
||||||
|
bool recurring = false, TimeSpan? recurrence = null, string? parentJobId = null, string translatedLanguage = "en") : base(clone, JobType.DownloadNewChaptersJob, connector, lastExecution, recurring,
|
||||||
|
recurrence, parentJobId)
|
||||||
|
{
|
||||||
|
this.manga = manga;
|
||||||
|
this.translatedLanguage = translatedLanguage;
|
||||||
|
}
|
||||||
|
|
||||||
|
public DownloadNewChapters(GlobalBase clone, MangaConnector connector, Manga manga, bool recurring = false, TimeSpan? recurrence = null, string? parentJobId = null, string translatedLanguage = "en") : base (clone, JobType.DownloadNewChaptersJob, connector, recurring, recurrence, parentJobId)
|
||||||
|
{
|
||||||
|
this.manga = manga;
|
||||||
|
this.translatedLanguage = translatedLanguage;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override string GetId()
|
||||||
|
{
|
||||||
|
return $"{GetType()}-{manga.internalId}";
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
return $"{id} Manga: {manga}";
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override IEnumerable<Job> ExecuteReturnSubTasksInternal(JobBoss jobBoss)
|
||||||
|
{
|
||||||
|
manga.SaveSeriesInfoJson();
|
||||||
|
Chapter[] chapters = mangaConnector.GetNewChapters(manga, this.translatedLanguage);
|
||||||
|
this.progressToken.increments = chapters.Length;
|
||||||
|
List<Job> jobs = new();
|
||||||
|
mangaConnector.CopyCoverFromCacheToDownloadLocation(manga);
|
||||||
|
foreach (Chapter chapter in chapters)
|
||||||
|
{
|
||||||
|
DownloadChapter downloadChapterJob = new(this, this.mangaConnector, chapter, parentJobId: this.id);
|
||||||
|
jobs.Add(downloadChapterJob);
|
||||||
|
}
|
||||||
|
UpdateMetadata updateMetadataJob = new(this, this.mangaConnector, this.manga, parentJobId: this.id);
|
||||||
|
jobs.Add(updateMetadataJob);
|
||||||
|
progressToken.Complete();
|
||||||
|
return jobs;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool Equals(object? obj)
|
||||||
|
{
|
||||||
|
if (obj is not DownloadNewChapters otherJob)
|
||||||
|
return false;
|
||||||
|
return otherJob.mangaConnector == this.mangaConnector &&
|
||||||
|
otherJob.manga.publicationId == this.manga.publicationId;
|
||||||
|
}
|
||||||
|
}
|
98
Tranga/Jobs/Job.cs
Normal file
98
Tranga/Jobs/Job.cs
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
using Tranga.MangaConnectors;
|
||||||
|
|
||||||
|
namespace Tranga.Jobs;
|
||||||
|
|
||||||
|
public abstract class Job : GlobalBase
|
||||||
|
{
|
||||||
|
public MangaConnector mangaConnector { get; init; }
|
||||||
|
public ProgressToken progressToken { get; private set; }
|
||||||
|
public bool recurring { get; init; }
|
||||||
|
public TimeSpan? recurrenceTime { get; set; }
|
||||||
|
public DateTime? lastExecution { get; private set; }
|
||||||
|
public DateTime nextExecution => NextExecution();
|
||||||
|
public string id => GetId();
|
||||||
|
internal IEnumerable<Job>? subJobs { get; private set; }
|
||||||
|
public string? parentJobId { get; init; }
|
||||||
|
public enum JobType : byte { DownloadChapterJob, DownloadNewChaptersJob, UpdateMetaDataJob }
|
||||||
|
|
||||||
|
public JobType jobType;
|
||||||
|
|
||||||
|
internal Job(GlobalBase clone, JobType jobType, MangaConnector connector, bool recurring = false, TimeSpan? recurrenceTime = null, string? parentJobId = null) : base(clone)
|
||||||
|
{
|
||||||
|
this.jobType = jobType;
|
||||||
|
this.mangaConnector = connector;
|
||||||
|
this.progressToken = new ProgressToken(0);
|
||||||
|
this.recurring = recurring;
|
||||||
|
if (recurring && recurrenceTime is null)
|
||||||
|
throw new ArgumentException("If recurrence is set to true, a recurrence time has to be provided.");
|
||||||
|
else if(recurring && recurrenceTime is not null)
|
||||||
|
this.lastExecution = DateTime.Now.Subtract((TimeSpan)recurrenceTime);
|
||||||
|
this.recurrenceTime = recurrenceTime ?? TimeSpan.Zero;
|
||||||
|
this.parentJobId = parentJobId;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal Job(GlobalBase clone, JobType jobType, MangaConnector connector, DateTime lastExecution, bool recurring = false,
|
||||||
|
TimeSpan? recurrenceTime = null, string? parentJobId = null) : base(clone)
|
||||||
|
{
|
||||||
|
this.jobType = jobType;
|
||||||
|
this.mangaConnector = connector;
|
||||||
|
this.progressToken = new ProgressToken(0);
|
||||||
|
this.recurring = recurring;
|
||||||
|
if (recurring && recurrenceTime is null)
|
||||||
|
throw new ArgumentException("If recurrence is set to true, a recurrence time has to be provided.");
|
||||||
|
this.lastExecution = lastExecution;
|
||||||
|
this.recurrenceTime = recurrenceTime ?? TimeSpan.Zero;
|
||||||
|
this.parentJobId = parentJobId;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract string GetId();
|
||||||
|
|
||||||
|
public void AddSubJob(Job job)
|
||||||
|
{
|
||||||
|
subJobs ??= new List<Job>();
|
||||||
|
subJobs = subJobs.Append(job);
|
||||||
|
}
|
||||||
|
|
||||||
|
private DateTime NextExecution()
|
||||||
|
{
|
||||||
|
if(recurrenceTime.HasValue && lastExecution.HasValue)
|
||||||
|
return lastExecution.Value.Add(recurrenceTime.Value);
|
||||||
|
if(recurrenceTime.HasValue && !lastExecution.HasValue)
|
||||||
|
return DateTime.Now;
|
||||||
|
return DateTime.MaxValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ResetProgress()
|
||||||
|
{
|
||||||
|
this.progressToken.increments -= progressToken.incrementsCompleted;
|
||||||
|
this.lastExecution = DateTime.Now;
|
||||||
|
this.progressToken.Waiting();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ExecutionEnqueue()
|
||||||
|
{
|
||||||
|
this.progressToken.increments -= progressToken.incrementsCompleted;
|
||||||
|
this.progressToken.Standby();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Cancel()
|
||||||
|
{
|
||||||
|
Log($"Cancelling {this}");
|
||||||
|
this.progressToken.cancellationRequested = true;
|
||||||
|
this.progressToken.Cancel();
|
||||||
|
this.lastExecution = DateTime.Now;
|
||||||
|
if(subJobs is not null)
|
||||||
|
foreach(Job subJob in subJobs)
|
||||||
|
subJob.Cancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerable<Job> ExecuteReturnSubTasks(JobBoss jobBoss)
|
||||||
|
{
|
||||||
|
progressToken.Start();
|
||||||
|
subJobs = ExecuteReturnSubTasksInternal(jobBoss);
|
||||||
|
lastExecution = DateTime.Now;
|
||||||
|
return subJobs;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract IEnumerable<Job> ExecuteReturnSubTasksInternal(JobBoss jobBoss);
|
||||||
|
}
|
301
Tranga/Jobs/JobBoss.cs
Normal file
301
Tranga/Jobs/JobBoss.cs
Normal file
@ -0,0 +1,301 @@
|
|||||||
|
using System.Diagnostics;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using Tranga.MangaConnectors;
|
||||||
|
using static System.IO.UnixFileMode;
|
||||||
|
|
||||||
|
namespace Tranga.Jobs;
|
||||||
|
|
||||||
|
public class JobBoss : GlobalBase
|
||||||
|
{
|
||||||
|
public HashSet<Job> jobs { get; init; }
|
||||||
|
private Dictionary<MangaConnector, Queue<Job>> mangaConnectorJobQueue { get; init; }
|
||||||
|
|
||||||
|
public JobBoss(GlobalBase clone, HashSet<MangaConnector> connectors) : base(clone)
|
||||||
|
{
|
||||||
|
this.jobs = new();
|
||||||
|
LoadJobsList(connectors);
|
||||||
|
this.mangaConnectorJobQueue = new();
|
||||||
|
Log($"Next job in {jobs.MinBy(job => job.nextExecution)?.nextExecution.Subtract(DateTime.Now)} {jobs.MinBy(job => job.nextExecution)?.id}");
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool AddJob(Job job, string? jobFile = null)
|
||||||
|
{
|
||||||
|
if (ContainsJobLike(job))
|
||||||
|
{
|
||||||
|
Log($"Already Contains Job {job}");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (!this.jobs.Add(job))
|
||||||
|
return false;
|
||||||
|
Log($"Added {job}");
|
||||||
|
UpdateJobFile(job, jobFile);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddJobs(IEnumerable<Job> jobsToAdd)
|
||||||
|
{
|
||||||
|
foreach (Job job in jobsToAdd)
|
||||||
|
AddJob(job);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Compares contents of the provided job and all current jobs
|
||||||
|
/// Does not check if objects are the same
|
||||||
|
/// </summary>
|
||||||
|
public bool ContainsJobLike(Job job)
|
||||||
|
{
|
||||||
|
return this.jobs.Any(existingJob => existingJob.Equals(job));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RemoveJob(Job job)
|
||||||
|
{
|
||||||
|
Log($"Removing {job}");
|
||||||
|
job.Cancel();
|
||||||
|
this.jobs.Remove(job);
|
||||||
|
if(job.subJobs is not null && job.subJobs.Any())
|
||||||
|
RemoveJobs(job.subJobs);
|
||||||
|
UpdateJobFile(job);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RemoveJobs(IEnumerable<Job?> jobsToRemove)
|
||||||
|
{
|
||||||
|
List<Job?> toRemove = jobsToRemove.ToList(); //Prevent multiple enumeration
|
||||||
|
Log($"Removing {toRemove.Count()} jobs.");
|
||||||
|
foreach (Job? job in toRemove)
|
||||||
|
if(job is not null)
|
||||||
|
RemoveJob(job);
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerable<Job> GetJobsLike(string? connectorName = null, string? internalId = null, float? chapterNumber = null)
|
||||||
|
{
|
||||||
|
IEnumerable<Job> ret = this.jobs;
|
||||||
|
if (connectorName is not null)
|
||||||
|
ret = ret.Where(job => job.mangaConnector.name == connectorName);
|
||||||
|
|
||||||
|
if (internalId is not null && chapterNumber is not null)
|
||||||
|
ret = ret.Where(jjob =>
|
||||||
|
{
|
||||||
|
if (jjob is not DownloadChapter job)
|
||||||
|
return false;
|
||||||
|
return job.chapter.parentManga.internalId == internalId &&
|
||||||
|
job.chapter.chapterNumber.Equals(chapterNumber);
|
||||||
|
});
|
||||||
|
else if (internalId is not null)
|
||||||
|
ret = ret.Where(jjob =>
|
||||||
|
{
|
||||||
|
if (jjob is not DownloadNewChapters job)
|
||||||
|
return false;
|
||||||
|
return job.manga.internalId == internalId;
|
||||||
|
});
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerable<Job> GetJobsLike(MangaConnector? mangaConnector = null, Manga? publication = null,
|
||||||
|
Chapter? chapter = null)
|
||||||
|
{
|
||||||
|
if (chapter is not null)
|
||||||
|
return GetJobsLike(mangaConnector?.name, chapter.Value.parentManga.internalId, chapter.Value.chapterNumber);
|
||||||
|
else
|
||||||
|
return GetJobsLike(mangaConnector?.name, publication?.internalId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Job? GetJobById(string jobId)
|
||||||
|
{
|
||||||
|
if (this.jobs.FirstOrDefault(jjob => jjob.id == jobId) is { } job)
|
||||||
|
return job;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryGetJobById(string jobId, out Job? job)
|
||||||
|
{
|
||||||
|
if (this.jobs.FirstOrDefault(jjob => jjob.id == jobId) is { } ret)
|
||||||
|
{
|
||||||
|
job = ret;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
job = null;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool QueueContainsJob(Job job)
|
||||||
|
{
|
||||||
|
if (mangaConnectorJobQueue.TryAdd(job.mangaConnector, new Queue<Job>()))//If we can add the queue, there is certainly no job in it
|
||||||
|
return true;
|
||||||
|
return mangaConnectorJobQueue[job.mangaConnector].Contains(job);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddJobToQueue(Job job)
|
||||||
|
{
|
||||||
|
Log($"Adding Job to Queue. {job}");
|
||||||
|
if(!QueueContainsJob(job))
|
||||||
|
mangaConnectorJobQueue[job.mangaConnector].Enqueue(job);
|
||||||
|
job.ExecutionEnqueue();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddJobsToQueue(IEnumerable<Job> newJobs)
|
||||||
|
{
|
||||||
|
foreach(Job job in newJobs)
|
||||||
|
AddJobToQueue(job);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void LoadJobsList(HashSet<MangaConnector> connectors)
|
||||||
|
{
|
||||||
|
if (!Directory.Exists(TrangaSettings.jobsFolderPath)) //No jobs to load
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(TrangaSettings.jobsFolderPath);
|
||||||
|
if(RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
||||||
|
File.SetUnixFileMode(TrangaSettings.jobsFolderPath, UserRead | UserWrite | UserExecute | GroupRead | OtherRead);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
//Load json-job-files
|
||||||
|
foreach (FileInfo file in Directory.GetFiles(TrangaSettings.jobsFolderPath, "*.json").Select(f => new FileInfo(f)))
|
||||||
|
{
|
||||||
|
Log($"Adding {file.Name}");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Job? job = JsonConvert.DeserializeObject<Job>(File.ReadAllText(file.FullName),
|
||||||
|
new JobJsonConverter(this, new MangaConnectorJsonConverter(this, connectors)));
|
||||||
|
if (job is null) throw new NullReferenceException();
|
||||||
|
|
||||||
|
Log($"Adding Job {job}");
|
||||||
|
if (!AddJob(job, file.FullName)) //If we detect a duplicate, delete the file.
|
||||||
|
{
|
||||||
|
//string path = string.Concat(file.FullName, ".duplicate");
|
||||||
|
//file.MoveTo(path);
|
||||||
|
//Log($"Duplicate detected or otherwise not able to add job to list.\nMoved job {job} to {path}");
|
||||||
|
Log($"Duplicate detected or otherwise not able to add job to list. Removed the file {file.FullName} {job}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
if (e is not UnreachableException or NullReferenceException)
|
||||||
|
throw;
|
||||||
|
Log(e.Message);
|
||||||
|
string newName = file.FullName + ".failed";
|
||||||
|
Log($"Failed loading file {file.Name}.\nMoving to {newName}.\n" +
|
||||||
|
$"If you think this is a bug, upload contents of the file to the Bugreport!");
|
||||||
|
File.Move(file.FullName, newName);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//Connect jobs to parent-jobs and add Publications to cache
|
||||||
|
foreach (Job job in this.jobs)
|
||||||
|
{
|
||||||
|
Log($"Loading Job {job}");
|
||||||
|
Job? parentJob = this.jobs.FirstOrDefault(jjob => jjob.id == job.parentJobId);
|
||||||
|
if (parentJob is not null)
|
||||||
|
{
|
||||||
|
parentJob.AddSubJob(job);
|
||||||
|
Log($"Parent Job {parentJob}");
|
||||||
|
}
|
||||||
|
if (job is DownloadNewChapters dncJob)
|
||||||
|
AddMangaToCache(dncJob.manga);
|
||||||
|
}
|
||||||
|
|
||||||
|
string[] coverFiles = Directory.GetFiles(TrangaSettings.coverImageCache);
|
||||||
|
foreach(string fileName in coverFiles.Where(fileName => !GetAllCachedManga().Any(manga => manga.coverFileNameInCache == fileName)))
|
||||||
|
File.Delete(fileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void UpdateJobFile(Job job, string? oldFile = null)
|
||||||
|
{
|
||||||
|
string newJobFilePath = Path.Join(TrangaSettings.jobsFolderPath, $"{job.id}.json");
|
||||||
|
string oldFilePath = oldFile??Path.Join(TrangaSettings.jobsFolderPath, $"{job.id}.json");
|
||||||
|
|
||||||
|
//Delete old file
|
||||||
|
if (File.Exists(oldFilePath))
|
||||||
|
{
|
||||||
|
Log($"Deleting Job-file {oldFilePath}");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
while(IsFileInUse(oldFilePath))
|
||||||
|
Thread.Sleep(10);
|
||||||
|
File.Delete(oldFilePath);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Log($"Error deleting {oldFilePath} job {job.id}\n{e}");
|
||||||
|
return; //Don't export a new file when we haven't actually deleted the old one
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//Export job (in new file) if it is still in our jobs list
|
||||||
|
if (GetJobById(job.id) is not null)
|
||||||
|
{
|
||||||
|
Log($"Exporting Job {newJobFilePath}");
|
||||||
|
string jobStr = JsonConvert.SerializeObject(job, Formatting.Indented);
|
||||||
|
while(IsFileInUse(newJobFilePath))
|
||||||
|
Thread.Sleep(10);
|
||||||
|
File.WriteAllText(newJobFilePath, jobStr);
|
||||||
|
if(RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
||||||
|
File.SetUnixFileMode(newJobFilePath, UserRead | UserWrite | GroupRead | OtherRead);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateAllJobFiles()
|
||||||
|
{
|
||||||
|
Log("Exporting Jobs");
|
||||||
|
foreach (Job job in this.jobs)
|
||||||
|
UpdateJobFile(job);
|
||||||
|
|
||||||
|
//Remove files with jobs not in this.jobs-list
|
||||||
|
Regex idRex = new (@"(.*)\.json");
|
||||||
|
foreach (FileInfo file in new DirectoryInfo(TrangaSettings.jobsFolderPath).EnumerateFiles())
|
||||||
|
{
|
||||||
|
if (idRex.IsMatch(file.Name))
|
||||||
|
{
|
||||||
|
string id = idRex.Match(file.Name).Groups[1].Value;
|
||||||
|
if (!this.jobs.Any(job => job.id == id))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
file.Delete();
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Log(e.ToString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void CheckJobs()
|
||||||
|
{
|
||||||
|
AddJobsToQueue(jobs.Where(job => job.progressToken.state == ProgressToken.State.Waiting && job.nextExecution < DateTime.Now && !QueueContainsJob(job)).OrderBy(job => job.nextExecution));
|
||||||
|
foreach (Queue<Job> jobQueue in mangaConnectorJobQueue.Values)
|
||||||
|
{
|
||||||
|
if(jobQueue.Count < 1)
|
||||||
|
continue;
|
||||||
|
Job queueHead = jobQueue.Peek();
|
||||||
|
if (queueHead.progressToken.state is ProgressToken.State.Complete or ProgressToken.State.Cancelled)
|
||||||
|
{
|
||||||
|
if(!queueHead.recurring)
|
||||||
|
RemoveJob(queueHead);
|
||||||
|
else
|
||||||
|
queueHead.ResetProgress();
|
||||||
|
jobQueue.Dequeue();
|
||||||
|
Log($"Next job in {jobs.MinBy(job => job.nextExecution)?.nextExecution.Subtract(DateTime.Now)} {jobs.MinBy(job => job.nextExecution)?.id}");
|
||||||
|
}else if (queueHead.progressToken.state is ProgressToken.State.Standby)
|
||||||
|
{
|
||||||
|
Job eJob = jobQueue.Peek();
|
||||||
|
Job[] subJobs = eJob.ExecuteReturnSubTasks(this).ToArray();
|
||||||
|
UpdateJobFile(eJob);
|
||||||
|
AddJobs(subJobs);
|
||||||
|
AddJobsToQueue(subJobs);
|
||||||
|
}else if (queueHead.progressToken.state is ProgressToken.State.Running && DateTime.Now.Subtract(queueHead.progressToken.lastUpdate) > TimeSpan.FromMinutes(5))
|
||||||
|
{
|
||||||
|
Log($"{queueHead} inactive for more than 5 minutes. Cancelling.");
|
||||||
|
queueHead.Cancel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
84
Tranga/Jobs/JobJsonConverter.cs
Normal file
84
Tranga/Jobs/JobJsonConverter.cs
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
using Newtonsoft.Json;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
using Tranga.MangaConnectors;
|
||||||
|
|
||||||
|
namespace Tranga.Jobs;
|
||||||
|
|
||||||
|
public class JobJsonConverter : JsonConverter
|
||||||
|
{
|
||||||
|
private GlobalBase _clone;
|
||||||
|
private MangaConnectorJsonConverter _mangaConnectorJsonConverter;
|
||||||
|
|
||||||
|
internal JobJsonConverter(GlobalBase clone, MangaConnectorJsonConverter mangaConnectorJsonConverter)
|
||||||
|
{
|
||||||
|
this._clone = clone;
|
||||||
|
this._mangaConnectorJsonConverter = mangaConnectorJsonConverter;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool CanConvert(Type objectType)
|
||||||
|
{
|
||||||
|
return (objectType == typeof(Job));
|
||||||
|
}
|
||||||
|
|
||||||
|
public override object ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
|
||||||
|
{
|
||||||
|
JObject jo = JObject.Load(reader);
|
||||||
|
|
||||||
|
if (jo.ContainsKey("jobType") && jo["jobType"]!.Value<byte>() == (byte)Job.JobType.UpdateMetaDataJob)
|
||||||
|
{
|
||||||
|
return new UpdateMetadata(this._clone,
|
||||||
|
jo.GetValue("mangaConnector")!.ToObject<MangaConnector>(JsonSerializer.Create(new JsonSerializerSettings()
|
||||||
|
{
|
||||||
|
Converters =
|
||||||
|
{
|
||||||
|
this._mangaConnectorJsonConverter
|
||||||
|
}
|
||||||
|
}))!,
|
||||||
|
jo.GetValue("manga")!.ToObject<Manga>(),
|
||||||
|
jo.GetValue("parentJobId")!.Value<string?>());
|
||||||
|
}else if ((jo.ContainsKey("jobType") && jo["jobType"]!.Value<byte>() == (byte)Job.JobType.DownloadNewChaptersJob) || jo.ContainsKey("translatedLanguage"))//TODO change to jobType
|
||||||
|
{
|
||||||
|
DateTime lastExecution = jo.GetValue("lastExecution") is {} le
|
||||||
|
? le.ToObject<DateTime>()
|
||||||
|
: DateTime.UnixEpoch; //TODO do null checks on all variables
|
||||||
|
return new DownloadNewChapters(this._clone,
|
||||||
|
jo.GetValue("mangaConnector")!.ToObject<MangaConnector>(JsonSerializer.Create(new JsonSerializerSettings()
|
||||||
|
{
|
||||||
|
Converters =
|
||||||
|
{
|
||||||
|
this._mangaConnectorJsonConverter
|
||||||
|
}
|
||||||
|
}))!,
|
||||||
|
jo.GetValue("manga")!.ToObject<Manga>(),
|
||||||
|
lastExecution,
|
||||||
|
jo.GetValue("recurring")!.Value<bool>(),
|
||||||
|
jo.GetValue("recurrenceTime")!.ToObject<TimeSpan?>(),
|
||||||
|
jo.GetValue("parentJobId")!.Value<string?>());
|
||||||
|
}else if ((jo.ContainsKey("jobType") && jo["jobType"]!.Value<byte>() == (byte)Job.JobType.DownloadChapterJob) || jo.ContainsKey("chapter"))//TODO change to jobType
|
||||||
|
{
|
||||||
|
return new DownloadChapter(this._clone,
|
||||||
|
jo.GetValue("mangaConnector")!.ToObject<MangaConnector>(JsonSerializer.Create(new JsonSerializerSettings()
|
||||||
|
{
|
||||||
|
Converters =
|
||||||
|
{
|
||||||
|
this._mangaConnectorJsonConverter
|
||||||
|
}
|
||||||
|
}))!,
|
||||||
|
jo.GetValue("chapter")!.ToObject<Chapter>(),
|
||||||
|
DateTime.UnixEpoch,
|
||||||
|
jo.GetValue("parentJobId")!.Value<string?>());
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Exception();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool CanWrite => false;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Don't call this
|
||||||
|
/// </summary>
|
||||||
|
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
|
||||||
|
{
|
||||||
|
throw new Exception("Dont call this");
|
||||||
|
}
|
||||||
|
}
|
78
Tranga/Jobs/ProgressToken.cs
Normal file
78
Tranga/Jobs/ProgressToken.cs
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
namespace Tranga.Jobs;
|
||||||
|
|
||||||
|
public class ProgressToken
|
||||||
|
{
|
||||||
|
public bool cancellationRequested { get; set; }
|
||||||
|
public int increments { get; set; }
|
||||||
|
public int incrementsCompleted { get; set; }
|
||||||
|
public float progress => GetProgress();
|
||||||
|
public DateTime lastUpdate { get; private set; }
|
||||||
|
public DateTime executionStarted { get; private set; }
|
||||||
|
public TimeSpan timeRemaining => GetTimeRemaining();
|
||||||
|
|
||||||
|
public enum State { Running, Complete, Standby, Cancelled, Waiting }
|
||||||
|
public State state { get; private set; }
|
||||||
|
|
||||||
|
public ProgressToken(int increments)
|
||||||
|
{
|
||||||
|
this.cancellationRequested = false;
|
||||||
|
this.increments = increments;
|
||||||
|
this.incrementsCompleted = 0;
|
||||||
|
this.state = State.Waiting;
|
||||||
|
this.executionStarted = DateTime.UnixEpoch;
|
||||||
|
this.lastUpdate = DateTime.UnixEpoch;
|
||||||
|
}
|
||||||
|
|
||||||
|
private float GetProgress()
|
||||||
|
{
|
||||||
|
if(increments > 0 && incrementsCompleted > 0)
|
||||||
|
return incrementsCompleted / (float)increments;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private TimeSpan GetTimeRemaining()
|
||||||
|
{
|
||||||
|
if (increments > 0 && incrementsCompleted > 0)
|
||||||
|
return DateTime.Now.Subtract(this.executionStarted).Divide(incrementsCompleted).Multiply(increments - incrementsCompleted);
|
||||||
|
return TimeSpan.MaxValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Increment()
|
||||||
|
{
|
||||||
|
this.lastUpdate = DateTime.Now;
|
||||||
|
this.incrementsCompleted++;
|
||||||
|
if (incrementsCompleted > increments)
|
||||||
|
state = State.Complete;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Standby()
|
||||||
|
{
|
||||||
|
this.lastUpdate = DateTime.Now;
|
||||||
|
state = State.Standby;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Start()
|
||||||
|
{
|
||||||
|
this.lastUpdate = DateTime.Now;
|
||||||
|
state = State.Running;
|
||||||
|
this.executionStarted = DateTime.Now;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Complete()
|
||||||
|
{
|
||||||
|
this.lastUpdate = DateTime.Now;
|
||||||
|
state = State.Complete;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Cancel()
|
||||||
|
{
|
||||||
|
this.lastUpdate = DateTime.Now;
|
||||||
|
state = State.Cancelled;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Waiting()
|
||||||
|
{
|
||||||
|
this.lastUpdate = DateTime.Now;
|
||||||
|
state = State.Waiting;
|
||||||
|
}
|
||||||
|
}
|
76
Tranga/Jobs/UpdateMetadata.cs
Normal file
76
Tranga/Jobs/UpdateMetadata.cs
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
using Tranga.MangaConnectors;
|
||||||
|
|
||||||
|
namespace Tranga.Jobs;
|
||||||
|
|
||||||
|
public class UpdateMetadata : Job
|
||||||
|
{
|
||||||
|
public Manga manga { get; set; }
|
||||||
|
|
||||||
|
public UpdateMetadata(GlobalBase clone, MangaConnector connector, Manga manga, string? parentJobId = null) : base(clone, JobType.UpdateMetaDataJob, connector, parentJobId: parentJobId)
|
||||||
|
{
|
||||||
|
this.manga = manga;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override string GetId()
|
||||||
|
{
|
||||||
|
return $"{GetType()}-{manga.internalId}";
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
return $"{id} Manga: {manga}";
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override IEnumerable<Job> ExecuteReturnSubTasksInternal(JobBoss jobBoss)
|
||||||
|
{
|
||||||
|
//Retrieve new Metadata
|
||||||
|
Manga? possibleUpdatedManga = mangaConnector.GetMangaFromId(manga.publicationId);
|
||||||
|
if (possibleUpdatedManga is { } updatedManga)
|
||||||
|
{
|
||||||
|
if (updatedManga.Equals(this.manga)) //Check if anything changed
|
||||||
|
{
|
||||||
|
this.progressToken.Complete();
|
||||||
|
return Array.Empty<Job>();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.manga = manga.WithMetadata(updatedManga);
|
||||||
|
this.manga.SaveSeriesInfoJson(true);
|
||||||
|
this.mangaConnector.CopyCoverFromCacheToDownloadLocation(manga);
|
||||||
|
foreach (Job job in jobBoss.GetJobsLike(publication: this.manga))
|
||||||
|
{
|
||||||
|
string oldFile;
|
||||||
|
if (job is DownloadNewChapters dc)
|
||||||
|
{
|
||||||
|
oldFile = dc.id;
|
||||||
|
dc.manga = this.manga;
|
||||||
|
}
|
||||||
|
else if (job is UpdateMetadata um)
|
||||||
|
{
|
||||||
|
oldFile = um.id;
|
||||||
|
um.manga = this.manga;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
continue;
|
||||||
|
jobBoss.UpdateJobFile(job, oldFile);
|
||||||
|
}
|
||||||
|
this.progressToken.Complete();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Log($"Could not find Manga {manga}");
|
||||||
|
this.progressToken.Cancel();
|
||||||
|
return Array.Empty<Job>();
|
||||||
|
}
|
||||||
|
this.progressToken.Cancel();
|
||||||
|
return Array.Empty<Job>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool Equals(object? obj)
|
||||||
|
{
|
||||||
|
|
||||||
|
if (obj is not UpdateMetadata otherJob)
|
||||||
|
return false;
|
||||||
|
return otherJob.mangaConnector == this.mangaConnector &&
|
||||||
|
otherJob.manga.publicationId == this.manga.publicationId;
|
||||||
|
}
|
||||||
|
}
|
@ -1,22 +1,29 @@
|
|||||||
using System.Text.Json;
|
using System.Text.Json.Nodes;
|
||||||
using System.Text.Json.Nodes;
|
using Logging;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using JsonSerializer = System.Text.Json.JsonSerializer;
|
||||||
|
|
||||||
namespace API.Schema.LibraryConnectors;
|
namespace Tranga.LibraryConnectors;
|
||||||
|
|
||||||
public class Kavita : LibraryConnector
|
public class Kavita : LibraryConnector
|
||||||
{
|
{
|
||||||
|
|
||||||
public Kavita(string baseUrl, string auth) : base(TokenGen.CreateToken(typeof(Kavita), baseUrl), LibraryType.Kavita, baseUrl, auth)
|
public Kavita(GlobalBase clone, string baseUrl, string username, string password) :
|
||||||
|
base(clone, baseUrl, GetToken(baseUrl, username, password, clone.logger), LibraryType.Kavita)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
public Kavita(string baseUrl, string username, string password) :
|
[JsonConstructor]
|
||||||
this(baseUrl, GetToken(baseUrl, username, password))
|
public Kavita(GlobalBase clone, string baseUrl, string auth) : base(clone, baseUrl, auth, LibraryType.Kavita)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
private static string GetToken(string baseUrl, string username, string password)
|
{
|
||||||
|
return $"Kavita {baseUrl}";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetToken(string baseUrl, string username, string password, Logger? logger = null)
|
||||||
{
|
{
|
||||||
HttpClient client = new()
|
HttpClient client = new()
|
||||||
{
|
{
|
||||||
@ -34,6 +41,7 @@ public class Kavita : LibraryConnector
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
HttpResponseMessage response = client.Send(requestMessage);
|
HttpResponseMessage response = client.Send(requestMessage);
|
||||||
|
logger?.WriteLine($"Kavita | GetToken {requestMessage.RequestUri} -> {response.StatusCode}");
|
||||||
if (response.IsSuccessStatusCode)
|
if (response.IsSuccessStatusCode)
|
||||||
{
|
{
|
||||||
JsonObject? result = JsonSerializer.Deserialize<JsonObject>(response.Content.ReadAsStream());
|
JsonObject? result = JsonSerializer.Deserialize<JsonObject>(response.Content.ReadAsStream());
|
||||||
@ -42,24 +50,28 @@ public class Kavita : LibraryConnector
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
logger?.WriteLine($"Kavita | {response.Content}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (HttpRequestException e)
|
catch (HttpRequestException e)
|
||||||
{
|
{
|
||||||
|
logger?.WriteLine($"Kavita | Unable to retrieve token:\n\r{e}");
|
||||||
}
|
}
|
||||||
|
logger?.WriteLine("Kavita | Did not receive token.");
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void UpdateLibraryInternal()
|
protected override void UpdateLibraryInternal()
|
||||||
{
|
{
|
||||||
|
Log("Updating libraries.");
|
||||||
foreach (KavitaLibrary lib in GetLibraries())
|
foreach (KavitaLibrary lib in GetLibraries())
|
||||||
NetClient.MakePost($"{BaseUrl}/api/Library/scan?libraryId={lib.id}", "Bearer", Auth);
|
NetClient.MakePost($"{baseUrl}/api/Library/scan?libraryId={lib.id}", "Bearer", auth, logger);
|
||||||
}
|
}
|
||||||
|
|
||||||
internal override bool Test()
|
internal override bool Test()
|
||||||
{
|
{
|
||||||
foreach (KavitaLibrary lib in GetLibraries())
|
foreach (KavitaLibrary lib in GetLibraries())
|
||||||
if (NetClient.MakePost($"{BaseUrl}/api/Library/scan?libraryId={lib.id}", "Bearer", Auth))
|
if (NetClient.MakePost($"{baseUrl}/api/Library/scan?libraryId={lib.id}", "Bearer", auth, logger))
|
||||||
return true;
|
return true;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -70,14 +82,17 @@ public class Kavita : LibraryConnector
|
|||||||
/// <returns>Array of KavitaLibrary</returns>
|
/// <returns>Array of KavitaLibrary</returns>
|
||||||
private IEnumerable<KavitaLibrary> GetLibraries()
|
private IEnumerable<KavitaLibrary> GetLibraries()
|
||||||
{
|
{
|
||||||
Stream data = NetClient.MakeRequest($"{BaseUrl}/api/Library/libraries", "Bearer", Auth);
|
Log("Getting libraries.");
|
||||||
|
Stream data = NetClient.MakeRequest($"{baseUrl}/api/Library/libraries", "Bearer", auth, logger);
|
||||||
if (data == Stream.Null)
|
if (data == Stream.Null)
|
||||||
{
|
{
|
||||||
|
Log("No libraries returned");
|
||||||
return Array.Empty<KavitaLibrary>();
|
return Array.Empty<KavitaLibrary>();
|
||||||
}
|
}
|
||||||
JsonArray? result = JsonSerializer.Deserialize<JsonArray>(data);
|
JsonArray? result = JsonSerializer.Deserialize<JsonArray>(data);
|
||||||
if (result is null)
|
if (result is null)
|
||||||
{
|
{
|
||||||
|
Log("No libraries returned");
|
||||||
return Array.Empty<KavitaLibrary>();
|
return Array.Empty<KavitaLibrary>();
|
||||||
}
|
}
|
||||||
|
|
@ -1,30 +1,41 @@
|
|||||||
using System.Text.Json;
|
using System.Text.Json.Nodes;
|
||||||
using System.Text.Json.Nodes;
|
using Newtonsoft.Json;
|
||||||
|
using JsonSerializer = System.Text.Json.JsonSerializer;
|
||||||
|
|
||||||
namespace API.Schema.LibraryConnectors;
|
namespace Tranga.LibraryConnectors;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Provides connectivity to Komga-API
|
||||||
|
/// Can fetch and update libraries
|
||||||
|
/// </summary>
|
||||||
public class Komga : LibraryConnector
|
public class Komga : LibraryConnector
|
||||||
{
|
{
|
||||||
public Komga(string baseUrl, string auth) : base(TokenGen.CreateToken(typeof(Komga), baseUrl), LibraryType.Komga,
|
public Komga(GlobalBase clone, string baseUrl, string username, string password)
|
||||||
baseUrl, auth)
|
: base(clone, baseUrl, Convert.ToBase64String(System.Text.Encoding.ASCII.GetBytes($"{username}:{password}")), LibraryType.Komga)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
public Komga(string baseUrl, string username, string password)
|
[JsonConstructor]
|
||||||
: this(baseUrl, Convert.ToBase64String(System.Text.Encoding.ASCII.GetBytes($"{username}:{password}")))
|
public Komga(GlobalBase clone, string baseUrl, string auth) : base(clone, baseUrl, auth, LibraryType.Komga)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
return $"Komga {baseUrl}";
|
||||||
|
}
|
||||||
|
|
||||||
protected override void UpdateLibraryInternal()
|
protected override void UpdateLibraryInternal()
|
||||||
{
|
{
|
||||||
|
Log("Updating libraries.");
|
||||||
foreach (KomgaLibrary lib in GetLibraries())
|
foreach (KomgaLibrary lib in GetLibraries())
|
||||||
NetClient.MakePost($"{BaseUrl}/api/v1/libraries/{lib.id}/scan", "Basic", Auth);
|
NetClient.MakePost($"{baseUrl}/api/v1/libraries/{lib.id}/scan", "Basic", auth, logger);
|
||||||
}
|
}
|
||||||
|
|
||||||
internal override bool Test()
|
internal override bool Test()
|
||||||
{
|
{
|
||||||
foreach (KomgaLibrary lib in GetLibraries())
|
foreach (KomgaLibrary lib in GetLibraries())
|
||||||
if (NetClient.MakePost($"{BaseUrl}/api/v1/libraries/{lib.id}/scan", "Basic", Auth))
|
if (NetClient.MakePost($"{baseUrl}/api/v1/libraries/{lib.id}/scan", "Basic", auth, logger))
|
||||||
return true;
|
return true;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -35,14 +46,17 @@ public class Komga : LibraryConnector
|
|||||||
/// <returns>Array of KomgaLibraries</returns>
|
/// <returns>Array of KomgaLibraries</returns>
|
||||||
private IEnumerable<KomgaLibrary> GetLibraries()
|
private IEnumerable<KomgaLibrary> GetLibraries()
|
||||||
{
|
{
|
||||||
Stream data = NetClient.MakeRequest($"{BaseUrl}/api/v1/libraries", "Basic", Auth);
|
Log("Getting Libraries");
|
||||||
|
Stream data = NetClient.MakeRequest($"{baseUrl}/api/v1/libraries", "Basic", auth, logger);
|
||||||
if (data == Stream.Null)
|
if (data == Stream.Null)
|
||||||
{
|
{
|
||||||
|
Log("No libraries returned");
|
||||||
return Array.Empty<KomgaLibrary>();
|
return Array.Empty<KomgaLibrary>();
|
||||||
}
|
}
|
||||||
JsonArray? result = JsonSerializer.Deserialize<JsonArray>(data);
|
JsonArray? result = JsonSerializer.Deserialize<JsonArray>(data);
|
||||||
if (result is null)
|
if (result is null)
|
||||||
{
|
{
|
||||||
|
Log("No libraries returned");
|
||||||
return Array.Empty<KomgaLibrary>();
|
return Array.Empty<KomgaLibrary>();
|
||||||
}
|
}
|
||||||
|
|
144
Tranga/LibraryConnectors/LibraryConnector.cs
Normal file
144
Tranga/LibraryConnectors/LibraryConnector.cs
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
using System.Net;
|
||||||
|
using System.Net.Http.Headers;
|
||||||
|
using Logging;
|
||||||
|
|
||||||
|
namespace Tranga.LibraryConnectors;
|
||||||
|
|
||||||
|
public abstract class LibraryConnector : GlobalBase
|
||||||
|
{
|
||||||
|
public enum LibraryType : byte
|
||||||
|
{
|
||||||
|
Komga = 0,
|
||||||
|
Kavita = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReSharper disable once UnusedAutoPropertyAccessor.Global
|
||||||
|
public LibraryType libraryType { get; }
|
||||||
|
public string baseUrl { get; }
|
||||||
|
// ReSharper disable once MemberCanBeProtected.Global
|
||||||
|
public string auth { get; } //Base64 encoded, if you use your password everywhere, you have problems
|
||||||
|
private DateTime? _updateLibraryRequested = null;
|
||||||
|
private readonly Thread? _libraryBufferThread = null;
|
||||||
|
private const int NoChangeTimeout = 2, BiggestInterval = 20;
|
||||||
|
|
||||||
|
protected LibraryConnector(GlobalBase clone, string baseUrl, string auth, LibraryType libraryType) : base(clone)
|
||||||
|
{
|
||||||
|
Log($"Creating libraryConnector {Enum.GetName(libraryType)}");
|
||||||
|
if (!baseUrlRex.IsMatch(baseUrl))
|
||||||
|
throw new ArgumentException("Base url does not match pattern");
|
||||||
|
if(auth == "")
|
||||||
|
throw new ArgumentNullException(nameof(auth), "Auth can not be empty");
|
||||||
|
this.baseUrl = baseUrlRex.Match(baseUrl).Value;
|
||||||
|
this.auth = auth;
|
||||||
|
this.libraryType = libraryType;
|
||||||
|
|
||||||
|
if (TrangaSettings.bufferLibraryUpdates)
|
||||||
|
{
|
||||||
|
_libraryBufferThread = new(CheckLibraryBuffer);
|
||||||
|
_libraryBufferThread.Start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CheckLibraryBuffer()
|
||||||
|
{
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
if (_updateLibraryRequested is not null && DateTime.Now.Subtract((DateTime)_updateLibraryRequested) > TimeSpan.FromMinutes(NoChangeTimeout)) //If no updates have been requested for NoChangeTimeout minutes, update library
|
||||||
|
{
|
||||||
|
UpdateLibraryInternal();
|
||||||
|
_updateLibraryRequested = null;
|
||||||
|
}
|
||||||
|
Thread.Sleep(100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UpdateLibrary()
|
||||||
|
{
|
||||||
|
_updateLibraryRequested ??= DateTime.Now;
|
||||||
|
if (!TrangaSettings.bufferLibraryUpdates)
|
||||||
|
{
|
||||||
|
UpdateLibraryInternal();
|
||||||
|
return;
|
||||||
|
}else if (_updateLibraryRequested is not null &&
|
||||||
|
DateTime.Now.Subtract((DateTime)_updateLibraryRequested) > TimeSpan.FromMinutes(BiggestInterval)) //If the last update has been more than BiggestInterval minutes ago, update library
|
||||||
|
{
|
||||||
|
UpdateLibraryInternal();
|
||||||
|
_updateLibraryRequested = null;
|
||||||
|
}
|
||||||
|
else if(_updateLibraryRequested is not null)
|
||||||
|
{
|
||||||
|
Log($"Buffering Library Updates (Updates in latest {((DateTime)_updateLibraryRequested).Add(TimeSpan.FromMinutes(BiggestInterval)).Subtract(DateTime.Now)} or {((DateTime)_updateLibraryRequested).Add(TimeSpan.FromMinutes(NoChangeTimeout)).Subtract(DateTime.Now)})");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract void UpdateLibraryInternal();
|
||||||
|
internal abstract bool Test();
|
||||||
|
|
||||||
|
protected static class NetClient
|
||||||
|
{
|
||||||
|
public static Stream MakeRequest(string url, string authScheme, string auth, Logger? logger)
|
||||||
|
{
|
||||||
|
HttpClient client = new();
|
||||||
|
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(authScheme, auth);
|
||||||
|
|
||||||
|
HttpRequestMessage requestMessage = new ()
|
||||||
|
{
|
||||||
|
Method = HttpMethod.Get,
|
||||||
|
RequestUri = new Uri(url)
|
||||||
|
};
|
||||||
|
try
|
||||||
|
{
|
||||||
|
|
||||||
|
HttpResponseMessage response = client.Send(requestMessage);
|
||||||
|
logger?.WriteLine("LibraryManager.NetClient",
|
||||||
|
$"GET {url} -> {(int)response.StatusCode}: {response.ReasonPhrase}");
|
||||||
|
|
||||||
|
if (response.StatusCode is HttpStatusCode.Unauthorized &&
|
||||||
|
response.RequestMessage!.RequestUri!.AbsoluteUri != url)
|
||||||
|
return MakeRequest(response.RequestMessage!.RequestUri!.AbsoluteUri, authScheme, auth, logger);
|
||||||
|
else if (response.IsSuccessStatusCode)
|
||||||
|
return response.Content.ReadAsStream();
|
||||||
|
else
|
||||||
|
return Stream.Null;
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
switch (e)
|
||||||
|
{
|
||||||
|
case HttpRequestException:
|
||||||
|
logger?.WriteLine("LibraryManager.NetClient", $"Failed to make Request:\n\r{e}\n\rContinuing.");
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
return Stream.Null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool MakePost(string url, string authScheme, string auth, Logger? logger)
|
||||||
|
{
|
||||||
|
HttpClient client = new()
|
||||||
|
{
|
||||||
|
DefaultRequestHeaders =
|
||||||
|
{
|
||||||
|
{ "Accept", "application/json" },
|
||||||
|
{ "Authorization", new AuthenticationHeaderValue(authScheme, auth).ToString() }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
HttpRequestMessage requestMessage = new ()
|
||||||
|
{
|
||||||
|
Method = HttpMethod.Post,
|
||||||
|
RequestUri = new Uri(url)
|
||||||
|
};
|
||||||
|
HttpResponseMessage response = client.Send(requestMessage);
|
||||||
|
logger?.WriteLine("LibraryManager.NetClient", $"POST {url} -> {(int)response.StatusCode}: {response.ReasonPhrase}");
|
||||||
|
|
||||||
|
if(response.StatusCode is HttpStatusCode.Unauthorized && response.RequestMessage!.RequestUri!.AbsoluteUri != url)
|
||||||
|
return MakePost(response.RequestMessage!.RequestUri!.AbsoluteUri, authScheme, auth, logger);
|
||||||
|
else if (response.IsSuccessStatusCode)
|
||||||
|
return true;
|
||||||
|
else
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
45
Tranga/LibraryConnectors/LibraryManagerJsonConverter.cs
Normal file
45
Tranga/LibraryConnectors/LibraryManagerJsonConverter.cs
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
using Newtonsoft.Json;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
|
||||||
|
namespace Tranga.LibraryConnectors;
|
||||||
|
|
||||||
|
public class LibraryManagerJsonConverter : JsonConverter
|
||||||
|
{
|
||||||
|
private readonly GlobalBase _clone;
|
||||||
|
|
||||||
|
internal LibraryManagerJsonConverter(GlobalBase clone)
|
||||||
|
{
|
||||||
|
this._clone = clone;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool CanConvert(Type objectType)
|
||||||
|
{
|
||||||
|
return (objectType == typeof(LibraryConnector));
|
||||||
|
}
|
||||||
|
|
||||||
|
public override object ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
|
||||||
|
{
|
||||||
|
JObject jo = JObject.Load(reader);
|
||||||
|
if (jo["libraryType"]!.Value<byte>() == (byte)LibraryConnector.LibraryType.Komga)
|
||||||
|
return new Komga(this._clone,
|
||||||
|
jo.GetValue("baseUrl")!.Value<string>()!,
|
||||||
|
jo.GetValue("auth")!.Value<string>()!);
|
||||||
|
|
||||||
|
if (jo["libraryType"]!.Value<byte>() == (byte)LibraryConnector.LibraryType.Kavita)
|
||||||
|
return new Kavita(this._clone,
|
||||||
|
jo.GetValue("baseUrl")!.Value<string>()!,
|
||||||
|
jo.GetValue("auth")!.Value<string>()!);
|
||||||
|
|
||||||
|
throw new Exception();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool CanWrite => false;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Don't call this
|
||||||
|
/// </summary>
|
||||||
|
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
|
||||||
|
{
|
||||||
|
throw new Exception("Dont call this");
|
||||||
|
}
|
||||||
|
}
|
222
Tranga/Manga.cs
Normal file
222
Tranga/Manga.cs
Normal file
@ -0,0 +1,222 @@
|
|||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using System.Web;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using static System.IO.UnixFileMode;
|
||||||
|
|
||||||
|
namespace Tranga;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Contains information on a Publication (Manga)
|
||||||
|
/// </summary>
|
||||||
|
public struct Manga
|
||||||
|
{
|
||||||
|
public string sortName { get; private set; }
|
||||||
|
public List<string> authors { get; private set; }
|
||||||
|
// ReSharper disable once UnusedAutoPropertyAccessor.Global
|
||||||
|
public Dictionary<string,string> altTitles { get; private set; }
|
||||||
|
// ReSharper disable once MemberCanBePrivate.Global
|
||||||
|
public string? description { get; private set; }
|
||||||
|
public string[] tags { get; private set; }
|
||||||
|
// ReSharper disable once UnusedAutoPropertyAccessor.Global
|
||||||
|
public string? coverUrl { get; private set; }
|
||||||
|
public string? coverFileNameInCache { get; private set; }
|
||||||
|
// ReSharper disable once UnusedAutoPropertyAccessor.Global
|
||||||
|
public Dictionary<string,string> links { get; }
|
||||||
|
// ReSharper disable once MemberCanBePrivate.Global
|
||||||
|
public int? year { get; private set; }
|
||||||
|
public string? originalLanguage { get; }
|
||||||
|
// ReSharper disable twice MemberCanBePrivate.Global
|
||||||
|
public string status { get; private set; }
|
||||||
|
public ReleaseStatusByte releaseStatus { get; private set; }
|
||||||
|
public enum ReleaseStatusByte : byte
|
||||||
|
{
|
||||||
|
Continuing = 0,
|
||||||
|
Completed = 1,
|
||||||
|
OnHiatus = 2,
|
||||||
|
Cancelled = 3,
|
||||||
|
Unreleased = 4
|
||||||
|
};
|
||||||
|
public string folderName { get; private set; }
|
||||||
|
public string publicationId { get; }
|
||||||
|
public string internalId { get; }
|
||||||
|
public float ignoreChaptersBelow { get; set; }
|
||||||
|
public float latestChapterDownloaded { get; set; }
|
||||||
|
public float latestChapterAvailable { get; set; }
|
||||||
|
|
||||||
|
public string? websiteUrl { get; private set; }
|
||||||
|
|
||||||
|
private static readonly Regex LegalCharacters = new (@"[A-Za-zÀ-ÖØ-öø-ÿ0-9 \.\-,'\'\)\(~!\+]*");
|
||||||
|
|
||||||
|
[JsonConstructor]
|
||||||
|
public Manga(string sortName, List<string> authors, string? description, Dictionary<string,string> altTitles, string[] tags, string? coverUrl, string? coverFileNameInCache, Dictionary<string,string>? links, int? year, string? originalLanguage, string publicationId, ReleaseStatusByte releaseStatus, string? websiteUrl = null, string? folderName = null, float? ignoreChaptersBelow = 0)
|
||||||
|
{
|
||||||
|
this.sortName = HttpUtility.HtmlDecode(sortName);
|
||||||
|
this.authors = authors.Select(HttpUtility.HtmlDecode).ToList()!;
|
||||||
|
this.description = HttpUtility.HtmlDecode(description);
|
||||||
|
this.altTitles = altTitles.ToDictionary(a => HttpUtility.HtmlDecode(a.Key), a => HttpUtility.HtmlDecode(a.Value));
|
||||||
|
this.tags = tags.Select(HttpUtility.HtmlDecode).ToArray()!;
|
||||||
|
this.coverFileNameInCache = coverFileNameInCache;
|
||||||
|
this.coverUrl = coverUrl;
|
||||||
|
this.links = links ?? new Dictionary<string, string>();
|
||||||
|
this.year = year;
|
||||||
|
this.originalLanguage = originalLanguage;
|
||||||
|
this.publicationId = publicationId;
|
||||||
|
this.folderName = folderName ?? string.Concat(LegalCharacters.Matches(HttpUtility.HtmlDecode(sortName)));
|
||||||
|
while (this.folderName.EndsWith('.'))
|
||||||
|
this.folderName = this.folderName.Substring(0, this.folderName.Length - 1);
|
||||||
|
string onlyLowerLetters = string.Concat(this.sortName.ToLower().Where(Char.IsLetter));
|
||||||
|
this.internalId = DateTime.Now.Ticks.ToString();
|
||||||
|
this.ignoreChaptersBelow = ignoreChaptersBelow ?? 0f;
|
||||||
|
this.latestChapterDownloaded = 0;
|
||||||
|
this.latestChapterAvailable = 0;
|
||||||
|
this.releaseStatus = releaseStatus;
|
||||||
|
this.status = Enum.GetName(releaseStatus) ?? "";
|
||||||
|
this.websiteUrl = websiteUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Manga WithMetadata(Manga newManga)
|
||||||
|
{
|
||||||
|
return this with
|
||||||
|
{
|
||||||
|
sortName = newManga.sortName,
|
||||||
|
description = newManga.description,
|
||||||
|
coverUrl = newManga.coverUrl,
|
||||||
|
authors = authors.Union(newManga.authors).ToList(),
|
||||||
|
altTitles = altTitles.UnionBy(newManga.altTitles, kv => kv.Key).ToDictionary(x => x.Key, x => x.Value),
|
||||||
|
tags = tags.Union(newManga.tags).ToArray(),
|
||||||
|
status = newManga.status,
|
||||||
|
releaseStatus = newManga.releaseStatus,
|
||||||
|
websiteUrl = newManga.websiteUrl,
|
||||||
|
year = newManga.year,
|
||||||
|
coverFileNameInCache = newManga.coverFileNameInCache
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool Equals(object? obj)
|
||||||
|
{
|
||||||
|
if (obj is not Manga compareManga)
|
||||||
|
return false;
|
||||||
|
return this.description == compareManga.description &&
|
||||||
|
this.year == compareManga.year &&
|
||||||
|
this.status == compareManga.status &&
|
||||||
|
this.releaseStatus == compareManga.releaseStatus &&
|
||||||
|
this.sortName == compareManga.sortName &&
|
||||||
|
this.latestChapterAvailable.Equals(compareManga.latestChapterAvailable) &&
|
||||||
|
this.authors.All(a => compareManga.authors.Contains(a)) &&
|
||||||
|
(this.coverFileNameInCache??"").Equals(compareManga.coverFileNameInCache) &&
|
||||||
|
(this.websiteUrl??"").Equals(compareManga.websiteUrl) &&
|
||||||
|
this.tags.All(t => compareManga.tags.Contains(t));
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
return $"Publication {sortName} {internalId}";
|
||||||
|
}
|
||||||
|
|
||||||
|
public string CreatePublicationFolder(string downloadDirectory)
|
||||||
|
{
|
||||||
|
string publicationFolder = Path.Join(downloadDirectory, this.folderName);
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void MovePublicationFolder(string downloadDirectory, string newFolderName)
|
||||||
|
{
|
||||||
|
string oldPath = Path.Join(downloadDirectory, this.folderName);
|
||||||
|
this.folderName = newFolderName;//Create new Path with the new folderName
|
||||||
|
string newPath = CreatePublicationFolder(downloadDirectory);
|
||||||
|
if (Directory.Exists(oldPath))
|
||||||
|
{
|
||||||
|
if (Directory.Exists(newPath)) //Move/Overwrite old Files, Delete old Directory
|
||||||
|
{
|
||||||
|
IEnumerable<string> newPathFileNames = new DirectoryInfo(newPath).GetFiles().Select(fi => fi.Name);
|
||||||
|
foreach(FileInfo fileInfo in new DirectoryInfo(oldPath).GetFiles().Where(fi => newPathFileNames.Contains(fi.Name) == false))
|
||||||
|
File.Move(fileInfo.FullName, Path.Join(newPath, fileInfo.Name), true);
|
||||||
|
Directory.Delete(oldPath);
|
||||||
|
}else
|
||||||
|
Directory.Move(oldPath, newPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UpdateLatestDownloadedChapter(Chapter chapter)//TODO check files if chapters are all downloaded
|
||||||
|
{
|
||||||
|
float chapterNumber = Convert.ToSingle(chapter.chapterNumber, GlobalBase.numberFormatDecimalPoint);
|
||||||
|
latestChapterDownloaded = latestChapterDownloaded < chapterNumber ? chapterNumber : latestChapterDownloaded;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SaveSeriesInfoJson(bool overwrite = false)
|
||||||
|
{
|
||||||
|
string publicationFolder = CreatePublicationFolder(TrangaSettings.downloadLocation);
|
||||||
|
string seriesInfoPath = Path.Join(publicationFolder, "series.json");
|
||||||
|
if(overwrite || (!overwrite && !File.Exists(seriesInfoPath)))
|
||||||
|
File.WriteAllText(seriesInfoPath,this.GetSeriesInfoJson());
|
||||||
|
if(RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
||||||
|
File.SetUnixFileMode(seriesInfoPath, GroupRead | GroupWrite | OtherRead | OtherWrite | UserRead | UserWrite);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <returns>Serialized JSON String for series.json</returns>
|
||||||
|
private string GetSeriesInfoJson()
|
||||||
|
{
|
||||||
|
SeriesInfo si = new (new Metadata(this));
|
||||||
|
return System.Text.Json.JsonSerializer.Serialize(si);
|
||||||
|
}
|
||||||
|
|
||||||
|
//Only for series.json
|
||||||
|
private struct SeriesInfo
|
||||||
|
{
|
||||||
|
// ReSharper disable once UnusedAutoPropertyAccessor.Local we need it, trust
|
||||||
|
[JsonRequired]public Metadata metadata { get; }
|
||||||
|
public SeriesInfo(Metadata metadata) => this.metadata = metadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
//Only for series.json what an abomination, why are all the fields not-null????
|
||||||
|
private struct Metadata
|
||||||
|
{
|
||||||
|
// ReSharper disable UnusedAutoPropertyAccessor.Local we need them all, trust me
|
||||||
|
[JsonRequired] public string type { get; }
|
||||||
|
[JsonRequired] public string publisher { get; }
|
||||||
|
// ReSharper disable twice IdentifierTypo
|
||||||
|
[JsonRequired] public int comicid { get; }
|
||||||
|
[JsonRequired] public string booktype { get; }
|
||||||
|
// ReSharper disable InconsistentNaming This one property is capitalized. Why?
|
||||||
|
[JsonRequired] public string ComicImage { get; }
|
||||||
|
[JsonRequired] public int total_issues { get; }
|
||||||
|
[JsonRequired] public string publication_run { get; }
|
||||||
|
[JsonRequired]public string name { get; }
|
||||||
|
[JsonRequired]public string year { get; }
|
||||||
|
[JsonRequired]public string status { get; }
|
||||||
|
[JsonRequired]public string description_text { get; }
|
||||||
|
|
||||||
|
public Metadata(Manga manga) : this(manga.sortName, manga.year.ToString() ?? string.Empty, manga.releaseStatus, manga.description ?? "")
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public Metadata(string name, string year, ReleaseStatusByte status, string description_text)
|
||||||
|
{
|
||||||
|
this.name = name;
|
||||||
|
this.year = year;
|
||||||
|
this.status = status switch
|
||||||
|
{
|
||||||
|
ReleaseStatusByte.Continuing => "Continuing",
|
||||||
|
ReleaseStatusByte.Completed => "Ended",
|
||||||
|
_ => Enum.GetName(status) ?? "Ended"
|
||||||
|
};
|
||||||
|
this.description_text = description_text;
|
||||||
|
|
||||||
|
//kill it with fire, but otherwise Komga will not parse
|
||||||
|
type = "Manga";
|
||||||
|
publisher = "";
|
||||||
|
comicid = 0;
|
||||||
|
booktype = "";
|
||||||
|
ComicImage = "";
|
||||||
|
total_issues = 0;
|
||||||
|
publication_run = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,54 +1,58 @@
|
|||||||
using System.Text.RegularExpressions;
|
using System.Net;
|
||||||
using API.MangaDownloadClients;
|
using System.Text.RegularExpressions;
|
||||||
using HtmlAgilityPack;
|
using HtmlAgilityPack;
|
||||||
using log4net;
|
using Tranga.Jobs;
|
||||||
|
|
||||||
namespace API.Schema.MangaConnectors;
|
namespace Tranga.MangaConnectors;
|
||||||
|
|
||||||
public class AsuraToon : MangaConnector
|
public class AsuraToon : MangaConnector
|
||||||
{
|
{
|
||||||
|
|
||||||
public AsuraToon() : base("AsuraToon", ["en"], ["asuracomic.net"], "https://asuracomic.net/images/logo.webp")
|
public AsuraToon(GlobalBase clone) : base(clone, "AsuraToon", ["en"])
|
||||||
{
|
{
|
||||||
this.downloadClient = new ChromiumDownloadClient();
|
this.downloadClient = new ChromiumDownloadClient(clone);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)[] GetManga(string publicationTitle = "")
|
public override Manga[] GetManga(string publicationTitle = "")
|
||||||
{
|
{
|
||||||
|
Log($"Searching Publications. Term=\"{publicationTitle}\"");
|
||||||
string sanitizedTitle = string.Join(' ', Regex.Matches(publicationTitle, "[A-z]*").Where(m => m.Value.Length > 0)).ToLower();
|
string sanitizedTitle = string.Join(' ', Regex.Matches(publicationTitle, "[A-z]*").Where(m => m.Value.Length > 0)).ToLower();
|
||||||
string requestUrl = $"https://asuracomic.net/series?name={sanitizedTitle}";
|
string requestUrl = $"https://asuracomic.net/series?name={sanitizedTitle}";
|
||||||
RequestResult requestResult =
|
RequestResult requestResult =
|
||||||
downloadClient.MakeRequest(requestUrl, RequestType.Default);
|
downloadClient.MakeRequest(requestUrl, RequestType.Default);
|
||||||
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
|
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
|
||||||
return [];
|
return Array.Empty<Manga>();
|
||||||
|
|
||||||
if (requestResult.htmlDocument is null)
|
if (requestResult.htmlDocument is null)
|
||||||
{
|
{
|
||||||
return [];
|
Log($"Failed to retrieve site");
|
||||||
|
return Array.Empty<Manga>();
|
||||||
}
|
}
|
||||||
|
|
||||||
(Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)[] publications = ParsePublicationsFromHtml(requestResult.htmlDocument);
|
Manga[] publications = ParsePublicationsFromHtml(requestResult.htmlDocument);
|
||||||
|
Log($"Retrieved {publications.Length} publications. Term=\"{publicationTitle}\"");
|
||||||
return publications;
|
return publications;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)? GetMangaFromId(string publicationId)
|
public override Manga? GetMangaFromId(string publicationId)
|
||||||
{
|
{
|
||||||
return GetMangaFromUrl($"https://asuracomic.net/series/{publicationId}");
|
return GetMangaFromUrl($"https://asuracomic.net/series/{publicationId}");
|
||||||
}
|
}
|
||||||
|
|
||||||
public override (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)? GetMangaFromUrl(string url)
|
public override Manga? GetMangaFromUrl(string url)
|
||||||
{
|
{
|
||||||
RequestResult requestResult = downloadClient.MakeRequest(url, RequestType.MangaInfo);
|
RequestResult requestResult = downloadClient.MakeRequest(url, RequestType.MangaInfo);
|
||||||
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
|
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
|
||||||
return null;
|
return null;
|
||||||
if (requestResult.htmlDocument is null)
|
if (requestResult.htmlDocument is null)
|
||||||
{
|
{
|
||||||
|
Log($"Failed to retrieve site");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return ParseSinglePublicationFromHtml(requestResult.htmlDocument, url.Split('/')[^1], url);
|
return ParseSinglePublicationFromHtml(requestResult.htmlDocument, url.Split('/')[^1], url);
|
||||||
}
|
}
|
||||||
|
|
||||||
private (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)[] ParsePublicationsFromHtml(HtmlDocument document)
|
private Manga[] ParsePublicationsFromHtml(HtmlDocument document)
|
||||||
{
|
{
|
||||||
HtmlNodeCollection mangaList = document.DocumentNode.SelectNodes("//a[starts-with(@href,'series')]");
|
HtmlNodeCollection mangaList = document.DocumentNode.SelectNodes("//a[starts-with(@href,'series')]");
|
||||||
if (mangaList is null || mangaList.Count < 1)
|
if (mangaList is null || mangaList.Count < 1)
|
||||||
@ -56,41 +60,41 @@ public class AsuraToon : MangaConnector
|
|||||||
|
|
||||||
IEnumerable<string> urls = mangaList.Select(a => $"https://asuracomic.net/{a.GetAttributeValue("href", "")}");
|
IEnumerable<string> urls = mangaList.Select(a => $"https://asuracomic.net/{a.GetAttributeValue("href", "")}");
|
||||||
|
|
||||||
List<(Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)> ret = new();
|
List<Manga> ret = new();
|
||||||
foreach (string url in urls)
|
foreach (string url in urls)
|
||||||
{
|
{
|
||||||
(Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)? manga = GetMangaFromUrl(url);
|
Manga? manga = GetMangaFromUrl(url);
|
||||||
if (manga is { } x)
|
if (manga is not null)
|
||||||
ret.Add(x);
|
ret.Add((Manga)manga);
|
||||||
}
|
}
|
||||||
|
|
||||||
return ret.ToArray();
|
return ret.ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
private (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?) ParseSinglePublicationFromHtml(HtmlDocument document, string publicationId, string websiteUrl)
|
private Manga ParseSinglePublicationFromHtml(HtmlDocument document, string publicationId, string websiteUrl)
|
||||||
{
|
{
|
||||||
string? originalLanguage = null;
|
string? originalLanguage = null;
|
||||||
Dictionary<string, string> altTitles = new(), links = new();
|
Dictionary<string, string> altTitles = new(), links = new();
|
||||||
|
|
||||||
HtmlNodeCollection genreNodes = document.DocumentNode.SelectNodes("//h3[text()='Genres']/../div/button");
|
HtmlNodeCollection genreNodes = document.DocumentNode.SelectNodes("//h3[text()='Genres']/../div/button");
|
||||||
string[] tags = genreNodes.Select(b => b.InnerText).ToArray();
|
string[] tags = genreNodes.Select(b => b.InnerText).ToArray();
|
||||||
List<MangaTag> mangaTags = tags.Select(t => new MangaTag(t)).ToList();
|
|
||||||
|
|
||||||
HtmlNode statusNode = document.DocumentNode.SelectSingleNode("//h3[text()='Status']/../h3[2]");
|
HtmlNode statusNode = document.DocumentNode.SelectSingleNode("//h3[text()='Status']/../h3[2]");
|
||||||
MangaReleaseStatus releaseStatus = statusNode.InnerText.ToLower() switch
|
Manga.ReleaseStatusByte releaseStatus = statusNode.InnerText.ToLower() switch
|
||||||
{
|
{
|
||||||
"ongoing" => MangaReleaseStatus.Continuing,
|
"ongoing" => Manga.ReleaseStatusByte.Continuing,
|
||||||
"hiatus" => MangaReleaseStatus.OnHiatus,
|
"hiatus" => Manga.ReleaseStatusByte.OnHiatus,
|
||||||
"completed" => MangaReleaseStatus.Completed,
|
"completed" => Manga.ReleaseStatusByte.Completed,
|
||||||
"dropped" => MangaReleaseStatus.Cancelled,
|
"dropped" => Manga.ReleaseStatusByte.Cancelled,
|
||||||
"season end" => MangaReleaseStatus.Continuing,
|
"season end" => Manga.ReleaseStatusByte.Continuing,
|
||||||
"coming soon" => MangaReleaseStatus.Unreleased,
|
"coming soon" => Manga.ReleaseStatusByte.Unreleased,
|
||||||
_ => MangaReleaseStatus.Unreleased
|
_ => Manga.ReleaseStatusByte.Unreleased
|
||||||
};
|
};
|
||||||
|
|
||||||
HtmlNode coverNode =
|
HtmlNode coverNode =
|
||||||
document.DocumentNode.SelectSingleNode("//img[@alt='poster']");
|
document.DocumentNode.SelectSingleNode("//img[@alt='poster']");
|
||||||
string coverUrl = coverNode.GetAttributeValue("src", "");
|
string coverUrl = coverNode.GetAttributeValue("src", "");
|
||||||
|
string coverFileNameInCache = SaveCoverImageToCache(coverUrl, publicationId, RequestType.MangaCover);
|
||||||
|
|
||||||
HtmlNode titleNode =
|
HtmlNode titleNode =
|
||||||
document.DocumentNode.SelectSingleNode("//title");
|
document.DocumentNode.SelectSingleNode("//title");
|
||||||
@ -104,34 +108,30 @@ public class AsuraToon : MangaConnector
|
|||||||
HtmlNodeCollection artistNodes = document.DocumentNode.SelectNodes("//h3[text()='Artist']/../h3[not(text()='Artist' or text()='_')]");
|
HtmlNodeCollection artistNodes = document.DocumentNode.SelectNodes("//h3[text()='Artist']/../h3[not(text()='Artist' or text()='_')]");
|
||||||
IEnumerable<string> authorNames = authorNodes is null ? [] : authorNodes.Select(a => a.InnerText);
|
IEnumerable<string> authorNames = authorNodes is null ? [] : authorNodes.Select(a => a.InnerText);
|
||||||
IEnumerable<string> artistNames = artistNodes is null ? [] : artistNodes.Select(a => a.InnerText);
|
IEnumerable<string> artistNames = artistNodes is null ? [] : artistNodes.Select(a => a.InnerText);
|
||||||
List<string> authorStrings = authorNames.Concat(artistNames).ToList();
|
List<string> authors = authorNames.Concat(artistNames).ToList();
|
||||||
List<Author> authors = authorStrings.Select(author => new Author(author)).ToList();
|
|
||||||
|
|
||||||
HtmlNode? firstChapterNode = document.DocumentNode.SelectSingleNode("//a[contains(@href, 'chapter/1')]/../following-sibling::h3");
|
HtmlNode? firstChapterNode = document.DocumentNode.SelectSingleNode("//a[contains(@href, 'chapter/1')]/../following-sibling::h3");
|
||||||
uint year = uint.Parse(firstChapterNode?.InnerText.Split(' ')[^1] ?? "2000");
|
int? year = int.Parse(firstChapterNode?.InnerText.Split(' ')[^1] ?? "2000");
|
||||||
|
|
||||||
Manga manga = new (publicationId, sortName, description, websiteUrl, coverUrl, null, year,
|
|
||||||
originalLanguage, releaseStatus, -1,
|
|
||||||
this,
|
|
||||||
authors,
|
|
||||||
mangaTags,
|
|
||||||
[],
|
|
||||||
[]);
|
|
||||||
|
|
||||||
return (manga, authors, mangaTags, [], []);
|
Manga manga = new (sortName, authors, description, altTitles, tags, coverUrl, coverFileNameInCache, links,
|
||||||
|
year, originalLanguage, publicationId, releaseStatus, websiteUrl);
|
||||||
|
AddMangaToCache(manga);
|
||||||
|
return manga;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override Chapter[] GetChapters(Manga manga, string language="en")
|
public override Chapter[] GetChapters(Manga manga, string language="en")
|
||||||
{
|
{
|
||||||
string requestUrl = $"https://asuracomic.net/series/{manga.MangaId}";
|
Log($"Getting chapters {manga}");
|
||||||
|
string requestUrl = $"https://asuracomic.net/series/{manga.publicationId}";
|
||||||
// Leaving this in for verification if the page exists
|
// Leaving this in for verification if the page exists
|
||||||
RequestResult requestResult =
|
RequestResult requestResult =
|
||||||
downloadClient.MakeRequest(requestUrl, RequestType.Default);
|
downloadClient.MakeRequest(requestUrl, RequestType.Default);
|
||||||
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
|
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
|
||||||
return [];
|
return Array.Empty<Chapter>();
|
||||||
|
|
||||||
//Return Chapters ordered by Chapter-Number
|
//Return Chapters ordered by Chapter-Number
|
||||||
List<Chapter> chapters = ParseChaptersFromHtml(manga, requestUrl);
|
List<Chapter> chapters = ParseChaptersFromHtml(manga, requestUrl);
|
||||||
|
Log($"Got {chapters.Count} chapters. {manga}");
|
||||||
return chapters.Order().ToArray();
|
return chapters.Order().ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -140,6 +140,7 @@ public class AsuraToon : MangaConnector
|
|||||||
RequestResult result = downloadClient.MakeRequest(mangaUrl, RequestType.Default);
|
RequestResult result = downloadClient.MakeRequest(mangaUrl, RequestType.Default);
|
||||||
if ((int)result.statusCode < 200 || (int)result.statusCode >= 300 || result.htmlDocument is null)
|
if ((int)result.statusCode < 200 || (int)result.statusCode >= 300 || result.htmlDocument is null)
|
||||||
{
|
{
|
||||||
|
Log("Failed to load site");
|
||||||
return new List<Chapter>();
|
return new List<Chapter>();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -153,38 +154,63 @@ public class AsuraToon : MangaConnector
|
|||||||
string chapterUrl = chapterInfo.GetAttributeValue("href", "");
|
string chapterUrl = chapterInfo.GetAttributeValue("href", "");
|
||||||
|
|
||||||
Match match = infoRex.Match(chapterInfo.InnerText);
|
Match match = infoRex.Match(chapterInfo.InnerText);
|
||||||
string chapterNumber = new(match.Groups[1].Value);
|
string chapterNumber = match.Groups[1].Value;
|
||||||
string? chapterName = match.Groups[2].Success && match.Groups[2].Length > 1 ? match.Groups[2].Value : null;
|
string? chapterName = match.Groups[2].Success && match.Groups[2].Length > 1 ? match.Groups[2].Value : null;
|
||||||
string url = $"https://asuracomic.net/series/{chapterUrl}";
|
string url = $"https://asuracomic.net/series/{chapterUrl}";
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
ret.Add(new Chapter(manga, url, chapterNumber, null, chapterName));
|
ret.Add(new Chapter(manga, chapterName, null, chapterNumber, url));
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
|
Log($"Failed to load chapter {chapterNumber}: {e.Message}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
internal override string[] GetChapterImageUrls(Chapter chapter)
|
public override HttpStatusCode DownloadChapter(Chapter chapter, ProgressToken? progressToken = null)
|
||||||
{
|
{
|
||||||
string requestUrl = chapter.Url;
|
if (progressToken?.cancellationRequested ?? false)
|
||||||
|
{
|
||||||
|
progressToken.Cancel();
|
||||||
|
return HttpStatusCode.RequestTimeout;
|
||||||
|
}
|
||||||
|
|
||||||
|
Manga chapterParentManga = chapter.parentManga;
|
||||||
|
Log($"Retrieving chapter-info {chapter} {chapterParentManga}");
|
||||||
|
string requestUrl = chapter.url;
|
||||||
// Leaving this in to check if the page exists
|
// Leaving this in to check if the page exists
|
||||||
RequestResult requestResult =
|
RequestResult requestResult =
|
||||||
downloadClient.MakeRequest(requestUrl, RequestType.Default);
|
downloadClient.MakeRequest(requestUrl, RequestType.Default);
|
||||||
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300 || requestResult.htmlDocument is null)
|
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
|
||||||
{
|
{
|
||||||
return [];
|
progressToken?.Cancel();
|
||||||
|
return requestResult.statusCode;
|
||||||
}
|
}
|
||||||
string[] imageUrls = ParseImageUrlsFromHtml(requestResult.htmlDocument);
|
|
||||||
return imageUrls;
|
string[] imageUrls = ParseImageUrlsFromHtml(requestUrl);
|
||||||
|
|
||||||
|
return DownloadChapterImages(imageUrls, chapter, RequestType.MangaImage, progressToken:progressToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
private string[] ParseImageUrlsFromHtml(HtmlDocument document)
|
private string[] ParseImageUrlsFromHtml(string mangaUrl)
|
||||||
{
|
{
|
||||||
HtmlNodeCollection images = document.DocumentNode.SelectNodes("//img[contains(@alt, 'chapter page')]");
|
RequestResult requestResult =
|
||||||
|
downloadClient.MakeRequest(mangaUrl, RequestType.Default);
|
||||||
|
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
|
||||||
|
{
|
||||||
|
return Array.Empty<string>();
|
||||||
|
}
|
||||||
|
if (requestResult.htmlDocument is null)
|
||||||
|
{
|
||||||
|
Log($"Failed to retrieve site");
|
||||||
|
return Array.Empty<string>();
|
||||||
|
}
|
||||||
|
|
||||||
|
HtmlNodeCollection images =
|
||||||
|
requestResult.htmlDocument.DocumentNode.SelectNodes("//img[contains(@alt, 'chapter page')]");
|
||||||
|
|
||||||
return images.Select(i => i.GetAttributeValue("src", "")).ToArray();
|
return images.Select(i => i.GetAttributeValue("src", "")).ToArray();
|
||||||
}
|
}
|
@ -1,74 +1,78 @@
|
|||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using API.MangaDownloadClients;
|
|
||||||
using HtmlAgilityPack;
|
using HtmlAgilityPack;
|
||||||
|
using Tranga.Jobs;
|
||||||
|
|
||||||
namespace API.Schema.MangaConnectors;
|
namespace Tranga.MangaConnectors;
|
||||||
|
|
||||||
public class Bato : MangaConnector
|
public class Bato : MangaConnector
|
||||||
{
|
{
|
||||||
|
|
||||||
public Bato() : base("Bato", ["en"], ["bato.to"], "https://bato.to/amsta/img/batoto/favicon.ico")
|
public Bato(GlobalBase clone) : base(clone, "Bato", ["en"])
|
||||||
{
|
{
|
||||||
this.downloadClient = new HttpDownloadClient();
|
this.downloadClient = new HttpDownloadClient(clone);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)[] GetManga(string publicationTitle = "")
|
public override Manga[] GetManga(string publicationTitle = "")
|
||||||
{
|
{
|
||||||
|
Log($"Searching Publications. Term=\"{publicationTitle}\"");
|
||||||
string sanitizedTitle = string.Join(' ', Regex.Matches(publicationTitle, "[A-z]*").Where(m => m.Value.Length > 0)).ToLower();
|
string sanitizedTitle = string.Join(' ', Regex.Matches(publicationTitle, "[A-z]*").Where(m => m.Value.Length > 0)).ToLower();
|
||||||
string requestUrl = $"https://bato.to/v3x-search?word={sanitizedTitle}&lang=en";
|
string requestUrl = $"https://bato.to/v3x-search?word={sanitizedTitle}&lang=en";
|
||||||
RequestResult requestResult =
|
RequestResult requestResult =
|
||||||
downloadClient.MakeRequest(requestUrl, RequestType.Default);
|
downloadClient.MakeRequest(requestUrl, RequestType.Default);
|
||||||
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
|
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
|
||||||
return [];
|
return Array.Empty<Manga>();
|
||||||
|
|
||||||
if (requestResult.htmlDocument is null)
|
if (requestResult.htmlDocument is null)
|
||||||
{
|
{
|
||||||
return [];
|
Log($"Failed to retrieve site");
|
||||||
|
return Array.Empty<Manga>();
|
||||||
}
|
}
|
||||||
|
|
||||||
(Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)[] publications = ParsePublicationsFromHtml(requestResult.htmlDocument);
|
Manga[] publications = ParsePublicationsFromHtml(requestResult.htmlDocument);
|
||||||
|
Log($"Retrieved {publications.Length} publications. Term=\"{publicationTitle}\"");
|
||||||
return publications;
|
return publications;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)? GetMangaFromId(string publicationId)
|
public override Manga? GetMangaFromId(string publicationId)
|
||||||
{
|
{
|
||||||
return GetMangaFromUrl($"https://bato.to/title/{publicationId}");
|
return GetMangaFromUrl($"https://bato.to/title/{publicationId}");
|
||||||
}
|
}
|
||||||
|
|
||||||
public override (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)? GetMangaFromUrl(string url)
|
public override Manga? GetMangaFromUrl(string url)
|
||||||
{
|
{
|
||||||
RequestResult requestResult = downloadClient.MakeRequest(url, RequestType.MangaInfo);
|
RequestResult requestResult = downloadClient.MakeRequest(url, RequestType.MangaInfo);
|
||||||
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
|
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
|
||||||
return null;
|
return null;
|
||||||
if (requestResult.htmlDocument is null)
|
if (requestResult.htmlDocument is null)
|
||||||
{
|
{
|
||||||
|
Log($"Failed to retrieve site");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return ParseSinglePublicationFromHtml(requestResult.htmlDocument, url.Split('/')[^1], url);
|
return ParseSinglePublicationFromHtml(requestResult.htmlDocument, url.Split('/')[^1], url);
|
||||||
}
|
}
|
||||||
|
|
||||||
private (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)[] ParsePublicationsFromHtml(HtmlDocument document)
|
private Manga[] ParsePublicationsFromHtml(HtmlDocument document)
|
||||||
{
|
{
|
||||||
HtmlNode mangaList = document.DocumentNode.SelectSingleNode("//div[@data-hk='0-0-2']");
|
HtmlNode mangaList = document.DocumentNode.SelectSingleNode("//div[@data-hk='0-0-2']");
|
||||||
if (!mangaList.ChildNodes.Any(node => node.Name == "div"))
|
if (!mangaList.ChildNodes.Any(node => node.Name == "div"))
|
||||||
return [];
|
return Array.Empty<Manga>();
|
||||||
|
|
||||||
List<string> urls = mangaList.ChildNodes
|
List<string> urls = mangaList.ChildNodes
|
||||||
.Select(node => $"https://bato.to{node.Descendants("div").First().FirstChild.GetAttributeValue("href", "")}").ToList();
|
.Select(node => $"https://bato.to{node.Descendants("div").First().FirstChild.GetAttributeValue("href", "")}").ToList();
|
||||||
|
|
||||||
HashSet<(Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)> ret = new();
|
HashSet<Manga> ret = new();
|
||||||
foreach (string url in urls)
|
foreach (string url in urls)
|
||||||
{
|
{
|
||||||
(Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)? manga = GetMangaFromUrl(url);
|
Manga? manga = GetMangaFromUrl(url);
|
||||||
if (manga is { } x)
|
if (manga is not null)
|
||||||
ret.Add(x);
|
ret.Add((Manga)manga);
|
||||||
}
|
}
|
||||||
|
|
||||||
return ret.ToArray();
|
return ret.ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
private (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?) ParseSinglePublicationFromHtml(HtmlDocument document, string publicationId, string websiteUrl)
|
private Manga ParseSinglePublicationFromHtml(HtmlDocument document, string publicationId, string websiteUrl)
|
||||||
{
|
{
|
||||||
HtmlNode infoNode = document.DocumentNode.SelectSingleNode("/html/body/div/main/div[1]/div[2]");
|
HtmlNode infoNode = document.DocumentNode.SelectSingleNode("/html/body/div/main/div[1]/div[2]");
|
||||||
|
|
||||||
@ -78,61 +82,57 @@ public class Bato : MangaConnector
|
|||||||
|
|
||||||
string[] altTitlesList = infoNode.ChildNodes[1].ChildNodes[2].InnerText.Split('/');
|
string[] altTitlesList = infoNode.ChildNodes[1].ChildNodes[2].InnerText.Split('/');
|
||||||
int i = 0;
|
int i = 0;
|
||||||
List<MangaAltTitle> altTitles = altTitlesList.Select(a => new MangaAltTitle(i++.ToString(), a)).ToList();
|
Dictionary<string, string> altTitles = altTitlesList.ToDictionary(s => i++.ToString(), s => s);
|
||||||
|
|
||||||
string coverUrl = document.DocumentNode.SelectNodes("//img")
|
string posterUrl = document.DocumentNode.SelectNodes("//img")
|
||||||
.First(child => child.GetAttributeValue("data-hk", "") == "0-1-0").GetAttributeValue("src", "").Replace("&", "&");
|
.First(child => child.GetAttributeValue("data-hk", "") == "0-1-0").GetAttributeValue("src", "").Replace("&", "&");
|
||||||
|
string coverFileNameInCache = SaveCoverImageToCache(posterUrl, publicationId, RequestType.MangaCover);
|
||||||
|
|
||||||
List<HtmlNode> genreNodes = document.DocumentNode.SelectSingleNode("//b[text()='Genres:']/..").SelectNodes("span").ToList();
|
List<HtmlNode> genreNodes = document.DocumentNode.SelectSingleNode("//b[text()='Genres:']/..").SelectNodes("span").ToList();
|
||||||
string[] tags = genreNodes.Select(node => node.FirstChild.InnerText).ToArray();
|
string[] tags = genreNodes.Select(node => node.FirstChild.InnerText).ToArray();
|
||||||
List<MangaTag> mangaTags = tags.Select(s => new MangaTag(s)).ToList();
|
|
||||||
|
|
||||||
List<HtmlNode> authorsNodes = infoNode.ChildNodes[1].ChildNodes[3].Descendants("a").ToList();
|
List<HtmlNode> authorsNodes = infoNode.ChildNodes[1].ChildNodes[3].Descendants("a").ToList();
|
||||||
List<string> authorNames = authorsNodes.Select(node => node.InnerText.Replace("amp;", "")).ToList();
|
List<string> authors = authorsNodes.Select(node => node.InnerText.Replace("amp;", "")).ToList();
|
||||||
List<Author> authors = authorNames.Select(n => new Author(n)).ToList();
|
|
||||||
|
|
||||||
HtmlNode? originalLanguageNode = document.DocumentNode.SelectSingleNode("//span[text()='Tr From']/..");
|
HtmlNode? originalLanguageNode = document.DocumentNode.SelectSingleNode("//span[text()='Tr From']/..");
|
||||||
string originalLanguage = originalLanguageNode is not null ? originalLanguageNode.LastChild.InnerText : "";
|
string originalLanguage = originalLanguageNode is not null ? originalLanguageNode.LastChild.InnerText : "";
|
||||||
|
|
||||||
if (!uint.TryParse(
|
if (!int.TryParse(
|
||||||
document.DocumentNode.SelectSingleNode("//span[text()='Original Publication:']/..").LastChild.InnerText.Split('-')[0],
|
document.DocumentNode.SelectSingleNode("//span[text()='Original Publication:']/..").LastChild.InnerText.Split('-')[0],
|
||||||
out uint year))
|
out int year))
|
||||||
year = (uint)DateTime.UtcNow.Year;
|
year = DateTime.Now.Year;
|
||||||
|
|
||||||
string status = document.DocumentNode.SelectSingleNode("//span[text()='Original Publication:']/..")
|
string status = document.DocumentNode.SelectSingleNode("//span[text()='Original Publication:']/..")
|
||||||
.ChildNodes[2].InnerText;
|
.ChildNodes[2].InnerText;
|
||||||
MangaReleaseStatus releaseStatus = MangaReleaseStatus.Unreleased;
|
Manga.ReleaseStatusByte releaseStatus = Manga.ReleaseStatusByte.Unreleased;
|
||||||
switch (status.ToLower())
|
switch (status.ToLower())
|
||||||
{
|
{
|
||||||
case "ongoing": releaseStatus = MangaReleaseStatus.Continuing; break;
|
case "ongoing": releaseStatus = Manga.ReleaseStatusByte.Continuing; break;
|
||||||
case "completed": releaseStatus = MangaReleaseStatus.Completed; break;
|
case "completed": releaseStatus = Manga.ReleaseStatusByte.Completed; break;
|
||||||
case "hiatus": releaseStatus = MangaReleaseStatus.OnHiatus; break;
|
case "hiatus": releaseStatus = Manga.ReleaseStatusByte.OnHiatus; break;
|
||||||
case "cancelled": releaseStatus = MangaReleaseStatus.Cancelled; break;
|
case "cancelled": releaseStatus = Manga.ReleaseStatusByte.Cancelled; break;
|
||||||
case "pending": releaseStatus = MangaReleaseStatus.Unreleased; break;
|
case "pending": releaseStatus = Manga.ReleaseStatusByte.Unreleased; break;
|
||||||
}
|
}
|
||||||
|
|
||||||
Manga manga = new (publicationId, sortName, description, websiteUrl, coverUrl, null, year,
|
Manga manga = new (sortName, authors, description, altTitles, tags, posterUrl, coverFileNameInCache, new Dictionary<string, string>(),
|
||||||
originalLanguage, releaseStatus, -1,
|
year, originalLanguage, publicationId, releaseStatus, websiteUrl: websiteUrl);
|
||||||
this,
|
AddMangaToCache(manga);
|
||||||
authors,
|
return manga;
|
||||||
mangaTags,
|
|
||||||
[],
|
|
||||||
altTitles);
|
|
||||||
|
|
||||||
return (manga, authors, mangaTags, [], altTitles);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public override Chapter[] GetChapters(Manga manga, string language="en")
|
public override Chapter[] GetChapters(Manga manga, string language="en")
|
||||||
{
|
{
|
||||||
string requestUrl = $"https://bato.to/title/{manga.MangaId}";
|
Log($"Getting chapters {manga}");
|
||||||
|
string requestUrl = $"https://bato.to/title/{manga.publicationId}";
|
||||||
// Leaving this in for verification if the page exists
|
// Leaving this in for verification if the page exists
|
||||||
RequestResult requestResult =
|
RequestResult requestResult =
|
||||||
downloadClient.MakeRequest(requestUrl, RequestType.Default);
|
downloadClient.MakeRequest(requestUrl, RequestType.Default);
|
||||||
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
|
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
|
||||||
return [];
|
return Array.Empty<Chapter>();
|
||||||
|
|
||||||
//Return Chapters ordered by Chapter-Number
|
//Return Chapters ordered by Chapter-Number
|
||||||
List<Chapter> chapters = ParseChaptersFromHtml(manga, requestUrl);
|
List<Chapter> chapters = ParseChaptersFromHtml(manga, requestUrl);
|
||||||
|
Log($"Got {chapters.Count} chapters. {manga}");
|
||||||
return chapters.Order().ToArray();
|
return chapters.Order().ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -141,6 +141,7 @@ public class Bato : MangaConnector
|
|||||||
RequestResult result = downloadClient.MakeRequest(mangaUrl, RequestType.Default);
|
RequestResult result = downloadClient.MakeRequest(mangaUrl, RequestType.Default);
|
||||||
if ((int)result.statusCode < 200 || (int)result.statusCode >= 300 || result.htmlDocument is null)
|
if ((int)result.statusCode < 200 || (int)result.statusCode >= 300 || result.htmlDocument is null)
|
||||||
{
|
{
|
||||||
|
Log("Failed to load site");
|
||||||
return new List<Chapter>();
|
return new List<Chapter>();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -158,38 +159,64 @@ public class Bato : MangaConnector
|
|||||||
|
|
||||||
Match match = numberRex.Match(chapterUrl);
|
Match match = numberRex.Match(chapterUrl);
|
||||||
string id = match.Groups[1].Value;
|
string id = match.Groups[1].Value;
|
||||||
int? volumeNumber = match.Groups[2].Success ? int.Parse(match.Groups[2].Value) : null;
|
string? volumeNumber = match.Groups[2].Success ? match.Groups[2].Value : null;
|
||||||
string chapterNumber = new(match.Groups[3].Value);
|
string chapterNumber = match.Groups[3].Value;
|
||||||
|
string chapterName = chapterNumber;
|
||||||
string url = $"https://bato.to{chapterUrl}?load=2";
|
string url = $"https://bato.to{chapterUrl}?load=2";
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
ret.Add(new Chapter(manga, url, chapterNumber, volumeNumber, null));
|
ret.Add(new Chapter(manga, chapterName, volumeNumber, chapterNumber, url));
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
|
Log($"Failed to load chapter {chapterNumber}: {e.Message}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
internal override string[] GetChapterImageUrls(Chapter chapter)
|
public override HttpStatusCode DownloadChapter(Chapter chapter, ProgressToken? progressToken = null)
|
||||||
{
|
{
|
||||||
string requestUrl = chapter.Url;
|
if (progressToken?.cancellationRequested ?? false)
|
||||||
|
{
|
||||||
|
progressToken.Cancel();
|
||||||
|
return HttpStatusCode.RequestTimeout;
|
||||||
|
}
|
||||||
|
|
||||||
|
Manga chapterParentManga = chapter.parentManga;
|
||||||
|
Log($"Retrieving chapter-info {chapter} {chapterParentManga}");
|
||||||
|
string requestUrl = chapter.url;
|
||||||
// Leaving this in to check if the page exists
|
// Leaving this in to check if the page exists
|
||||||
RequestResult requestResult =
|
RequestResult requestResult =
|
||||||
downloadClient.MakeRequest(requestUrl, RequestType.Default);
|
downloadClient.MakeRequest(requestUrl, RequestType.Default);
|
||||||
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300 || requestResult.htmlDocument is null)
|
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
|
||||||
{
|
{
|
||||||
return [];
|
progressToken?.Cancel();
|
||||||
|
return requestResult.statusCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
string[] imageUrls = ParseImageUrlsFromHtml(requestResult.htmlDocument);
|
string[] imageUrls = ParseImageUrlsFromHtml(requestUrl);
|
||||||
return imageUrls;
|
|
||||||
|
return DownloadChapterImages(imageUrls, chapter, RequestType.MangaImage, progressToken:progressToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
private string[] ParseImageUrlsFromHtml(HtmlDocument document)
|
private string[] ParseImageUrlsFromHtml(string mangaUrl)
|
||||||
{
|
{
|
||||||
|
RequestResult requestResult =
|
||||||
|
downloadClient.MakeRequest(mangaUrl, RequestType.Default);
|
||||||
|
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
|
||||||
|
{
|
||||||
|
return Array.Empty<string>();
|
||||||
|
}
|
||||||
|
if (requestResult.htmlDocument is null)
|
||||||
|
{
|
||||||
|
Log($"Failed to retrieve site");
|
||||||
|
return Array.Empty<string>();
|
||||||
|
}
|
||||||
|
|
||||||
|
HtmlDocument document = requestResult.htmlDocument;
|
||||||
|
|
||||||
HtmlNode images = document.DocumentNode.SelectNodes("//astro-island").First(node =>
|
HtmlNode images = document.DocumentNode.SelectNodes("//astro-island").First(node =>
|
||||||
node.GetAttributeValue("component-url", "").Contains("/_astro/ImageList."));
|
node.GetAttributeValue("component-url", "").Contains("/_astro/ImageList."));
|
||||||
|
|
@ -2,19 +2,19 @@
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using HtmlAgilityPack;
|
using HtmlAgilityPack;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
using PuppeteerSharp;
|
using PuppeteerSharp;
|
||||||
|
|
||||||
namespace API.MangaDownloadClients;
|
namespace Tranga.MangaConnectors;
|
||||||
|
|
||||||
internal class ChromiumDownloadClient : DownloadClient
|
internal class ChromiumDownloadClient : DownloadClient
|
||||||
{
|
{
|
||||||
private static IBrowser? _browser;
|
private static IBrowser? _browser;
|
||||||
private readonly HttpDownloadClient _httpDownloadClient;
|
private readonly HttpDownloadClient _httpDownloadClient;
|
||||||
private readonly Thread _closeStalePagesThread;
|
|
||||||
private readonly List<KeyValuePair<IPage, DateTime>> _openPages = new ();
|
|
||||||
|
|
||||||
private static async Task<IBrowser> StartBrowser()
|
private static async Task<IBrowser> StartBrowser(Logging.Logger? logger = null)
|
||||||
{
|
{
|
||||||
|
logger?.WriteLine("Starting ChromiumDownloadClient Puppeteer");
|
||||||
return await Puppeteer.LaunchAsync(new LaunchOptions
|
return await Puppeteer.LaunchAsync(new LaunchOptions
|
||||||
{
|
{
|
||||||
Headless = true,
|
Headless = true,
|
||||||
@ -23,29 +23,40 @@ internal class ChromiumDownloadClient : DownloadClient
|
|||||||
"--disable-dev-shm-usage",
|
"--disable-dev-shm-usage",
|
||||||
"--disable-setuid-sandbox",
|
"--disable-setuid-sandbox",
|
||||||
"--no-sandbox"},
|
"--no-sandbox"},
|
||||||
Timeout = 30000
|
Timeout = TrangaSettings.ChromiumStartupTimeoutMs
|
||||||
});
|
}, new LoggerFactory([new LogProvider(logger)]));
|
||||||
}
|
}
|
||||||
|
|
||||||
public ChromiumDownloadClient()
|
private class LogProvider : GlobalBase, ILoggerProvider
|
||||||
{
|
{
|
||||||
_httpDownloadClient = new();
|
public LogProvider(Logging.Logger? logger) : base(logger) { }
|
||||||
if(_browser is null)
|
|
||||||
_browser = StartBrowser().Result;
|
public void Dispose() { }
|
||||||
_closeStalePagesThread = new Thread(CheckStalePages);
|
|
||||||
_closeStalePagesThread.Start();
|
public ILogger CreateLogger(string categoryName) => new Logger(logger);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void CheckStalePages()
|
private class Logger : GlobalBase, ILogger
|
||||||
{
|
{
|
||||||
while (true)
|
public Logger(Logging.Logger? logger) : base(logger) { }
|
||||||
|
|
||||||
|
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
|
||||||
{
|
{
|
||||||
Thread.Sleep(TimeSpan.FromHours(1));
|
if (logLevel <= LogLevel.Information)
|
||||||
foreach ((IPage? key, DateTime value) in _openPages.Where(kv => kv.Value.Subtract(DateTime.Now) > TimeSpan.FromHours(1)))
|
return;
|
||||||
{
|
logger?.WriteLine("Puppeteer", formatter.Invoke(state, exception));
|
||||||
key.CloseAsync().Wait();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool IsEnabled(LogLevel logLevel) => true;
|
||||||
|
|
||||||
|
public IDisposable? BeginScope<TState>(TState state) where TState : notnull => null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ChromiumDownloadClient(GlobalBase clone) : base(clone)
|
||||||
|
{
|
||||||
|
_httpDownloadClient = new(this);
|
||||||
|
if(_browser is null)
|
||||||
|
_browser = StartBrowser(this.logger).Result;
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly Regex _imageUrlRex = new(@"https?:\/\/.*\.(?:p?jpe?g|gif|a?png|bmp|avif|webp)(\?.*)?");
|
private readonly Regex _imageUrlRex = new(@"https?:\/\/.*\.(?:p?jpe?g|gif|a?png|bmp|avif|webp)(\?.*)?");
|
||||||
@ -61,20 +72,18 @@ internal class ChromiumDownloadClient : DownloadClient
|
|||||||
if (_browser is null)
|
if (_browser is null)
|
||||||
return new RequestResult(HttpStatusCode.InternalServerError, null, Stream.Null);
|
return new RequestResult(HttpStatusCode.InternalServerError, null, Stream.Null);
|
||||||
IPage page = _browser.NewPageAsync().Result;
|
IPage page = _browser.NewPageAsync().Result;
|
||||||
_openPages.Add(new(page, DateTime.Now));
|
page.DefaultTimeout = TrangaSettings.ChromiumPageTimeoutMs;
|
||||||
page.SetExtraHttpHeadersAsync(new() { { "Referer", referrer } });
|
page.SetExtraHttpHeadersAsync(new() { { "Referer", referrer } });
|
||||||
page.DefaultTimeout = 30000;
|
|
||||||
IResponse response;
|
IResponse response;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
response = page.GoToAsync(url, WaitUntilNavigation.Networkidle0).Result;
|
response = page.GoToAsync(url, WaitUntilNavigation.Networkidle0).Result;
|
||||||
//Log($"Page loaded. {url}");
|
Log($"Page loaded. {url}");
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
//Log($"Could not load Page {url}\n{e.Message}");
|
Log($"Could not load Page {url}\n{e.Message}");
|
||||||
page.CloseAsync();
|
page.CloseAsync();
|
||||||
_openPages.Remove(_openPages.Find(i => i.Key == page));
|
|
||||||
return new RequestResult(HttpStatusCode.InternalServerError, null, Stream.Null);
|
return new RequestResult(HttpStatusCode.InternalServerError, null, Stream.Null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -98,13 +107,11 @@ internal class ChromiumDownloadClient : DownloadClient
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
page.CloseAsync().Wait();
|
page.CloseAsync();
|
||||||
_openPages.Remove(_openPages.Find(i => i.Key == page));
|
|
||||||
return new RequestResult(HttpStatusCode.InternalServerError, null, Stream.Null);
|
return new RequestResult(HttpStatusCode.InternalServerError, null, Stream.Null);
|
||||||
}
|
}
|
||||||
|
|
||||||
page.CloseAsync().Wait();
|
page.CloseAsync();
|
||||||
_openPages.Remove(_openPages.Find(i => i.Key == page));
|
|
||||||
return new RequestResult(response.Status, document, stream, false, "");
|
return new RequestResult(response.Status, document, stream, false, "");
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,16 +1,14 @@
|
|||||||
using System.Net;
|
using System.Net;
|
||||||
using log4net;
|
using HtmlAgilityPack;
|
||||||
|
|
||||||
namespace API.MangaDownloadClients;
|
namespace Tranga.MangaConnectors;
|
||||||
|
|
||||||
internal abstract class DownloadClient
|
internal abstract class DownloadClient : GlobalBase
|
||||||
{
|
{
|
||||||
private readonly Dictionary<RequestType, DateTime> _lastExecutedRateLimit;
|
private readonly Dictionary<RequestType, DateTime> _lastExecutedRateLimit;
|
||||||
protected ILog Log { get; init; }
|
|
||||||
|
|
||||||
protected DownloadClient()
|
protected DownloadClient(GlobalBase clone) : base(clone)
|
||||||
{
|
{
|
||||||
this.Log = LogManager.GetLogger(GetType());
|
|
||||||
this._lastExecutedRateLimit = new();
|
this._lastExecutedRateLimit = new();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -18,6 +16,7 @@ internal abstract class DownloadClient
|
|||||||
{
|
{
|
||||||
if (!TrangaSettings.requestLimits.ContainsKey(requestType))
|
if (!TrangaSettings.requestLimits.ContainsKey(requestType))
|
||||||
{
|
{
|
||||||
|
Log("RequestType not configured for rate-limit.");
|
||||||
return new RequestResult(HttpStatusCode.NotAcceptable, null, Stream.Null);
|
return new RequestResult(HttpStatusCode.NotAcceptable, null, Stream.Null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -26,17 +25,18 @@ internal abstract class DownloadClient
|
|||||||
: TrangaSettings.requestLimits[requestType];
|
: TrangaSettings.requestLimits[requestType];
|
||||||
|
|
||||||
TimeSpan timeBetweenRequests = TimeSpan.FromMinutes(1).Divide(rateLimit);
|
TimeSpan timeBetweenRequests = TimeSpan.FromMinutes(1).Divide(rateLimit);
|
||||||
_lastExecutedRateLimit.TryAdd(requestType, DateTime.UtcNow.Subtract(timeBetweenRequests));
|
_lastExecutedRateLimit.TryAdd(requestType, DateTime.Now.Subtract(timeBetweenRequests));
|
||||||
|
|
||||||
TimeSpan rateLimitTimeout = timeBetweenRequests.Subtract(DateTime.UtcNow.Subtract(_lastExecutedRateLimit[requestType]));
|
TimeSpan rateLimitTimeout = timeBetweenRequests.Subtract(DateTime.Now.Subtract(_lastExecutedRateLimit[requestType]));
|
||||||
|
|
||||||
if (rateLimitTimeout > TimeSpan.Zero)
|
if (rateLimitTimeout > TimeSpan.Zero)
|
||||||
{
|
{
|
||||||
|
Log($"Waiting {rateLimitTimeout.TotalSeconds} seconds");
|
||||||
Thread.Sleep(rateLimitTimeout);
|
Thread.Sleep(rateLimitTimeout);
|
||||||
}
|
}
|
||||||
|
|
||||||
RequestResult result = MakeRequestInternal(url, referrer, clickButton);
|
RequestResult result = MakeRequestInternal(url, referrer, clickButton);
|
||||||
_lastExecutedRateLimit[requestType] = DateTime.UtcNow;
|
_lastExecutedRateLimit[requestType] = DateTime.Now;
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user