mirror of
https://github.com/C9Glax/tranga.git
synced 2025-09-10 11:58:19 +02:00
Compare commits
64 Commits
testing
...
c5689557b3
Author | SHA1 | Date | |
---|---|---|---|
c5689557b3 | |||
c044532564 | |||
383258ac87 | |||
de36ce9c16 | |||
eba79abf51 | |||
ae20ad47a8 | |||
691506b2d8 | |||
356a22d72e | |||
5d8f203a35 | |||
17995d1603 | |||
d2f9ab64aa | |||
d79dd8c3d5 | |||
2b527e15b0 | |||
305b9d900c | |||
413fb0e69e | |||
64b89d4537 | |||
fab70501ce | |||
0a1021b488 | |||
18c6f64405 | |||
5202bfe782 | |||
8408407a8e | |||
a091250195 | |||
a352495866 | |||
3a46d0fd24 | |||
6034937c23 | |||
454f468fd4 | |||
1ee442ea2e | |||
6b4317834d | |||
88fef8417c | |||
eb9fc08b2d | |||
9743bb6e8e | |||
e8d612557f | |||
cf2dbeaf6a | |||
84940c414c | |||
ea627081b8 | |||
a90a6fb200 | |||
c3a0bb03e9 | |||
f8ccd2d69e | |||
ad224190a2 | |||
f05f2cc8e0 | |||
d6f0630a99 | |||
0ac4c23ac9 | |||
d6847d769e | |||
f6f5e21151 | |||
da3b5078af | |||
681d56710a | |||
6f5823596a | |||
8a06ed648c | |||
4dcd6ee035 | |||
e327e93163 | |||
6cd836540a | |||
91c91e4989 | |||
57bb87120a | |||
07880fedb5 | |||
f1d3203ae1 | |||
314700aa8e | |||
b299cc9109 | |||
7e9ba7090a | |||
ea73d03b8f | |||
e9d9bebcd7 | |||
e5937d2654 | |||
7c9e0eddf9 | |||
ae0c6c8240 | |||
2eb0d941b2 |
4
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
4
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -1,13 +1,13 @@
|
||||
name: Bug Report
|
||||
description: File a bug report
|
||||
title: "[Tranga broke]: <title>"
|
||||
title: "[It broke]: "
|
||||
labels: ["bug"]
|
||||
body:
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: What is broken?
|
||||
description: What happened? How did we get here?
|
||||
placeholder: Tell me what you expected to happen, and what happened instead.
|
||||
placeholder: The place where you tell me what you expected to happen, and what happened instead.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
|
13
.github/ISSUE_TEMPLATE/new_connector.yml
vendored
13
.github/ISSUE_TEMPLATE/new_connector.yml
vendored
@@ -1,6 +1,6 @@
|
||||
name: New Connector Request
|
||||
description: Request a new site to be added
|
||||
title: "[New Connector]: <title>"
|
||||
title: "[New Connector]: "
|
||||
labels: ["New Connector"]
|
||||
body:
|
||||
- type: input
|
||||
@@ -9,12 +9,15 @@ body:
|
||||
placeholder: https://
|
||||
validations:
|
||||
required: true
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Is the Website free to access?
|
||||
description: We can't support pay-to-use sites, or captcha-proxied sites as Cloudflare.
|
||||
options:
|
||||
- label: The Website is freely accessible.
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Anything else?
|
||||
validations:
|
||||
required: false
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
If you want implement this read [Contributing](https://github.com/C9Glax/tranga#contributing). Thank you!
|
31
.github/PULL_REQUEST_TEMPLATE/new_connector.yml
vendored
31
.github/PULL_REQUEST_TEMPLATE/new_connector.yml
vendored
@@ -1,31 +0,0 @@
|
||||
name: New Connector Implementation
|
||||
description: New Connector
|
||||
title: "<title>"
|
||||
labels: ["New Connector"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thank you for contributing. Please make sure you have read [Contributing](https://github.com/C9Glax/tranga#contributing).
|
||||
Just for the sake of completion:
|
||||
- type: checkboxes
|
||||
id: Contributing
|
||||
attributes:
|
||||
label: Contributing
|
||||
description: I have checked
|
||||
options:
|
||||
- label: Formatting (if not done yet, mark this PR as draft)
|
||||
required: false
|
||||
- label: I have read https://github.com/C9Glax/tranga#if-you-want-to-add-a-new-website-connector
|
||||
required: true
|
||||
- type: input
|
||||
attributes:
|
||||
label: Existing Issue
|
||||
placeholder: #<Issue number>
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Anything else?
|
||||
validations:
|
||||
required: false
|
10
.github/workflows/docker-image-cuttingedge.yml
vendored
10
.github/workflows/docker-image-cuttingedge.yml
vendored
@@ -13,7 +13,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# https://github.com/docker/setup-qemu-action#usage
|
||||
- name: Set up QEMU
|
||||
@@ -22,7 +22,7 @@ jobs:
|
||||
# https://github.com/marketplace/actions/docker-setup-buildx
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v3.11.1
|
||||
uses: docker/setup-buildx-action@v3.10.0
|
||||
|
||||
# https://github.com/docker/login-action#docker-hub
|
||||
- name: Login to Docker Hub
|
||||
@@ -33,12 +33,12 @@ jobs:
|
||||
|
||||
# https://github.com/docker/build-push-action#multi-platform-image
|
||||
- name: Build and push API
|
||||
uses: docker/build-push-action@v6.18.0
|
||||
uses: docker/build-push-action@v6.15.0
|
||||
with:
|
||||
context: ./
|
||||
file: ./Dockerfile
|
||||
#platforms: linux/amd64,linux/amd64/v2,linux/amd64/v3,linux/arm64,linux/arm/v7
|
||||
platforms: linux/amd64,linux/amd64/v2,linux/amd64/v3,linux/arm64,linux/arm/v7
|
||||
#platforms: linux/amd64,linux/arm64,linux/riscv64,linux/ppc64le,linux/s390x,linux/386,linux/mips64le,linux/mips64,linux/arm/v7,linux/arm/v6
|
||||
platforms: linux/amd64,linux/arm64
|
||||
pull: true
|
||||
push: true
|
||||
tags: |
|
||||
|
6
.github/workflows/docker-image-master.yml
vendored
6
.github/workflows/docker-image-master.yml
vendored
@@ -13,7 +13,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# https://github.com/docker/setup-qemu-action#usage
|
||||
- name: Set up QEMU
|
||||
@@ -22,7 +22,7 @@ jobs:
|
||||
# https://github.com/marketplace/actions/docker-setup-buildx
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v3.11.1
|
||||
uses: docker/setup-buildx-action@v3.10.0
|
||||
|
||||
# https://github.com/docker/login-action#docker-hub
|
||||
- name: Login to Docker Hub
|
||||
@@ -33,7 +33,7 @@ jobs:
|
||||
|
||||
# https://github.com/docker/build-push-action#multi-platform-image
|
||||
- name: Build and push API
|
||||
uses: docker/build-push-action@v6.18.0
|
||||
uses: docker/build-push-action@v6.15.0
|
||||
with:
|
||||
context: ./
|
||||
file: ./Dockerfile
|
||||
|
@@ -2,7 +2,7 @@ name: Docker Image CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "testing" ]
|
||||
branches: [ "postgres-Server-V2" ]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
@@ -13,7 +13,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# https://github.com/docker/setup-qemu-action#usage
|
||||
- name: Set up QEMU
|
||||
@@ -22,7 +22,7 @@ jobs:
|
||||
# https://github.com/marketplace/actions/docker-setup-buildx
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v3.11.1
|
||||
uses: docker/setup-buildx-action@v3.10.0
|
||||
|
||||
# https://github.com/docker/login-action#docker-hub
|
||||
- name: Login to Docker Hub
|
||||
@@ -33,13 +33,13 @@ jobs:
|
||||
|
||||
# https://github.com/docker/build-push-action#multi-platform-image
|
||||
- name: Build and push API
|
||||
uses: docker/build-push-action@v6.18.0
|
||||
uses: docker/build-push-action@v6.15.0
|
||||
with:
|
||||
context: ./
|
||||
file: ./Dockerfile
|
||||
#platforms: linux/amd64,linux/amd64/v2,linux/amd64/v3,linux/arm64,linux/arm/v7
|
||||
platforms: linux/amd64,linux/amd64/v2,linux/amd64/v3,linux/arm64,linux/arm/v7
|
||||
#platforms: linux/amd64,linux/arm64,linux/riscv64,linux/ppc64le,linux/s390x,linux/386,linux/mips64le,linux/mips64,linux/arm/v7,linux/arm/v6
|
||||
platforms: linux/amd64,linux/arm64
|
||||
pull: true
|
||||
push: true
|
||||
tags: |
|
||||
glax/tranga-api:testing
|
||||
glax/tranga-api:Server-V2
|
@@ -11,32 +11,30 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Asp.Versioning.Mvc.ApiExplorer" Version="8.1.0" />
|
||||
<PackageReference Include="HtmlAgilityPack" Version="1.12.2" />
|
||||
<PackageReference Include="HtmlAgilityPack" Version="1.12.0" />
|
||||
<PackageReference Include="JikanDotNet" Version="2.9.1" />
|
||||
<PackageReference Include="log4net" Version="3.2.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="9.0.8" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.8" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.8">
|
||||
<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="Microsoft.EntityFrameworkCore.Proxies" Version="9.0.5" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageReference Include="Npgsql" Version="9.0.3" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.Design" Version="1.1.0" />
|
||||
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.11" />
|
||||
<PackageReference Include="Soenneker.Utils.String.NeedlemanWunsch" Version="3.0.978" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.4" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore.Newtonsoft" Version="9.0.4" />
|
||||
<PackageReference Include="System.Drawing.Common" Version="9.0.8" />
|
||||
<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\Manga\" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="Log4Net.config.xml" CopyToOutputDirectory="Always" CopyToPublishDirectory="Always" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
@@ -0,0 +1,5 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace API.APIEndpointRecords;
|
||||
|
||||
public record DownloadAvailableChaptersJobRecord([Required]string language, [Required]ulong recurrenceTimeMs, [Required]string localLibraryId);
|
16
API/APIEndpointRecords/GotifyRecord.cs
Normal file
16
API/APIEndpointRecords/GotifyRecord.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
namespace API.APIEndpointRecords;
|
||||
|
||||
public record GotifyRecord(string Name, 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;
|
||||
}
|
||||
}
|
3
API/APIEndpointRecords/ModifyWorkerRecord.cs
Normal file
3
API/APIEndpointRecords/ModifyWorkerRecord.cs
Normal file
@@ -0,0 +1,3 @@
|
||||
namespace API.APIEndpointRecords;
|
||||
|
||||
public record ModifyWorkerRecord(ulong? IntervalMs);
|
17
API/APIEndpointRecords/NtfyRecord.cs
Normal file
17
API/APIEndpointRecords/NtfyRecord.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
namespace API.APIEndpointRecords;
|
||||
|
||||
public record NtfyRecord(string Name, 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;
|
||||
}
|
||||
}
|
13
API/APIEndpointRecords/PushoverRecord.cs
Normal file
13
API/APIEndpointRecords/PushoverRecord.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
namespace API.APIEndpointRecords;
|
||||
|
||||
public record PushoverRecord(string Name, string AppToken, string User)
|
||||
{
|
||||
public bool Validate()
|
||||
{
|
||||
if (AppToken == string.Empty)
|
||||
return false;
|
||||
if (User == string.Empty)
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
}
|
@@ -1,18 +0,0 @@
|
||||
using SixLabors.ImageSharp;
|
||||
|
||||
namespace API;
|
||||
|
||||
public struct Constants
|
||||
{
|
||||
public const string TRANGA =
|
||||
"\n\n" +
|
||||
" _______ v2\n" +
|
||||
"|_ _|.----..---.-..-----..-----..---.-.\n" +
|
||||
" | | | _|| _ || || _ || _ |\n" +
|
||||
" |___| |__| |___._||__|__||___ ||___._|\n" +
|
||||
" |_____| \n\n";
|
||||
|
||||
public static readonly Size ImageSmSize = new (225, 320);
|
||||
public static readonly Size ImageMdSize = new (450, 640);
|
||||
public static readonly Size ImageLgSize = new (900, 1280);
|
||||
}
|
@@ -1,24 +0,0 @@
|
||||
using System.ComponentModel;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace API.Controllers.DTOs;
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="API.Schema.MangaContext.AltTitle"/> DTO
|
||||
/// </summary>
|
||||
public sealed record AltTitle(string Language, string Title)
|
||||
{
|
||||
/// <summary>
|
||||
/// Language of the Title
|
||||
/// </summary>
|
||||
[Required]
|
||||
[Description("Language of the Title")]
|
||||
public string Language { get; init; } = Language;
|
||||
|
||||
/// <summary>
|
||||
/// Title
|
||||
/// </summary>
|
||||
[Required]
|
||||
[Description("Title")]
|
||||
public string Title { get; init; } = Title;
|
||||
}
|
@@ -1,17 +0,0 @@
|
||||
using System.ComponentModel;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace API.Controllers.DTOs;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="API.Schema.MangaContext.Author"/> DTO
|
||||
/// </summary>
|
||||
public sealed record Author(string Key, string Name) : Identifiable(Key)
|
||||
{
|
||||
/// <summary>
|
||||
/// Name of the Author.
|
||||
/// </summary>
|
||||
[Required]
|
||||
[Description("Name of the Author.")]
|
||||
public string Name { get; init; } = Name;
|
||||
}
|
@@ -1,52 +0,0 @@
|
||||
using System.ComponentModel;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace API.Controllers.DTOs;
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="API.Schema.MangaContext.Chapter"/> DTO
|
||||
/// </summary>
|
||||
public sealed record Chapter(string Key, string MangaId, int? Volume, string ChapterNumber, string? Title, IEnumerable<MangaConnectorId> MangaConnectorIds, bool Downloaded) : Identifiable(Key)
|
||||
{
|
||||
/// <summary>
|
||||
/// Identifier of the Manga this Chapter belongs to
|
||||
/// </summary>
|
||||
[Required]
|
||||
[Description("Identifier of the Manga this Chapter belongs to")]
|
||||
public string MangaId { get; init; } = MangaId;
|
||||
|
||||
/// <summary>
|
||||
/// Volume number
|
||||
/// </summary>
|
||||
[Required]
|
||||
[Description("Volume number")]
|
||||
public int? Volume { get; init; } = Volume;
|
||||
|
||||
/// <summary>
|
||||
/// Chapter number
|
||||
/// </summary>
|
||||
[Required]
|
||||
[Description("Chapter number")]
|
||||
public string ChapterNumber { get; init; } = ChapterNumber;
|
||||
|
||||
/// <summary>
|
||||
/// Title of the Chapter
|
||||
/// </summary>
|
||||
[Required]
|
||||
[Description("Title of the Chapter")]
|
||||
public string? Title { get; init; } = Title;
|
||||
|
||||
/// <summary>
|
||||
/// Whether Chapter is Downloaded (on disk)
|
||||
/// </summary>
|
||||
[Required]
|
||||
[Description("Whether Chapter is Downloaded (on disk)")]
|
||||
public bool Downloaded { get; init; } = Downloaded;
|
||||
|
||||
/// <summary>
|
||||
/// Ids of the Manga on MangaConnectors
|
||||
/// </summary>
|
||||
[Required]
|
||||
[Description("Ids of the Manga on MangaConnectors")]
|
||||
public IEnumerable<MangaConnectorId> MangaConnectorIds { get; init; } = MangaConnectorIds;
|
||||
}
|
@@ -1,21 +0,0 @@
|
||||
using System.ComponentModel;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace API.Controllers.DTOs;
|
||||
|
||||
public sealed record FileLibrary(string Key, string BasePath, string LibraryName) : Identifiable(Key)
|
||||
{
|
||||
/// <summary>
|
||||
/// The directory Path of the library
|
||||
/// </summary>
|
||||
[Required]
|
||||
[Description("The directory Path of the library")]
|
||||
public string BasePath { get; internal set; } = BasePath;
|
||||
|
||||
/// <summary>
|
||||
/// The Name of the library
|
||||
/// </summary>
|
||||
[Required]
|
||||
[Description("The Name of the library")]
|
||||
public string LibraryName { get; internal set; } = LibraryName;
|
||||
}
|
@@ -1,18 +0,0 @@
|
||||
using System.ComponentModel;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace API.Controllers.DTOs;
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="API.Schema.Identifiable"/>
|
||||
/// </summary>
|
||||
public record Identifiable(string Key)
|
||||
{
|
||||
/// <summary>
|
||||
/// Unique Identifier of the DTO
|
||||
/// </summary>
|
||||
[Required]
|
||||
[Description("Unique Identifier of the DTO")]
|
||||
[StringLength(TokenGen.MaximumLength, MinimumLength = TokenGen.MinimumLength)]
|
||||
public string Key { get; init; } = Key;
|
||||
}
|
@@ -1,23 +0,0 @@
|
||||
using System.ComponentModel;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using API.Schema.LibraryContext.LibraryConnectors;
|
||||
|
||||
namespace API.Controllers.DTOs;
|
||||
|
||||
public record LibraryConnector(string Key, string BaseUrl, LibraryType Type) : Identifiable(Key)
|
||||
{
|
||||
/// <summary>
|
||||
/// The Url of the Library instance
|
||||
/// </summary>
|
||||
[Required]
|
||||
[Url]
|
||||
[Description("The Url of the Library instance")]
|
||||
public string BaseUrl { get; init;} = BaseUrl;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="LibraryType"/>
|
||||
/// </summary>
|
||||
[Required]
|
||||
[Description("The Library Type")]
|
||||
public LibraryType Type { get; init; } = Type;
|
||||
}
|
@@ -1,25 +0,0 @@
|
||||
using System.ComponentModel;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace API.Controllers.DTOs;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="API.Schema.MangaContext.Link"/> DTO
|
||||
/// </summary>
|
||||
public sealed record Link(string Key, string Provider, string Url) : Identifiable(Key)
|
||||
{
|
||||
/// <summary>
|
||||
/// Name of the Provider
|
||||
/// </summary>
|
||||
[Required]
|
||||
[Description("Name of the Provider")]
|
||||
public string Provider { get; init; } = Provider;
|
||||
|
||||
/// <summary>
|
||||
/// Url
|
||||
/// </summary>
|
||||
[Required]
|
||||
[Description("Url")]
|
||||
public string Url { get; init; } = Url;
|
||||
}
|
@@ -1,73 +0,0 @@
|
||||
using System.ComponentModel;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using API.Schema.MangaContext;
|
||||
|
||||
namespace API.Controllers.DTOs;
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="Schema.MangaContext.Manga"/> DTO
|
||||
/// </summary>
|
||||
public sealed record Manga(string Key, string Name, string Description, MangaReleaseStatus ReleaseStatus, IEnumerable<MangaConnectorId> MangaConnectorIds, float IgnoreChaptersBefore, uint? Year, string? OriginalLanguage, IEnumerable<string> ChapterIds, IEnumerable<Author> Authors, IEnumerable<string> Tags, IEnumerable<Link> Links, IEnumerable<AltTitle> AltTitles, string? FileLibraryId)
|
||||
: MinimalManga(Key, Name, Description, ReleaseStatus, MangaConnectorIds)
|
||||
{
|
||||
/// <summary>
|
||||
/// Chapter cutoff for Downloads (Chapters before this will not be downloaded)
|
||||
/// </summary>
|
||||
[Required]
|
||||
[Description("Chapter cutoff for Downloads (Chapters before this will not be downloaded)")]
|
||||
public float IgnoreChaptersBefore { get; init; } = IgnoreChaptersBefore;
|
||||
|
||||
/// <summary>
|
||||
/// Release Year
|
||||
/// </summary>
|
||||
[Description("Release Year")]
|
||||
public uint? Year { get; init; } = Year;
|
||||
|
||||
/// <summary>
|
||||
/// Release Language
|
||||
/// </summary>
|
||||
[Description("Release Language")]
|
||||
public string? OriginalLanguage { get; init; } = OriginalLanguage;
|
||||
|
||||
/// <summary>
|
||||
/// Keys of ChapterDTOs
|
||||
/// </summary>
|
||||
[Required]
|
||||
[Description("Keys of ChapterDTOs")]
|
||||
public IEnumerable<string> ChapterIds { get; init; } = ChapterIds;
|
||||
|
||||
/// <summary>
|
||||
/// Author-names
|
||||
/// </summary>
|
||||
[Required]
|
||||
[Description("Author-names")]
|
||||
public IEnumerable<Author> Authors { get; init; } = Authors;
|
||||
|
||||
/// <summary>
|
||||
/// Manga Tags
|
||||
/// </summary>
|
||||
[Required]
|
||||
[Description("Manga Tags")]
|
||||
public IEnumerable<string> Tags { get; init; } = Tags;
|
||||
|
||||
/// <summary>
|
||||
/// Links for more Metadata
|
||||
/// </summary>
|
||||
[Required]
|
||||
[Description("Links for more Metadata")]
|
||||
public IEnumerable<Link> Links { get; init; } = Links;
|
||||
|
||||
/// <summary>
|
||||
/// Alt Titles of Manga
|
||||
/// </summary>
|
||||
[Required]
|
||||
[Description("Alt Titles of Manga")]
|
||||
public IEnumerable<AltTitle> AltTitles { get; init; } = AltTitles;
|
||||
|
||||
/// <summary>
|
||||
/// Id of the Library the Manga gets downloaded to
|
||||
/// </summary>
|
||||
[Required]
|
||||
[Description("Id of the Library the Manga gets downloaded to")]
|
||||
public string? FileLibraryId { get; init; } = FileLibraryId;
|
||||
}
|
@@ -1,28 +0,0 @@
|
||||
using System.ComponentModel;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace API.Controllers.DTOs;
|
||||
|
||||
public sealed record MangaConnector(string Name, bool Enabled, string IconUrl, string[] SupportedLanguages) : Identifiable(Name)
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether Connector is used for Searches and Downloads
|
||||
/// </summary>
|
||||
[Required]
|
||||
[Description("Whether Connector is used for Searches and Downloads")]
|
||||
public bool Enabled { get; init; } = Enabled;
|
||||
|
||||
/// <summary>
|
||||
/// Languages supported by the Connector
|
||||
/// </summary>
|
||||
[Required]
|
||||
[Description("Languages supported by the Connector")]
|
||||
public string[] SupportedLanguages { get; init; } = SupportedLanguages;
|
||||
|
||||
/// <summary>
|
||||
/// Url of the Website Icon
|
||||
/// </summary>
|
||||
[Required]
|
||||
[Description("Url of the Website Icon")]
|
||||
public string IconUrl { get; init; } = IconUrl;
|
||||
}
|
@@ -1,38 +0,0 @@
|
||||
using System.ComponentModel;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using API.Schema.MangaContext;
|
||||
|
||||
namespace API.Controllers.DTOs;
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="MangaConnectorId{T}"/> DTO
|
||||
/// </summary>
|
||||
public sealed record MangaConnectorId(string Key, string MangaConnectorName, string ForeignKey, string? WebsiteUrl, bool UseForDownload) : Identifiable(Key)
|
||||
{
|
||||
/// <summary>
|
||||
/// Name of the Connector
|
||||
/// </summary>
|
||||
[Required]
|
||||
[Description("Name of the Connector")]
|
||||
public string MangaConnectorName { get; init; } = MangaConnectorName;
|
||||
|
||||
/// <summary>
|
||||
/// Key of the referenced DTO
|
||||
/// </summary>
|
||||
[Required]
|
||||
[Description("Key of the referenced DTO")]
|
||||
public string ForeignKey { get; init; } = ForeignKey;
|
||||
|
||||
/// <summary>
|
||||
/// Website Link for reference, if any
|
||||
/// </summary>
|
||||
[Description("Website Link for reference, if any")]
|
||||
public string? WebsiteUrl { get; init; } = WebsiteUrl;
|
||||
|
||||
/// <summary>
|
||||
/// Whether this Link is used for downloads
|
||||
/// </summary>
|
||||
[Required]
|
||||
[Description("Whether this Link is used for downloads")]
|
||||
public bool UseForDownload { get; init; } = UseForDownload;
|
||||
}
|
@@ -1,39 +0,0 @@
|
||||
using System.ComponentModel;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using API.Schema.MangaContext;
|
||||
|
||||
namespace API.Controllers.DTOs;
|
||||
|
||||
/// <summary>
|
||||
/// Shortened Version of <see cref="Manga"/>
|
||||
/// </summary>
|
||||
public record MinimalManga(string Key, string Name, string Description, MangaReleaseStatus ReleaseStatus, IEnumerable<MangaConnectorId> MangaConnectorIds) : Identifiable(Key)
|
||||
{
|
||||
/// <summary>
|
||||
/// Name of the Manga
|
||||
/// </summary>
|
||||
[Required]
|
||||
[Description("Name of the Manga")]
|
||||
public string Name { get; init; } = Name;
|
||||
|
||||
/// <summary>
|
||||
/// Description of the Manga
|
||||
/// </summary>
|
||||
[Required]
|
||||
[Description("Description of the Manga")]
|
||||
public string Description { get; init; } = Description;
|
||||
|
||||
/// <summary>
|
||||
/// ReleaseStatus of the Manga
|
||||
/// </summary>
|
||||
[Required]
|
||||
[Description("ReleaseStatus of the Manga")]
|
||||
public MangaReleaseStatus ReleaseStatus { get; init; } = ReleaseStatus;
|
||||
|
||||
/// <summary>
|
||||
/// Ids of the Manga on MangaConnectors
|
||||
/// </summary>
|
||||
[Required]
|
||||
[Description("Ids of the Manga on MangaConnectors")]
|
||||
public IEnumerable<MangaConnectorId> MangaConnectorIds { get; init; } = MangaConnectorIds;
|
||||
}
|
@@ -1,46 +0,0 @@
|
||||
using System.ComponentModel;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace API.Controllers.DTOs;
|
||||
|
||||
public record NotificationConnector(string Name, string Url, string HttpMethod, string Body, Dictionary<string, string> Headers) : Identifiable(Name)
|
||||
{
|
||||
/// <summary>
|
||||
/// The Name of the Notification Connector
|
||||
/// </summary>
|
||||
[Required]
|
||||
[Description("The Name of the Notification Connector")]
|
||||
public string Name { get; init; } = Name;
|
||||
|
||||
/// <summary>
|
||||
/// The Url of the Instance
|
||||
/// </summary>
|
||||
/// <remarks>Formatting placeholders: "%title" and "%text" will be replaced when notifications are sent</remarks>
|
||||
[Required]
|
||||
[Url]
|
||||
[Description("The Url of the Instance")]
|
||||
public string Url { get; internal set; } = Url;
|
||||
|
||||
/// <summary>
|
||||
/// The HTTP Request Method to use for notifications
|
||||
/// </summary>
|
||||
[Required]
|
||||
[Description("The HTTP Request Method to use for notifications")]
|
||||
public string HttpMethod { get; internal set; } = HttpMethod;
|
||||
|
||||
/// <summary>
|
||||
/// The Request Body to use to send notifications
|
||||
/// </summary>
|
||||
/// <remarks>Formatting placeholders: "%title" and "%text" will be replaced when notifications are sent</remarks>
|
||||
[Required]
|
||||
[Description("The Request Body to use to send notifications")]
|
||||
public string Body { get; internal set; } = Body;
|
||||
|
||||
/// <summary>
|
||||
/// The Request Headers to use to send notifications
|
||||
/// </summary>
|
||||
/// <remarks>Formatting placeholders: "%title" and "%text" will be replaced when notifications are sent</remarks>
|
||||
[Required]
|
||||
[Description("The Request Headers to use to send notifications")]
|
||||
public Dictionary<string, string> Headers { get; internal set; } = Headers;
|
||||
}
|
@@ -1,33 +0,0 @@
|
||||
using System.ComponentModel;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using API.Workers;
|
||||
|
||||
namespace API.Controllers.DTOs;
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="IPeriodic"/> DTO (<seealso cref="Worker"/> <seealso cref="BaseWorker"/>)
|
||||
/// </summary>
|
||||
public sealed record PeriodicWorker(string Key, IEnumerable<string> Dependencies, IEnumerable<string> MissingDependencies, bool DependenciesFulfilled, WorkerExecutionState State, DateTime LastExecution, TimeSpan Interval, DateTime NextExecution)
|
||||
: Worker(Key, Dependencies, MissingDependencies, DependenciesFulfilled, State)
|
||||
{
|
||||
/// <summary>
|
||||
/// Timestamp when Worker executed last.
|
||||
/// </summary>
|
||||
[Required]
|
||||
[Description("Timestamp when Worker executed last.")]
|
||||
public DateTime LastExecution { get; init; } = LastExecution;
|
||||
|
||||
/// <summary>
|
||||
/// Interval at which Worker runs.
|
||||
/// </summary>
|
||||
[Required]
|
||||
[Description("Interval at which Worker runs.")]
|
||||
public TimeSpan Interval { get; init; } = Interval;
|
||||
|
||||
/// <summary>
|
||||
/// Timestamp when Worker is scheduled to execute next.
|
||||
/// </summary>
|
||||
[Required]
|
||||
[Description("Timestamp when Worker is scheduled to execute next.")]
|
||||
public DateTime NextExecution { get; init; } = LastExecution;
|
||||
}
|
@@ -1,39 +0,0 @@
|
||||
using System.ComponentModel;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using API.Workers;
|
||||
|
||||
namespace API.Controllers.DTOs;
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="BaseWorker"/> DTO
|
||||
/// </summary>
|
||||
public record Worker(string Key, IEnumerable<string> Dependencies, IEnumerable<string> MissingDependencies, bool DependenciesFulfilled, WorkerExecutionState State) : Identifiable(Key)
|
||||
{
|
||||
/// <summary>
|
||||
/// Workers this worker depends on having ran.
|
||||
/// </summary>
|
||||
[Required]
|
||||
[Description("Workers this worker depends on having ran.")]
|
||||
public IEnumerable<string> Dependencies { get; init; } = Dependencies;
|
||||
|
||||
/// <summary>
|
||||
/// Workers that have not yet ran, that need to run for this Worker to run.
|
||||
/// </summary>
|
||||
[Required]
|
||||
[Description("Workers that have not yet ran, that need to run for this Worker to run.")]
|
||||
public IEnumerable<string> MissingDependencies { get; init; } = MissingDependencies;
|
||||
|
||||
/// <summary>
|
||||
/// Worker can run.
|
||||
/// </summary>
|
||||
[Required]
|
||||
[Description("Worker can run.")]
|
||||
public bool DependenciesFulfilled { get; init; } = DependenciesFulfilled;
|
||||
|
||||
/// <summary>
|
||||
/// Execution state of the Worker.
|
||||
/// </summary>
|
||||
[Required]
|
||||
[Description("Execution state of the Worker.")]
|
||||
public WorkerExecutionState State { get; init; } = State;
|
||||
}
|
@@ -1,12 +1,7 @@
|
||||
using API.Controllers.Requests;
|
||||
using API.Schema.MangaContext;
|
||||
using API.Schema.MangaContext;
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Http.HttpResults;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using static Microsoft.AspNetCore.Http.StatusCodes;
|
||||
using FileLibrary = API.Controllers.DTOs.FileLibrary;
|
||||
|
||||
// ReSharper disable InconsistentNaming
|
||||
|
||||
namespace API.Controllers;
|
||||
@@ -17,21 +12,14 @@ namespace API.Controllers;
|
||||
public class FileLibraryController(MangaContext context) : Controller
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns all <see cref="DTOs.FileLibrary"/>
|
||||
/// Returns all <see cref="FileLibrary"/>
|
||||
/// </summary>
|
||||
/// <response code="200"></response>
|
||||
/// <response code="500">Error during Database Operation</response>
|
||||
[HttpGet]
|
||||
[ProducesResponseType<List<FileLibrary>>(Status200OK, "application/json")]
|
||||
[ProducesResponseType(Status500InternalServerError)]
|
||||
public async Task<Results<Ok<List<FileLibrary>>, InternalServerError>> GetFileLibraries ()
|
||||
[ProducesResponseType<FileLibrary[]>(Status200OK, "application/json")]
|
||||
public IActionResult GetFileLibraries()
|
||||
{
|
||||
if (await context.FileLibraries.ToListAsync(HttpContext.RequestAborted) is not { } result)
|
||||
return TypedResults.InternalServerError();
|
||||
|
||||
List<FileLibrary> fileLibraries = result.Select(f => new FileLibrary(f.Key, f.BasePath, f.LibraryName)).ToList();
|
||||
|
||||
return TypedResults.Ok(fileLibraries);
|
||||
return Ok(context.FileLibraries.ToArray());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -42,13 +30,13 @@ public class FileLibraryController(MangaContext context) : Controller
|
||||
/// <response code="404"><see cref="FileLibrary"/> with <paramref name="FileLibraryId"/> not found.</response>
|
||||
[HttpGet("{FileLibraryId}")]
|
||||
[ProducesResponseType<FileLibrary>(Status200OK, "application/json")]
|
||||
[ProducesResponseType<string>(Status404NotFound, "text/plain")]
|
||||
public async Task<Results<Ok<FileLibrary>, NotFound<string>>> GetFileLibrary (string FileLibraryId)
|
||||
[ProducesResponseType(Status404NotFound)]
|
||||
public IActionResult GetFileLibrary(string FileLibraryId)
|
||||
{
|
||||
if(await context.FileLibraries.FirstOrDefaultAsync(l => l.Key == FileLibraryId, HttpContext.RequestAborted) is not { } library)
|
||||
return TypedResults.NotFound(nameof(FileLibraryId));
|
||||
if (context.FileLibraries.Find(FileLibraryId) is not { } library)
|
||||
return NotFound();
|
||||
|
||||
return TypedResults.Ok(new FileLibrary(library.Key, library.BasePath, library.LibraryName));
|
||||
return Ok(library);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -61,19 +49,19 @@ public class FileLibraryController(MangaContext context) : Controller
|
||||
/// <response code="500">Error during Database Operation</response>
|
||||
[HttpPatch("{FileLibraryId}/ChangeBasePath")]
|
||||
[ProducesResponseType(Status200OK)]
|
||||
[ProducesResponseType<string>(Status404NotFound, "text/plain")]
|
||||
[ProducesResponseType(Status404NotFound)]
|
||||
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
|
||||
public async Task<Results<Ok, NotFound<string>, InternalServerError<string>>> ChangeLibraryBasePath (string FileLibraryId, [FromBody]string newBasePath)
|
||||
public IActionResult ChangeLibraryBasePath(string FileLibraryId, [FromBody]string newBasePath)
|
||||
{
|
||||
if(await context.FileLibraries.FirstOrDefaultAsync(l => l.Key == FileLibraryId, HttpContext.RequestAborted) is not { } library)
|
||||
return TypedResults.NotFound(nameof(FileLibraryId));
|
||||
if (context.FileLibraries.Find(FileLibraryId) is not { } library)
|
||||
return NotFound();
|
||||
|
||||
//TODO Path check
|
||||
library.BasePath = newBasePath;
|
||||
|
||||
if(await context.Sync(HttpContext.RequestAborted) is { success: false } result)
|
||||
return TypedResults.InternalServerError(result.exceptionMessage);
|
||||
return TypedResults.Ok();
|
||||
if(context.Sync() is { success: false } result)
|
||||
return StatusCode(Status500InternalServerError, result.exceptionMessage);
|
||||
return Ok();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -86,40 +74,40 @@ public class FileLibraryController(MangaContext context) : Controller
|
||||
/// <response code="500">Error during Database Operation</response>
|
||||
[HttpPatch("{FileLibraryId}/ChangeName")]
|
||||
[ProducesResponseType(Status200OK)]
|
||||
[ProducesResponseType<string>(Status404NotFound, "text/plain")]
|
||||
[ProducesResponseType(Status404NotFound)]
|
||||
[ProducesResponseType(Status400BadRequest)]
|
||||
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
|
||||
public async Task<Results<Ok, NotFound<string>, InternalServerError<string>>> ChangeLibraryName (string FileLibraryId, [FromBody] string newName)
|
||||
public IActionResult ChangeLibraryName(string FileLibraryId, [FromBody] string newName)
|
||||
{
|
||||
if(await context.FileLibraries.FirstOrDefaultAsync(l => l.Key == FileLibraryId, HttpContext.RequestAborted) is not { } library)
|
||||
return TypedResults.NotFound(nameof(FileLibraryId));
|
||||
if (context.FileLibraries.Find(FileLibraryId) is not { } library)
|
||||
return NotFound();
|
||||
|
||||
//TODO Name check
|
||||
library.LibraryName = newName;
|
||||
|
||||
if(await context.Sync(HttpContext.RequestAborted) is { success: false } result)
|
||||
return TypedResults.InternalServerError(result.exceptionMessage);
|
||||
return TypedResults.Ok();
|
||||
if(context.Sync() is { success: false } result)
|
||||
return StatusCode(Status500InternalServerError, result.exceptionMessage);
|
||||
return Ok();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates new <see cref="FileLibrary"/>
|
||||
/// Creates new <see cref="FileLibraryId"/>
|
||||
/// </summary>
|
||||
/// <param name="requestData">New <see cref="FileLibrary"/> to add</param>
|
||||
/// <response code="200">Key of new Library</response>
|
||||
/// <param name="library">New <see cref="FileLibrary"/> to add</param>
|
||||
/// <response code="200"></response>
|
||||
/// <response code="500">Error during Database Operation</response>
|
||||
[HttpPut]
|
||||
[ProducesResponseType<string>(Status201Created, "text/plain")]
|
||||
[ProducesResponseType(Status201Created)]
|
||||
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
|
||||
public async Task<Results<Created<string>, InternalServerError<string>>> CreateNewLibrary ([FromBody]CreateLibraryRecord requestData)
|
||||
public IActionResult CreateNewLibrary([FromBody]FileLibrary library)
|
||||
{
|
||||
|
||||
//TODO Parameter check
|
||||
Schema.MangaContext.FileLibrary library = new Schema.MangaContext.FileLibrary(requestData.BasePath, requestData.LibraryName);
|
||||
context.FileLibraries.Add(library);
|
||||
|
||||
if(await context.Sync(HttpContext.RequestAborted) is { success: false } result)
|
||||
return TypedResults.InternalServerError(result.exceptionMessage);
|
||||
|
||||
return TypedResults.Created(string.Empty, library.Key);
|
||||
if(context.Sync() is { success: false } result)
|
||||
return StatusCode(Status500InternalServerError, result.exceptionMessage);
|
||||
return Created();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -127,21 +115,20 @@ public class FileLibraryController(MangaContext context) : Controller
|
||||
/// </summary>
|
||||
/// <param name="FileLibraryId"><see cref="FileLibrary"/>.Key</param>
|
||||
/// <response code="200"></response>
|
||||
/// <response code="404"><see cref="FileLibrary"/> with <paramref name="FileLibraryId"/> not found.</response>
|
||||
/// <response code="500">Error during Database Operation</response>
|
||||
[HttpDelete("{FileLibraryId}")]
|
||||
[ProducesResponseType(Status200OK)]
|
||||
[ProducesResponseType<string>(Status404NotFound, "text/plain")]
|
||||
[ProducesResponseType(Status404NotFound)]
|
||||
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
|
||||
public async Task<Results<Ok, NotFound<string>, InternalServerError<string>>> DeleteLocalLibrary (string FileLibraryId)
|
||||
public IActionResult DeleteLocalLibrary(string FileLibraryId)
|
||||
{
|
||||
if(await context.FileLibraries.FirstOrDefaultAsync(l => l.Key == FileLibraryId, HttpContext.RequestAborted) is not { } library)
|
||||
return TypedResults.NotFound(nameof(FileLibraryId));
|
||||
if (context.FileLibraries.Find(FileLibraryId) is not { } library)
|
||||
return NotFound();
|
||||
|
||||
context.FileLibraries.Remove(library);
|
||||
|
||||
if(await context.Sync(HttpContext.RequestAborted) is { success: false } result)
|
||||
return TypedResults.InternalServerError(result.exceptionMessage);
|
||||
return TypedResults.Ok();
|
||||
if(context.Sync() is { success: false } result)
|
||||
return StatusCode(Status500InternalServerError, result.exceptionMessage);
|
||||
return Ok();
|
||||
}
|
||||
}
|
@@ -1,13 +1,8 @@
|
||||
using API.Controllers.Requests;
|
||||
using API.Schema.LibraryContext;
|
||||
using API.Schema.LibraryContext;
|
||||
using API.Schema.LibraryContext.LibraryConnectors;
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Http.HttpResults;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using static Microsoft.AspNetCore.Http.StatusCodes;
|
||||
using LibraryConnector = API.Controllers.DTOs.LibraryConnector;
|
||||
|
||||
// ReSharper disable InconsistentNaming
|
||||
|
||||
namespace API.Controllers;
|
||||
@@ -18,20 +13,16 @@ namespace API.Controllers;
|
||||
public class LibraryConnectorController(LibraryContext context) : Controller
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets all configured <see cref="DTOs.LibraryConnector"/>
|
||||
/// Gets all configured <see cref="LibraryConnector"/>
|
||||
/// </summary>
|
||||
/// <response code="200"></response>
|
||||
/// <response code="500">Error during Database Operation</response>
|
||||
[HttpGet]
|
||||
[ProducesResponseType<List<LibraryConnector>>(Status200OK, "application/json")]
|
||||
public async Task<Results<Ok<List<LibraryConnector>>, InternalServerError>> GetAllConnectors ()
|
||||
[ProducesResponseType<LibraryConnector[]>(Status200OK, "application/json")]
|
||||
public IActionResult GetAllConnectors()
|
||||
{
|
||||
if (await context.LibraryConnectors.ToListAsync(HttpContext.RequestAborted) is not { } connectors)
|
||||
return TypedResults.InternalServerError();
|
||||
LibraryConnector[] connectors = context.LibraryConnectors.ToArray();
|
||||
|
||||
List<LibraryConnector> libraryConnectors = connectors.Select(c => new LibraryConnector(c.Key, c.BaseUrl, c.LibraryType)).ToList();
|
||||
|
||||
return TypedResults.Ok(libraryConnectors);
|
||||
return Ok(connectors);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -42,39 +33,32 @@ public class LibraryConnectorController(LibraryContext context) : Controller
|
||||
/// <response code="404"><see cref="LibraryConnector"/> with <paramref name="LibraryConnectorId"/> not found.</response>
|
||||
[HttpGet("{LibraryConnectorId}")]
|
||||
[ProducesResponseType<LibraryConnector>(Status200OK, "application/json")]
|
||||
[ProducesResponseType<string>(Status404NotFound, "text/plain")]
|
||||
public async Task<Results<Ok<LibraryConnector>, NotFound<string>>> GetConnector (string LibraryConnectorId)
|
||||
[ProducesResponseType(Status404NotFound)]
|
||||
public IActionResult GetConnector(string LibraryConnectorId)
|
||||
{
|
||||
if (await context.LibraryConnectors.FirstOrDefaultAsync(l => l.Key == LibraryConnectorId) is not { } connector)
|
||||
return TypedResults.NotFound(nameof(LibraryConnectorId));
|
||||
if (context.LibraryConnectors.Find(LibraryConnectorId) is not { } connector)
|
||||
return NotFound();
|
||||
|
||||
return TypedResults.Ok(new LibraryConnector(connector.Key, connector.BaseUrl, connector.LibraryType));
|
||||
return Ok(connector);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="LibraryConnector"/>
|
||||
/// </summary>
|
||||
/// <param name="requestData"></param>
|
||||
/// <param name="libraryConnector"></param>
|
||||
/// <response code="201"></response>
|
||||
/// <response code="500">Error during Database Operation</response>
|
||||
[HttpPut]
|
||||
[ProducesResponseType<string>(Status201Created, "text/plain")]
|
||||
[ProducesResponseType(Status201Created)]
|
||||
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
|
||||
public async Task<Results<Created<string>, InternalServerError<string>>> CreateConnector ([FromBody]CreateLibraryConnectorRecord requestData)
|
||||
public IActionResult CreateConnector([FromBody]LibraryConnector libraryConnector)
|
||||
{
|
||||
//TODO verify data
|
||||
API.Schema.LibraryContext.LibraryConnectors.LibraryConnector connector = requestData.LibraryType switch
|
||||
{
|
||||
LibraryType.Kavita => new Kavita(requestData.Url, requestData.Username, requestData.Password),
|
||||
LibraryType.Komga => new Komga(requestData.Url, requestData.Username, requestData.Password),
|
||||
_ => throw new NotImplementedException()
|
||||
};
|
||||
|
||||
context.LibraryConnectors.Add(connector);
|
||||
context.LibraryConnectors.Add(libraryConnector);
|
||||
|
||||
if(await context.Sync(HttpContext.RequestAborted) is { success: false } result)
|
||||
return TypedResults.InternalServerError(result.exceptionMessage);
|
||||
return TypedResults.Created(string.Empty, connector.Key);
|
||||
if(context.Sync() is { success: false } result)
|
||||
return StatusCode(Status500InternalServerError, result.exceptionMessage);
|
||||
return Created();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -82,21 +66,21 @@ public class LibraryConnectorController(LibraryContext context) : Controller
|
||||
/// </summary>
|
||||
/// <param name="LibraryConnectorId">ToFileLibrary-Connector-ID</param>
|
||||
/// <response code="200"></response>
|
||||
/// <response code="404"><see cref="LibraryConnector"/> with <paramref name="LibraryConnectorId"/> not found.</response>
|
||||
/// <response code="404"><see cref="LibraryConnector"/> with <<paramref name="LibraryConnectorId"/> not found.</response>
|
||||
/// <response code="500">Error during Database Operation</response>
|
||||
[HttpDelete("{LibraryConnectorId}")]
|
||||
[ProducesResponseType(Status200OK)]
|
||||
[ProducesResponseType<string>(Status404NotFound, "text/plain")]
|
||||
[ProducesResponseType(Status404NotFound)]
|
||||
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
|
||||
public async Task<Results<Ok, NotFound<string>, InternalServerError<string>>> DeleteConnector (string LibraryConnectorId)
|
||||
public IActionResult DeleteConnector(string LibraryConnectorId)
|
||||
{
|
||||
if (await context.LibraryConnectors.FirstOrDefaultAsync(l => l.Key == LibraryConnectorId) is not { } connector)
|
||||
return TypedResults.NotFound(nameof(LibraryConnectorId));
|
||||
if (context.LibraryConnectors.Find(LibraryConnectorId) is not { } connector)
|
||||
return NotFound();
|
||||
|
||||
context.LibraryConnectors.Remove(connector);
|
||||
|
||||
if(await context.Sync(HttpContext.RequestAborted) is { success: false } result)
|
||||
return TypedResults.InternalServerError(result.exceptionMessage);
|
||||
return TypedResults.Ok();
|
||||
if(context.Sync() is { success: false } result)
|
||||
return StatusCode(Status500InternalServerError, result.exceptionMessage);
|
||||
return Ok();
|
||||
}
|
||||
}
|
@@ -1,7 +1,6 @@
|
||||
using API.MangaConnectors;
|
||||
using API.Schema.MangaContext;
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Http.HttpResults;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using static Microsoft.AspNetCore.Http.StatusCodes;
|
||||
@@ -22,18 +21,16 @@ public class MaintenanceController(MangaContext mangaContext) : Controller
|
||||
[HttpPost("CleanupNoDownloadManga")]
|
||||
[ProducesResponseType(Status200OK)]
|
||||
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
|
||||
public async Task<Results<Ok, InternalServerError<string>>> CleanupNoDownloadManga()
|
||||
public IActionResult CleanupNoDownloadManga()
|
||||
{
|
||||
if (await mangaContext.Mangas
|
||||
.Include(m => m.MangaConnectorIds)
|
||||
.Where(m => !m.MangaConnectorIds.Any(id => id.UseForDownload))
|
||||
.ToArrayAsync(HttpContext.RequestAborted) is not { } noDownloads)
|
||||
return TypedResults.InternalServerError("Could not fetch Manga");
|
||||
|
||||
Manga[] noDownloads = mangaContext.Mangas
|
||||
.Include(m => m.MangaConnectorIds)
|
||||
.Where(m => !m.MangaConnectorIds.Any(id => id.UseForDownload))
|
||||
.ToArray();
|
||||
mangaContext.Mangas.RemoveRange(noDownloads);
|
||||
|
||||
if(await mangaContext.Sync(HttpContext.RequestAborted) is { success: false } result)
|
||||
return TypedResults.InternalServerError(result.exceptionMessage);
|
||||
return TypedResults.Ok();
|
||||
if(mangaContext.Sync() is { success: false } result)
|
||||
return StatusCode(Status500InternalServerError, result.exceptionMessage);
|
||||
return Ok();
|
||||
}
|
||||
}
|
@@ -1,7 +1,6 @@
|
||||
using API.Controllers.DTOs;
|
||||
using API.MangaConnectors;
|
||||
using API.Schema.MangaContext;
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Http.HttpResults;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using static Microsoft.AspNetCore.Http.StatusCodes;
|
||||
// ReSharper disable InconsistentNaming
|
||||
@@ -14,85 +13,77 @@ namespace API.Controllers;
|
||||
public class MangaConnectorController(MangaContext context) : Controller
|
||||
{
|
||||
/// <summary>
|
||||
/// Get all <see cref="API.MangaConnectors.MangaConnector"/> (Scanlation-Sites)
|
||||
/// Get all <see cref="MangaConnector"/> (Scanlation-Sites)
|
||||
/// </summary>
|
||||
/// <response code="200">Names of <see cref="API.MangaConnectors.MangaConnector"/> (Scanlation-Sites)</response>
|
||||
/// <response code="200">Names of <see cref="MangaConnector"/> (Scanlation-Sites)</response>
|
||||
[HttpGet]
|
||||
[ProducesResponseType<List<MangaConnector>>(Status200OK, "application/json")]
|
||||
public Ok<List<MangaConnector>> GetConnectors()
|
||||
[ProducesResponseType<MangaConnector[]>(Status200OK, "application/json")]
|
||||
public IActionResult GetConnectors()
|
||||
{
|
||||
return TypedResults.Ok(Tranga.MangaConnectors
|
||||
.Select(c => new MangaConnector(c.Name, c.Enabled, c.IconUrl, c.SupportedLanguages))
|
||||
.ToList());
|
||||
return Ok(Tranga.MangaConnectors.ToArray());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the <see cref="API.MangaConnectors.MangaConnector"/> (Scanlation-Sites) with the requested Name
|
||||
/// Returns the <see cref="MangaConnector"/> (Scanlation-Sites) with the requested Name
|
||||
/// </summary>
|
||||
/// <param name="MangaConnectorName"><see cref="API.MangaConnectors.MangaConnector"/>.Name</param>
|
||||
/// <param name="MangaConnectorName"><see cref="MangaConnector"/>.Name</param>
|
||||
/// <response code="200"></response>
|
||||
/// <response code="404"><see cref="MangaConnector"/> (Scanlation-Sites) with Name not found.</response>
|
||||
[HttpGet("{MangaConnectorName}")]
|
||||
[ProducesResponseType<MangaConnector>(Status200OK, "application/json")]
|
||||
[ProducesResponseType<string>(Status404NotFound, "text/plain")]
|
||||
public Results<Ok<MangaConnector>, NotFound<string>> GetConnector(string MangaConnectorName)
|
||||
[ProducesResponseType(Status404NotFound)]
|
||||
public IActionResult GetConnector(string MangaConnectorName)
|
||||
{
|
||||
if(Tranga.MangaConnectors.FirstOrDefault(c => c.Name.Equals(MangaConnectorName, StringComparison.InvariantCultureIgnoreCase)) is not { } connector)
|
||||
return TypedResults.NotFound(nameof(MangaConnectorName));
|
||||
return NotFound();
|
||||
|
||||
return TypedResults.Ok(new MangaConnector(connector.Name, connector.Enabled, connector.IconUrl, connector.SupportedLanguages));
|
||||
return Ok(connector);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get all enabled <see cref="API.MangaConnectors.MangaConnector"/> (Scanlation-Sites)
|
||||
/// Get all enabled <see cref="MangaConnector"/> (Scanlation-Sites)
|
||||
/// </summary>
|
||||
/// <response code="200"></response>
|
||||
[HttpGet("Enabled")]
|
||||
[ProducesResponseType<List<MangaConnector>>(Status200OK, "application/json")]
|
||||
public Ok<List<MangaConnector>> GetEnabledConnectors()
|
||||
[ProducesResponseType<MangaConnector[]>(Status200OK, "application/json")]
|
||||
public IActionResult GetEnabledConnectors()
|
||||
{
|
||||
return TypedResults.Ok(Tranga.MangaConnectors
|
||||
.Where(c => c.Enabled)
|
||||
.Select(c => new MangaConnector(c.Name, c.Enabled, c.IconUrl, c.SupportedLanguages))
|
||||
.ToList());
|
||||
return Ok(Tranga.MangaConnectors.Where(c => c.Enabled).ToArray());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get all disabled <see cref="API.MangaConnectors.MangaConnector"/> (Scanlation-Sites)
|
||||
/// Get all disabled <see cref="MangaConnector"/> (Scanlation-Sites)
|
||||
/// </summary>
|
||||
/// <response code="200"></response>
|
||||
[HttpGet("Disabled")]
|
||||
[ProducesResponseType<List<MangaConnector>>(Status200OK, "application/json")]
|
||||
public Ok<List<MangaConnector>> GetDisabledConnectors()
|
||||
[ProducesResponseType<MangaConnector[]>(Status200OK, "application/json")]
|
||||
public IActionResult GetDisabledConnectors()
|
||||
{
|
||||
|
||||
return TypedResults.Ok(Tranga.MangaConnectors
|
||||
.Where(c => c.Enabled == false)
|
||||
.Select(c => new MangaConnector(c.Name, c.Enabled, c.IconUrl, c.SupportedLanguages))
|
||||
.ToList());
|
||||
return Ok(Tranga.MangaConnectors.Where(c => c.Enabled == false).ToArray());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enabled or disables <see cref="API.MangaConnectors.MangaConnector"/> (Scanlation-Sites) with Name
|
||||
/// Enabled or disables <see cref="MangaConnector"/> (Scanlation-Sites) with Name
|
||||
/// </summary>
|
||||
/// <param name="MangaConnectorName"><see cref="API.MangaConnectors.MangaConnector"/>.Name</param>
|
||||
/// <param name="MangaConnectorName"><see cref="MangaConnector"/>.Name</param>
|
||||
/// <param name="Enabled">Set true to enable, false to disable</param>
|
||||
/// <response code="200"></response>
|
||||
/// <response code="404"><see cref="API.MangaConnectors.MangaConnector"/> (Scanlation-Sites) with Name not found.</response>
|
||||
/// <response code="202"></response>
|
||||
/// <response code="404"><see cref="MangaConnector"/> (Scanlation-Sites) with Name not found.</response>
|
||||
/// <response code="500">Error during Database Operation</response>
|
||||
[HttpPatch("{MangaConnectorName}/SetEnabled/{Enabled}")]
|
||||
[ProducesResponseType(Status200OK)]
|
||||
[ProducesResponseType<string>(Status404NotFound, "text/plain")]
|
||||
[ProducesResponseType(Status202Accepted)]
|
||||
[ProducesResponseType(Status404NotFound)]
|
||||
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
|
||||
public async Task<Results<Ok, NotFound<string>, InternalServerError<string>>> SetEnabled(string MangaConnectorName, bool Enabled)
|
||||
public IActionResult SetEnabled(string MangaConnectorName, bool Enabled)
|
||||
{
|
||||
if(Tranga.MangaConnectors.FirstOrDefault(c => c.Name.Equals(MangaConnectorName, StringComparison.InvariantCultureIgnoreCase)) is not { } connector)
|
||||
return TypedResults.NotFound(nameof(MangaConnectorName));
|
||||
return NotFound();
|
||||
|
||||
connector.Enabled = Enabled;
|
||||
|
||||
if(await context.Sync(HttpContext.RequestAborted) is { success: false } result)
|
||||
return TypedResults.InternalServerError(result.exceptionMessage);
|
||||
return TypedResults.Ok();
|
||||
if(context.Sync() is { success: false } result)
|
||||
return StatusCode(Status500InternalServerError, result.exceptionMessage);
|
||||
return Accepted();
|
||||
}
|
||||
}
|
@@ -1,19 +1,16 @@
|
||||
using API.Controllers.DTOs;
|
||||
using API.MangaConnectors;
|
||||
using API.Schema.MangaContext;
|
||||
using API.Workers;
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Http.HttpResults;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.ChangeTracking;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.Formats.Jpeg;
|
||||
using SixLabors.ImageSharp.Processing;
|
||||
using SixLabors.ImageSharp.Processing.Processors.Transforms;
|
||||
using static Microsoft.AspNetCore.Http.StatusCodes;
|
||||
using AltTitle = API.Controllers.DTOs.AltTitle;
|
||||
using Author = API.Controllers.DTOs.Author;
|
||||
using Chapter = API.Controllers.DTOs.Chapter;
|
||||
using Link = API.Controllers.DTOs.Link;
|
||||
using Manga = API.Controllers.DTOs.Manga;
|
||||
|
||||
// ReSharper disable InconsistentNaming
|
||||
|
||||
namespace API.Controllers;
|
||||
@@ -23,116 +20,45 @@ namespace API.Controllers;
|
||||
[Route("v{v:apiVersion}/[controller]")]
|
||||
public class MangaController(MangaContext context) : Controller
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Returns all cached <see cref="DTOs.Manga"/>
|
||||
/// Returns all cached <see cref="Manga"/>
|
||||
/// </summary>
|
||||
/// <response code="200"><see cref="MinimalManga"/> exert of <see cref="Schema.MangaContext.Manga"/>. Use <see cref="GetManga"/> for more information</response>
|
||||
/// <response code="500">Error during Database Operation</response>
|
||||
/// <response code="200"><see cref="Manga"/> Keys/IDs</response>
|
||||
[HttpGet]
|
||||
[ProducesResponseType<List<MinimalManga>>(Status200OK, "application/json")]
|
||||
[ProducesResponseType(Status500InternalServerError)]
|
||||
public async Task<Results<Ok<List<MinimalManga>>, InternalServerError>> GetAllManga ()
|
||||
{
|
||||
if (await context.Mangas.Include(m => m.MangaConnectorIds).ToArrayAsync(HttpContext.RequestAborted) is not
|
||||
{ } result)
|
||||
return TypedResults.InternalServerError();
|
||||
|
||||
return TypedResults.Ok(result.Select(m =>
|
||||
{
|
||||
IEnumerable<MangaConnectorId> ids = m.MangaConnectorIds.Select(id => new MangaConnectorId(id.Key, id.MangaConnectorName, id.ObjId, id.WebsiteUrl, id.UseForDownload));
|
||||
return new MinimalManga(m.Key, m.Name, m.Description, m.ReleaseStatus, ids);
|
||||
}).ToList());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns all cached <see cref="Schema.MangaContext.Manga"/>.Keys
|
||||
/// </summary>
|
||||
/// <response code="200"><see cref="Schema.MangaContext.Manga"/> Keys/IDs</response>
|
||||
/// <response code="500">Error during Database Operation</response>
|
||||
[HttpGet("Keys")]
|
||||
[ProducesResponseType<string[]>(Status200OK, "application/json")]
|
||||
[ProducesResponseType(Status500InternalServerError)]
|
||||
public async Task<Results<Ok<string[]>, InternalServerError>> GetAllMangaKeys ()
|
||||
public IActionResult GetAllManga()
|
||||
{
|
||||
if (await context.Mangas.Select(m => m.Key).ToArrayAsync(HttpContext.RequestAborted) is not { } result)
|
||||
return TypedResults.InternalServerError();
|
||||
|
||||
return TypedResults.Ok(result);
|
||||
string[] ret = context.Mangas.Select(m => m.Key).ToArray();
|
||||
return Ok(ret);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns all <see cref="Schema.MangaContext.Manga"/> that are being downloaded from at least one <see cref="API.MangaConnectors.MangaConnector"/>
|
||||
/// Returns all cached <see cref="Manga"/> with <paramref name="MangaIds"/>
|
||||
/// </summary>
|
||||
/// <response code="200"><see cref="MinimalManga"/> exert of <see cref="Schema.MangaContext.Manga"/>. Use <see cref="GetManga"/> for more information</response>
|
||||
/// <response code="500">Error during Database Operation</response>
|
||||
[HttpGet("Downloading")]
|
||||
[ProducesResponseType<MinimalManga[]>(Status200OK, "application/json")]
|
||||
[ProducesResponseType(Status500InternalServerError)]
|
||||
public async Task<Results<Ok<List<MinimalManga>>, InternalServerError>> GetMangaDownloading()
|
||||
{
|
||||
if (await context.Mangas
|
||||
.Include(m => m.MangaConnectorIds)
|
||||
.Where(m => m.MangaConnectorIds.Any(id => id.UseForDownload))
|
||||
.ToArrayAsync(HttpContext.RequestAborted) is not { } result)
|
||||
return TypedResults.InternalServerError();
|
||||
|
||||
return TypedResults.Ok(result.Select(m =>
|
||||
{
|
||||
IEnumerable<MangaConnectorId> ids = m.MangaConnectorIds.Select(id => new MangaConnectorId(id.Key, id.MangaConnectorName, id.ObjId, id.WebsiteUrl, id.UseForDownload));
|
||||
return new MinimalManga(m.Key, m.Name, m.Description, m.ReleaseStatus, ids);
|
||||
}).ToList());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns all cached <see cref="Schema.MangaContext.Manga"/> with <paramref name="MangaIds"/>
|
||||
/// </summary>
|
||||
/// <param name="MangaIds">Array of <see cref="Schema.MangaContext.Manga"/>.Key</param>
|
||||
/// <response code="200"><see cref="DTOs.Manga"/></response>
|
||||
/// <response code="500">Error during Database Operation</response>
|
||||
/// <param name="MangaIds">Array of <<see cref="Manga"/>.Key</param>
|
||||
/// <response code="200"></response>
|
||||
[HttpPost("WithIDs")]
|
||||
[ProducesResponseType<List<Manga>>(Status200OK, "application/json")]
|
||||
[ProducesResponseType(Status500InternalServerError)]
|
||||
public async Task<Results<Ok<List<Manga>>, InternalServerError>> GetMangaWithIds ([FromBody]string[] MangaIds)
|
||||
[ProducesResponseType<Manga[]>(Status200OK, "application/json")]
|
||||
public IActionResult GetManga([FromBody]string[] MangaIds)
|
||||
{
|
||||
if (await context.MangaIncludeAll()
|
||||
.Where(m => MangaIds.Contains(m.Key))
|
||||
.ToArrayAsync(HttpContext.RequestAborted) is not { } result)
|
||||
return TypedResults.InternalServerError();
|
||||
|
||||
return TypedResults.Ok(result.Select(m =>
|
||||
{
|
||||
IEnumerable<MangaConnectorId> ids = m.MangaConnectorIds.Select(id => new MangaConnectorId(id.Key, id.MangaConnectorName, id.ObjId, id.WebsiteUrl, id.UseForDownload));
|
||||
IEnumerable<Author> authors = m.Authors.Select(a => new Author(a.Key, a.AuthorName));
|
||||
IEnumerable<string> tags = m.MangaTags.Select(t => t.Tag);
|
||||
IEnumerable<Link> links = m.Links.Select(l => new Link(l.Key, l.LinkProvider, l.LinkUrl));
|
||||
IEnumerable<AltTitle> altTitles = m.AltTitles.Select(a => new AltTitle(a.Language, a.Title));
|
||||
return new Manga(m.Key, m.Name, m.Description, m.ReleaseStatus, ids, m.IgnoreChaptersBefore, m.Year, m.OriginalLanguage, m.ChapterIds, authors, tags, links, altTitles, m.LibraryId);
|
||||
}).ToList());
|
||||
Manga[] ret = context.MangaIncludeAll().Where(m => MangaIds.Contains(m.Key)).ToArray();
|
||||
return Ok(ret);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Return <see cref="Schema.MangaContext.Manga"/> with <paramref name="MangaId"/>
|
||||
/// Return <see cref="Manga"/> with <paramref name="MangaId"/>
|
||||
/// </summary>
|
||||
/// <param name="MangaId"><see cref="Schema.MangaContext.Manga"/>.Key</param>
|
||||
/// <param name="MangaId"><see cref="Manga"/>.Key</param>
|
||||
/// <response code="200"></response>
|
||||
/// <response code="404"><see cref="Manga"/> with <paramref name="MangaId"/> not found</response>
|
||||
[HttpGet("{MangaId}")]
|
||||
[ProducesResponseType<Manga>(Status200OK, "application/json")]
|
||||
[ProducesResponseType<string>(Status404NotFound, "text/plain")]
|
||||
public async Task<Results<Ok<Manga>, NotFound<string>>> GetManga (string MangaId)
|
||||
[ProducesResponseType(Status404NotFound)]
|
||||
public IActionResult GetManga(string MangaId)
|
||||
{
|
||||
if (await context.MangaIncludeAll().FirstOrDefaultAsync(m => m.Key == MangaId, HttpContext.RequestAborted) is not { } manga)
|
||||
return TypedResults.NotFound(nameof(MangaId));
|
||||
|
||||
IEnumerable<MangaConnectorId> ids = manga.MangaConnectorIds.Select(id => new MangaConnectorId(id.Key, id.MangaConnectorName, id.ObjId, id.WebsiteUrl, id.UseForDownload));
|
||||
IEnumerable<Author> authors = manga.Authors.Select(a => new Author(a.Key, a.AuthorName));
|
||||
IEnumerable<string> tags = manga.MangaTags.Select(t => t.Tag);
|
||||
IEnumerable<Link> links = manga.Links.Select(l => new Link(l.Key, l.LinkProvider, l.LinkUrl));
|
||||
IEnumerable<AltTitle> altTitles = manga.AltTitles.Select(a => new AltTitle(a.Language, a.Title));
|
||||
Manga result = new (manga.Key, manga.Name, manga.Description, manga.ReleaseStatus, ids, manga.IgnoreChaptersBefore, manga.Year, manga.OriginalLanguage, manga.ChapterIds, authors, tags, links, altTitles, manga.LibraryId);
|
||||
|
||||
return TypedResults.Ok(result);
|
||||
if (context.MangaIncludeAll().FirstOrDefault(m => m.Key == MangaId) is not { } manga)
|
||||
return NotFound(nameof(MangaId));
|
||||
return Ok(manga);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -144,18 +70,18 @@ public class MangaController(MangaContext context) : Controller
|
||||
/// <response code="500">Error during Database Operation</response>
|
||||
[HttpDelete("{MangaId}")]
|
||||
[ProducesResponseType(Status200OK)]
|
||||
[ProducesResponseType<string>(Status404NotFound, "text/plain")]
|
||||
[ProducesResponseType(Status404NotFound)]
|
||||
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
|
||||
public async Task<Results<Ok, NotFound<string>, InternalServerError<string>>> DeleteManga (string MangaId)
|
||||
public IActionResult DeleteManga(string MangaId)
|
||||
{
|
||||
if (await context.Mangas.FirstOrDefaultAsync(m => m.Key == MangaId, HttpContext.RequestAborted) is not { } manga)
|
||||
return TypedResults.NotFound(nameof(MangaId));
|
||||
if (context.Mangas.Find(MangaId) is not { } manga)
|
||||
return NotFound(nameof(MangaId));
|
||||
|
||||
context.Mangas.Remove(manga);
|
||||
|
||||
if(await context.Sync(HttpContext.RequestAborted) is { success: false } result)
|
||||
return TypedResults.InternalServerError(result.exceptionMessage);
|
||||
return TypedResults.Ok();
|
||||
if(context.Sync() is { success: false } result)
|
||||
return StatusCode(Status500InternalServerError, result.exceptionMessage);
|
||||
return Ok();
|
||||
}
|
||||
|
||||
|
||||
@@ -167,71 +93,82 @@ public class MangaController(MangaContext context) : Controller
|
||||
/// <response code="200"></response>
|
||||
/// <response code="404"><see cref="Manga"/> with <paramref name="MangaIdFrom"/> or <paramref name="MangaIdInto"/> not found</response>
|
||||
[HttpPatch("{MangaIdFrom}/MergeInto/{MangaIdInto}")]
|
||||
[ProducesResponseType(Status200OK)]
|
||||
[ProducesResponseType<string>(Status404NotFound, "text/plain")]
|
||||
public async Task<Results<Ok, NotFound<string>>> MergeIntoManga (string MangaIdFrom, string MangaIdInto)
|
||||
[ProducesResponseType<byte[]>(Status200OK,"image/jpeg")]
|
||||
[ProducesResponseType(Status404NotFound)]
|
||||
public IActionResult MergeIntoManga(string MangaIdFrom, string MangaIdInto)
|
||||
{
|
||||
if (await context.MangaIncludeAll().FirstOrDefaultAsync(m => m.Key == MangaIdFrom, HttpContext.RequestAborted) is not { } from)
|
||||
return TypedResults.NotFound(nameof(MangaIdFrom));
|
||||
if (await context.MangaIncludeAll().FirstOrDefaultAsync(m => m.Key == MangaIdInto, HttpContext.RequestAborted) is not { } into)
|
||||
return TypedResults.NotFound(nameof(MangaIdInto));
|
||||
if (context.Mangas.Find(MangaIdFrom) is not { } from)
|
||||
return NotFound(nameof(MangaIdFrom));
|
||||
if (context.Mangas.Find(MangaIdInto) is not { } into)
|
||||
return NotFound(nameof(MangaIdInto));
|
||||
|
||||
|
||||
foreach (CollectionEntry collectionEntry in context.Entry(from).Collections)
|
||||
collectionEntry.Load();
|
||||
context.Entry(from).Navigation(nameof(Manga.Library)).Load();
|
||||
|
||||
foreach (CollectionEntry collectionEntry in context.Entry(into).Collections)
|
||||
collectionEntry.Load();
|
||||
context.Entry(into).Navigation(nameof(Manga.Library)).Load();
|
||||
|
||||
BaseWorker[] newJobs = into.MergeFrom(from, context);
|
||||
Tranga.AddWorkers(newJobs);
|
||||
|
||||
return TypedResults.Ok();
|
||||
return Ok();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns Cover of <see cref="Manga"/> with <paramref name="MangaId"/>
|
||||
/// </summary>
|
||||
/// <param name="MangaId"><see cref="Manga"/>.Key</param>
|
||||
/// <param name="CoverSize">Size of the cover returned
|
||||
/// <br /> - <see cref="CoverSize.Small"/> <see cref="Constants.ImageSmSize"/>
|
||||
/// <br /> - <see cref="CoverSize.Medium"/> <see cref="Constants.ImageMdSize"/>
|
||||
/// <br /> - <see cref="CoverSize.Large"/> <see cref="Constants.ImageLgSize"/>
|
||||
/// </param>
|
||||
/// <param name="width">If <paramref name="width"/> is provided, <paramref name="height"/> needs to also be provided</param>
|
||||
/// <param name="height">If <paramref name="height"/> is provided, <paramref name="width"/> needs to also be provided</param>
|
||||
/// <response code="200">JPEG Image</response>
|
||||
/// <response code="204">Cover not loaded</response>
|
||||
/// <response code="400">The formatting-request was invalid</response>
|
||||
/// <response code="404"><see cref="Manga"/> with <paramref name="MangaId"/> not found</response>
|
||||
/// <response code="503">Retry later, downloading cover</response>
|
||||
[HttpGet("{MangaId}/Cover/{CoverSize?}")]
|
||||
[ProducesResponseType<FileContentResult>(Status200OK,"image/jpeg")]
|
||||
[HttpGet("{MangaId}/Cover")]
|
||||
[ProducesResponseType<byte[]>(Status200OK,"image/jpeg")]
|
||||
[ProducesResponseType(Status204NoContent)]
|
||||
[ProducesResponseType(Status400BadRequest)]
|
||||
[ProducesResponseType<string>(Status404NotFound, "text/plain")]
|
||||
[ProducesResponseType(Status503ServiceUnavailable)]
|
||||
public async Task<Results<FileContentHttpResult, NoContent, BadRequest, NotFound<string>, StatusCodeHttpResult>> GetCover (string MangaId, CoverSize? CoverSize = null)
|
||||
[ProducesResponseType(Status404NotFound)]
|
||||
[ProducesResponseType<int>(Status503ServiceUnavailable, "text/plain")]
|
||||
public IActionResult GetCover(string MangaId, [FromQuery]int? width, [FromQuery]int? height)
|
||||
{
|
||||
if (await context.Mangas.FirstOrDefaultAsync(m => m.Key == MangaId, HttpContext.RequestAborted) is not { } manga)
|
||||
return TypedResults.NotFound(nameof(MangaId));
|
||||
if (context.Mangas.Find(MangaId) is not { } manga)
|
||||
return NotFound(nameof(MangaId));
|
||||
|
||||
string cache = CoverSize switch
|
||||
{
|
||||
MangaController.CoverSize.Small => TrangaSettings.coverImageCacheSmall,
|
||||
MangaController.CoverSize.Medium => TrangaSettings.coverImageCacheMedium,
|
||||
MangaController.CoverSize.Large => TrangaSettings.coverImageCacheLarge,
|
||||
_ => TrangaSettings.coverImageCacheOriginal
|
||||
};
|
||||
|
||||
if (await manga.GetCoverImage(cache, HttpContext.RequestAborted) is not { } data)
|
||||
if (!System.IO.File.Exists(manga.CoverFileNameInCache))
|
||||
{
|
||||
if (Tranga.GetRunningWorkers().Any(worker => worker is DownloadCoverFromMangaconnectorWorker w && context.MangaConnectorToManga.Find(w.MangaConnectorIdId)?.ObjId == MangaId))
|
||||
{
|
||||
Response.Headers.Append("Retry-After", $"{Tranga.Settings.WorkCycleTimeoutMs * 2 / 1000:D}");
|
||||
return TypedResults.StatusCode(Status503ServiceUnavailable);
|
||||
return StatusCode(Status503ServiceUnavailable, Tranga.Settings.WorkCycleTimeoutMs * 2 / 1000);
|
||||
}
|
||||
return TypedResults.NoContent();
|
||||
else
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
DateTime lastModified = data.fileInfo.LastWriteTime;
|
||||
EntityTagHeaderValue entityTagHeaderValue = EntityTagHeaderValue.Parse($"\"{lastModified.Ticks}\"");
|
||||
if(HttpContext.Request.Headers.ETag.Equals(entityTagHeaderValue.Tag.Value))
|
||||
return TypedResults.StatusCode(Status304NotModified);
|
||||
Image image = Image.Load(manga.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});
|
||||
DateTime lastModified = new FileInfo(manga.CoverFileNameInCache).LastWriteTime;
|
||||
HttpContext.Response.Headers.CacheControl = "public";
|
||||
return TypedResults.Bytes(data.stream.ToArray(), "image/jpeg", lastModified: new DateTimeOffset(lastModified), entityTag: entityTagHeaderValue);
|
||||
return File(ms.GetBuffer(), "image/jpeg", new DateTimeOffset(lastModified), EntityTagHeaderValue.Parse($"\"{lastModified.Ticks}\""));
|
||||
}
|
||||
public enum CoverSize { Original, Large, Medium, Small }
|
||||
|
||||
/// <summary>
|
||||
/// Returns all <see cref="Chapter"/> of <see cref="Manga"/> with <paramref name="MangaId"/>
|
||||
@@ -240,20 +177,17 @@ public class MangaController(MangaContext context) : Controller
|
||||
/// <response code="200"></response>
|
||||
/// <response code="404"><see cref="Manga"/> with <paramref name="MangaId"/> not found</response>
|
||||
[HttpGet("{MangaId}/Chapters")]
|
||||
[ProducesResponseType<List<Chapter>>(Status200OK, "application/json")]
|
||||
[ProducesResponseType<Chapter[]>(Status200OK, "application/json")]
|
||||
[ProducesResponseType(Status404NotFound)]
|
||||
public async Task<Results<Ok<List<Chapter>>, NotFound<string>>> GetChapters(string MangaId)
|
||||
public IActionResult GetChapters(string MangaId)
|
||||
{
|
||||
if (await context.Mangas.Include(m => m.Chapters).ThenInclude(c => c.MangaConnectorIds).FirstOrDefaultAsync(m => m.Key == MangaId, HttpContext.RequestAborted) is not { } manga)
|
||||
return TypedResults.NotFound(nameof(MangaId));
|
||||
if (context.Mangas.Find(MangaId) is not { } manga)
|
||||
return NotFound(nameof(MangaId));
|
||||
|
||||
List<Chapter> chapters = manga.Chapters.Select(c =>
|
||||
{
|
||||
IEnumerable<MangaConnectorId> ids = c.MangaConnectorIds.Select(id =>
|
||||
new MangaConnectorId(id.Key, id.MangaConnectorName, id.ObjId, id.WebsiteUrl, id.UseForDownload));
|
||||
return new Chapter(c.Key, c.ParentMangaId, c.VolumeNumber, c.ChapterNumber, c.Title, ids, c.Downloaded);
|
||||
}).ToList();
|
||||
return TypedResults.Ok(chapters);
|
||||
context.Entry(manga).Collection(m => m.Chapters).Load();
|
||||
|
||||
Chapter[] chapters = manga.Chapters.ToArray();
|
||||
return Ok(chapters);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -266,22 +200,19 @@ public class MangaController(MangaContext context) : Controller
|
||||
[HttpGet("{MangaId}/Chapters/Downloaded")]
|
||||
[ProducesResponseType<Chapter[]>(Status200OK, "application/json")]
|
||||
[ProducesResponseType(Status204NoContent)]
|
||||
[ProducesResponseType<string>(Status404NotFound, "text/plain")]
|
||||
public async Task<Results<Ok<List<Chapter>>, NoContent, NotFound<string>>> GetChaptersDownloaded(string MangaId)
|
||||
[ProducesResponseType(Status404NotFound)]
|
||||
public IActionResult GetChaptersDownloaded(string MangaId)
|
||||
{
|
||||
if (await context.Mangas.Include(m => m.Chapters).FirstOrDefaultAsync(m => m.Key == MangaId, HttpContext.RequestAborted) is not { } manga)
|
||||
return TypedResults.NotFound(nameof(MangaId));
|
||||
if (context.Mangas.Find(MangaId) is not { } manga)
|
||||
return NotFound(nameof(MangaId));
|
||||
|
||||
List<Chapter> chapters = manga.Chapters.Where(c => c.Downloaded).Select(c =>
|
||||
{
|
||||
IEnumerable<MangaConnectorId> ids = c.MangaConnectorIds.Select(id =>
|
||||
new MangaConnectorId(id.Key, id.MangaConnectorName, id.ObjId, id.WebsiteUrl, id.UseForDownload));
|
||||
return new Chapter(c.Key, c.ParentMangaId, c.VolumeNumber, c.ChapterNumber, c.Title, ids, c.Downloaded);
|
||||
}).ToList();
|
||||
context.Entry(manga).Collection(m => m.Chapters).Load();
|
||||
|
||||
List<Chapter> chapters = manga.Chapters.Where(c => c.Downloaded).ToList();
|
||||
if (chapters.Count == 0)
|
||||
return TypedResults.NoContent();
|
||||
return NoContent();
|
||||
|
||||
return TypedResults.Ok(chapters);
|
||||
return Ok(chapters);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -292,28 +223,25 @@ public class MangaController(MangaContext context) : Controller
|
||||
/// <response code="204">No available chapters</response>
|
||||
/// <response code="404"><see cref="Manga"/> with <paramref name="MangaId"/> not found.</response>
|
||||
[HttpGet("{MangaId}/Chapters/NotDownloaded")]
|
||||
[ProducesResponseType<List<Chapter>>(Status200OK, "application/json")]
|
||||
[ProducesResponseType<Chapter[]>(Status200OK, "application/json")]
|
||||
[ProducesResponseType(Status204NoContent)]
|
||||
[ProducesResponseType<string>(Status404NotFound, "text/plain")]
|
||||
public async Task<Results<Ok<List<Chapter>>, NoContent, NotFound<string>>> GetChaptersNotDownloaded(string MangaId)
|
||||
[ProducesResponseType(Status404NotFound)]
|
||||
public IActionResult GetChaptersNotDownloaded(string MangaId)
|
||||
{
|
||||
if (await context.Mangas.Include(m => m.Chapters).FirstOrDefaultAsync(m => m.Key == MangaId, HttpContext.RequestAborted) is not { } manga)
|
||||
return TypedResults.NotFound(nameof(MangaId));
|
||||
if (context.Mangas.Find(MangaId) is not { } manga)
|
||||
return NotFound(nameof(MangaId));
|
||||
|
||||
List<Chapter> chapters = manga.Chapters.Where(c => c.Downloaded == false).Select(c =>
|
||||
{
|
||||
IEnumerable<MangaConnectorId> ids = c.MangaConnectorIds.Select(id =>
|
||||
new MangaConnectorId(id.Key, id.MangaConnectorName, id.ObjId, id.WebsiteUrl, id.UseForDownload));
|
||||
return new Chapter(c.Key, c.ParentMangaId, c.VolumeNumber, c.ChapterNumber, c.Title, ids, c.Downloaded);
|
||||
}).ToList();
|
||||
context.Entry(manga).Collection(m => m.Chapters).Load();
|
||||
|
||||
List<Chapter> chapters = manga.Chapters.Where(c => c.Downloaded == false).ToList();
|
||||
if (chapters.Count == 0)
|
||||
return TypedResults.NoContent();
|
||||
return NoContent();
|
||||
|
||||
return TypedResults.Ok(chapters);
|
||||
return Ok(chapters);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the latest <see cref="Chapter"/> of requested <see cref="Manga"/> available on <see cref="API.MangaConnectors.MangaConnector"/>
|
||||
/// Returns the latest <see cref="Chapter"/> of requested <see cref="Manga"/> available on <see cref="MangaConnector"/>
|
||||
/// </summary>
|
||||
/// <param name="MangaId"><see cref="Manga"/>.Key</param>
|
||||
/// <response code="200"></response>
|
||||
@@ -322,41 +250,38 @@ public class MangaController(MangaContext context) : Controller
|
||||
/// <response code="412">Could not retrieve the maximum chapter-number</response>
|
||||
/// <response code="503">Retry after timeout, updating value</response>
|
||||
[HttpGet("{MangaId}/Chapter/LatestAvailable")]
|
||||
[ProducesResponseType<int>(Status200OK, "application/json")]
|
||||
[ProducesResponseType<Chapter>(Status200OK, "application/json")]
|
||||
[ProducesResponseType(Status204NoContent)]
|
||||
[ProducesResponseType<string>(Status404NotFound, "text/plain")]
|
||||
[ProducesResponseType(Status500InternalServerError)]
|
||||
[ProducesResponseType(Status503ServiceUnavailable)]
|
||||
public async Task<Results<Ok<Chapter>, NoContent, InternalServerError, NotFound<string>, StatusCodeHttpResult>> GetLatestChapter(string MangaId)
|
||||
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
|
||||
[ProducesResponseType<int>(Status503ServiceUnavailable, "text/plain")]
|
||||
public IActionResult GetLatestChapter(string MangaId)
|
||||
{
|
||||
if (await context.Mangas.Include(m => m.Chapters).FirstOrDefaultAsync(m => m.Key == MangaId, HttpContext.RequestAborted) is not { } manga)
|
||||
return TypedResults.NotFound(nameof(MangaId));
|
||||
if (context.Mangas.Find(MangaId) is not { } manga)
|
||||
return NotFound(nameof(MangaId));
|
||||
|
||||
List<API.Schema.MangaContext.Chapter> chapters = manga.Chapters.ToList();
|
||||
context.Entry(manga).Collection(m => m.Chapters).Load();
|
||||
|
||||
List<Chapter> chapters = manga.Chapters.ToList();
|
||||
if (chapters.Count == 0)
|
||||
{
|
||||
if (Tranga.GetRunningWorkers().Any(worker =>
|
||||
worker is RetrieveMangaChaptersFromMangaconnectorWorker w &&
|
||||
context.MangaConnectorToManga.Find(w.MangaConnectorIdId)?.ObjId == MangaId &&
|
||||
w.State < WorkerExecutionState.Completed))
|
||||
if (Tranga.GetRunningWorkers().Any(worker => worker is RetrieveMangaChaptersFromMangaconnectorWorker w && context.MangaConnectorToManga.Find(w.MangaConnectorIdId)?.ObjId == MangaId && w.State < WorkerExecutionState.Completed))
|
||||
{
|
||||
Response.Headers.Append("Retry-After", $"{Tranga.Settings.WorkCycleTimeoutMs * 2 / 1000:D}");
|
||||
return TypedResults.StatusCode(Status503ServiceUnavailable);
|
||||
}
|
||||
else
|
||||
return TypedResults.NoContent();
|
||||
return StatusCode(Status503ServiceUnavailable, Tranga.Settings.WorkCycleTimeoutMs * 2/ 1000);
|
||||
}else
|
||||
return Ok(0);
|
||||
}
|
||||
|
||||
API.Schema.MangaContext.Chapter? max = chapters.Max();
|
||||
Chapter? max = chapters.Max();
|
||||
if (max is null)
|
||||
return TypedResults.InternalServerError();
|
||||
return StatusCode(Status500InternalServerError, "Max chapter could not be found");
|
||||
|
||||
foreach (CollectionEntry collectionEntry in context.Entry(max).Collections)
|
||||
await collectionEntry.LoadAsync(HttpContext.RequestAborted);
|
||||
collectionEntry.Load();
|
||||
context.Entry(max).Navigation(nameof(Chapter.ParentManga)).Load();
|
||||
|
||||
IEnumerable<MangaConnectorId> ids = max.MangaConnectorIds.Select(id =>
|
||||
new MangaConnectorId(id.Key, id.MangaConnectorName, id.ObjId, id.WebsiteUrl, id.UseForDownload));
|
||||
return TypedResults.Ok(new Chapter(max.Key, max.ParentMangaId, max.VolumeNumber, max.ChapterNumber, max.Title,ids, max.Downloaded));
|
||||
return Ok(max);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -371,34 +296,36 @@ public class MangaController(MangaContext context) : Controller
|
||||
[HttpGet("{MangaId}/Chapter/LatestDownloaded")]
|
||||
[ProducesResponseType<Chapter>(Status200OK, "application/json")]
|
||||
[ProducesResponseType(Status204NoContent)]
|
||||
[ProducesResponseType<string>(Status404NotFound, "text/plain")]
|
||||
[ProducesResponseType(Status412PreconditionFailed)]
|
||||
[ProducesResponseType(Status503ServiceUnavailable)]
|
||||
public async Task<Results<Ok<Chapter>, NoContent, NotFound<string>, StatusCodeHttpResult>> GetLatestChapterDownloaded(string MangaId)
|
||||
[ProducesResponseType(Status404NotFound)]
|
||||
[ProducesResponseType<string>(Status412PreconditionFailed, "text/plain")]
|
||||
[ProducesResponseType<int>(Status503ServiceUnavailable, "text/plain")]
|
||||
public IActionResult GetLatestChapterDownloaded(string MangaId)
|
||||
{
|
||||
if (await context.Mangas.FirstOrDefaultAsync(m => m.Key == MangaId, HttpContext.RequestAborted) is not { } manga)
|
||||
return TypedResults.NotFound(nameof(MangaId));
|
||||
if (context.Mangas.Find(MangaId) is not { } manga)
|
||||
return NotFound(nameof(MangaId));
|
||||
|
||||
await context.Entry(manga).Collection(m => m.Chapters).LoadAsync(HttpContext.RequestAborted);
|
||||
context.Entry(manga).Collection(m => m.Chapters).Load();
|
||||
|
||||
List<API.Schema.MangaContext.Chapter> chapters = manga.Chapters.ToList();
|
||||
List<Chapter> chapters = manga.Chapters.ToList();
|
||||
if (chapters.Count == 0)
|
||||
{
|
||||
if (Tranga.GetRunningWorkers().Any(worker => worker is RetrieveMangaChaptersFromMangaconnectorWorker w && context.MangaConnectorToManga.Find(w.MangaConnectorIdId)?.ObjId == MangaId && w.State < WorkerExecutionState.Completed))
|
||||
{
|
||||
Response.Headers.Append("Retry-After", $"{Tranga.Settings.WorkCycleTimeoutMs * 2 / 1000:D}");
|
||||
return TypedResults.StatusCode(Status503ServiceUnavailable);
|
||||
return StatusCode(Status503ServiceUnavailable, Tranga.Settings.WorkCycleTimeoutMs * 2/ 1000);
|
||||
}else
|
||||
return TypedResults.NoContent();
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
API.Schema.MangaContext.Chapter? max = chapters.Max();
|
||||
Chapter? max = chapters.Max();
|
||||
if (max is null)
|
||||
return TypedResults.StatusCode(Status412PreconditionFailed);
|
||||
return StatusCode(Status412PreconditionFailed, "Max chapter could not be found");
|
||||
|
||||
IEnumerable<MangaConnectorId> ids = max.MangaConnectorIds.Select(id =>
|
||||
new MangaConnectorId(id.Key, id.MangaConnectorName, id.ObjId, id.WebsiteUrl, id.UseForDownload));
|
||||
return TypedResults.Ok(new Chapter(max.Key, max.ParentMangaId, max.VolumeNumber, max.ChapterNumber, max.Title,ids, max.Downloaded));
|
||||
foreach (CollectionEntry collectionEntry in context.Entry(max).Collections)
|
||||
collectionEntry.Load();
|
||||
context.Entry(max).Navigation(nameof(Chapter.ParentManga)).Load();
|
||||
|
||||
return Ok(max);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -410,19 +337,19 @@ public class MangaController(MangaContext context) : Controller
|
||||
/// <response code="404"><see cref="Manga"/> with <paramref name="MangaId"/> not found.</response>
|
||||
/// <response code="500">Error during Database Operation</response>
|
||||
[HttpPatch("{MangaId}/IgnoreChaptersBefore")]
|
||||
[ProducesResponseType(Status200OK)]
|
||||
[ProducesResponseType<string>(Status404NotFound, "text/plain")]
|
||||
[ProducesResponseType(Status202Accepted)]
|
||||
[ProducesResponseType(Status404NotFound)]
|
||||
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
|
||||
public async Task<Results<Ok, NotFound<string>, InternalServerError<string>>> IgnoreChaptersBefore(string MangaId, [FromBody]float chapterThreshold)
|
||||
public IActionResult IgnoreChaptersBefore(string MangaId, [FromBody]float chapterThreshold)
|
||||
{
|
||||
if (await context.Mangas.FirstOrDefaultAsync(m => m.Key == MangaId, HttpContext.RequestAborted) is not { } manga)
|
||||
return TypedResults.NotFound(nameof(MangaId));
|
||||
if (context.Mangas.Find(MangaId) is not { } manga)
|
||||
return NotFound();
|
||||
|
||||
manga.IgnoreChaptersBefore = chapterThreshold;
|
||||
if(await context.Sync(HttpContext.RequestAborted) is { success: false } result)
|
||||
return TypedResults.InternalServerError(result.exceptionMessage);
|
||||
if(context.Sync() is { success: false } result)
|
||||
return StatusCode(Status500InternalServerError, result.exceptionMessage);
|
||||
|
||||
return TypedResults.Ok();
|
||||
return Accepted();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -433,35 +360,36 @@ public class MangaController(MangaContext context) : Controller
|
||||
/// <response code="202">Folder is going to be moved</response>
|
||||
/// <response code="404"><paramref name="MangaId"/> or <paramref name="LibraryId"/> not found</response>
|
||||
[HttpPost("{MangaId}/ChangeLibrary/{LibraryId}")]
|
||||
[ProducesResponseType(Status200OK)]
|
||||
[ProducesResponseType<string>(Status404NotFound, "text/plain")]
|
||||
public async Task<Results<Ok, NotFound<string>>> ChangeLibrary(string MangaId, string LibraryId)
|
||||
[ProducesResponseType(Status202Accepted)]
|
||||
[ProducesResponseType(Status404NotFound)]
|
||||
public IActionResult ChangeLibrary(string MangaId, string LibraryId)
|
||||
{
|
||||
if (await context.Mangas.FirstOrDefaultAsync(m => m.Key == MangaId, HttpContext.RequestAborted) is not { } manga)
|
||||
return TypedResults.NotFound(nameof(MangaId));
|
||||
if (await context.FileLibraries.FirstOrDefaultAsync(l => l.Key == LibraryId, HttpContext.RequestAborted) is not { } library)
|
||||
return TypedResults.NotFound(nameof(LibraryId));
|
||||
if (context.Mangas.Find(MangaId) is not { } manga)
|
||||
return NotFound(nameof(MangaId));
|
||||
if(context.FileLibraries.Find(LibraryId) is not { } library)
|
||||
return NotFound(nameof(LibraryId));
|
||||
|
||||
if(manga.LibraryId == library.Key)
|
||||
return TypedResults.Ok();
|
||||
foreach (CollectionEntry collectionEntry in context.Entry(manga).Collections)
|
||||
collectionEntry.Load();
|
||||
context.Entry(manga).Navigation(nameof(Manga.Library)).Load();
|
||||
|
||||
MoveMangaLibraryWorker moveLibrary = new(manga, library);
|
||||
|
||||
Tranga.AddWorkers([moveLibrary]);
|
||||
|
||||
return TypedResults.Ok();
|
||||
return Accepted();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// (Un-)Marks <see cref="Manga"/> as requested for Download from <see cref="API.MangaConnectors.MangaConnector"/>
|
||||
/// (Un-)Marks <see cref="Manga"/> as requested for Download from <see cref="MangaConnector"/>
|
||||
/// </summary>
|
||||
/// <param name="MangaId"><see cref="Manga"/> with <paramref name="MangaId"/></param>
|
||||
/// <param name="MangaConnectorName"><see cref="API.MangaConnectors.MangaConnector"/> with <paramref name="MangaConnectorName"/></param>
|
||||
/// <param name="MangaConnectorName"><see cref="MangaConnector"/> with <paramref name="MangaConnectorName"/></param>
|
||||
/// <param name="IsRequested">true to mark as requested, false to mark as not-requested</param>
|
||||
/// <response code="200"></response>
|
||||
/// <response code="404"><paramref name="MangaId"/> or <paramref name="MangaConnectorName"/> not found</response>
|
||||
/// <response code="412"><see cref="Manga"/> was not linked to <see cref="API.MangaConnectors.MangaConnector"/>, so nothing changed</response>
|
||||
/// <response code="428"><see cref="Manga"/> is not linked to <see cref="API.MangaConnectors.MangaConnector"/> yet. Search for <see cref="Manga"/> on <see cref="API.MangaConnectors.MangaConnector"/> first (to create a <see cref="MangaConnectorId{T}"/>).</response>
|
||||
/// <response code="412"><see cref="Manga"/> was not linked to <see cref="MangaConnector"/>, so nothing changed</response>
|
||||
/// <response code="428"><see cref="Manga"/> is not linked to <see cref="MangaConnector"/> yet. Search for <see cref="Manga"/> on <see cref="MangaConnector"/> first (to create a <see cref="MangaConnectorId{T}"/>).</response>
|
||||
/// <response code="500">Error during Database Operation</response>
|
||||
[HttpPost("{MangaId}/SetAsDownloadFrom/{MangaConnectorName}/{IsRequested}")]
|
||||
[ProducesResponseType(Status200OK)]
|
||||
@@ -469,50 +397,51 @@ public class MangaController(MangaContext context) : Controller
|
||||
[ProducesResponseType<string>(Status412PreconditionFailed, "text/plain")]
|
||||
[ProducesResponseType<string>(Status428PreconditionRequired, "text/plain")]
|
||||
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
|
||||
public async Task<Results<Ok, NotFound<string>, StatusCodeHttpResult, InternalServerError<string>>> MarkAsRequested(string MangaId, string MangaConnectorName, bool IsRequested)
|
||||
public IActionResult MarkAsRequested(string MangaId, string MangaConnectorName, bool IsRequested)
|
||||
{
|
||||
if (await context.Mangas.FirstOrDefaultAsync(m => m.Key == MangaId, HttpContext.RequestAborted) is not { } _)
|
||||
return TypedResults.NotFound(nameof(MangaId));
|
||||
if(!Tranga.TryGetMangaConnector(MangaConnectorName, out API.MangaConnectors.MangaConnector? _))
|
||||
return TypedResults.NotFound(nameof(MangaConnectorName));
|
||||
if (context.Mangas.Find(MangaId) is null)
|
||||
return NotFound(nameof(MangaId));
|
||||
if(!Tranga.TryGetMangaConnector(MangaConnectorName, out MangaConnector? mangaConnector))
|
||||
return NotFound(nameof(MangaConnectorName));
|
||||
|
||||
if (context.MangaConnectorToManga
|
||||
.FirstOrDefault(id => id.MangaConnectorName == MangaConnectorName && id.ObjId == MangaId)
|
||||
is not { } mcId)
|
||||
{
|
||||
if(IsRequested)
|
||||
return TypedResults.StatusCode(Status428PreconditionRequired);
|
||||
return StatusCode(Status428PreconditionRequired, "Don't know how to download this Manga from MangaConnector");
|
||||
else
|
||||
return TypedResults.StatusCode(Status412PreconditionFailed);
|
||||
return StatusCode(Status412PreconditionFailed, "Not linked anyways.");
|
||||
}
|
||||
|
||||
mcId.UseForDownload = IsRequested;
|
||||
if(await context.Sync(HttpContext.RequestAborted) is { success: false } result)
|
||||
return TypedResults.InternalServerError(result.exceptionMessage);
|
||||
if(context.Sync() is { success: false } result)
|
||||
return StatusCode(Status500InternalServerError, result.exceptionMessage);
|
||||
|
||||
|
||||
DownloadCoverFromMangaconnectorWorker downloadCover = new(mcId);
|
||||
RetrieveMangaChaptersFromMangaconnectorWorker retrieveChapters = new(mcId, Tranga.Settings.DownloadLanguage);
|
||||
Tranga.AddWorkers([downloadCover, retrieveChapters]);
|
||||
|
||||
return TypedResults.Ok();
|
||||
return Ok();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initiate a search for <see cref="API.Schema.MangaContext.Manga"/> on a different <see cref="API.MangaConnectors.MangaConnector"/>
|
||||
/// Initiate a search for <see cref="Manga"/> on a different <see cref="MangaConnector"/>
|
||||
/// </summary>
|
||||
/// <param name="MangaId"><see cref="API.Schema.MangaContext.Manga"/> with <paramref name="MangaId"/></param>
|
||||
/// <param name="MangaConnectorName"><see cref="API.MangaConnectors.MangaConnector"/>.Name</param>
|
||||
/// <response code="200"><see cref="MinimalManga"/> exert of <see cref="Schema.MangaContext.Manga"/></response>
|
||||
/// <response code="404"><see cref="API.MangaConnectors.MangaConnector"/> with Name not found</response>
|
||||
/// <response code="412"><see cref="API.MangaConnectors.MangaConnector"/> with Name is disabled</response>
|
||||
/// <param name="MangaId"><see cref="Manga"/> with <paramref name="MangaId"/></param>
|
||||
/// <param name="MangaConnectorName"><see cref="MangaConnector"/>.Name</param>
|
||||
/// <response code="200"></response>
|
||||
/// <response code="404"><see cref="MangaConnector"/> with Name not found</response>
|
||||
/// <response code="412"><see cref="MangaConnector"/> with Name is disabled</response>
|
||||
[HttpPost("{MangaId}/SearchOn/{MangaConnectorName}")]
|
||||
[ProducesResponseType<List<MinimalManga>>(Status200OK, "application/json")]
|
||||
[ProducesResponseType<string>(Status404NotFound, "text/plain")]
|
||||
[ProducesResponseType<Manga[]>(Status200OK, "application/json")]
|
||||
[ProducesResponseType(Status404NotFound)]
|
||||
[ProducesResponseType(Status406NotAcceptable)]
|
||||
public async Task<Results<Ok<List<MinimalManga>>, NotFound<string>, StatusCodeHttpResult>> SearchOnDifferentConnector (string MangaId, string MangaConnectorName)
|
||||
public IActionResult SearchOnDifferentConnector(string MangaId, string MangaConnectorName)
|
||||
{
|
||||
if (await context.Mangas.FirstOrDefaultAsync(m => m.Key == MangaId, HttpContext.RequestAborted) is not { } manga)
|
||||
return TypedResults.NotFound(nameof(MangaId));
|
||||
if (context.Mangas.Find(MangaId) is not { } manga)
|
||||
return NotFound(nameof(MangaId));
|
||||
|
||||
return new SearchController(context).SearchManga(MangaConnectorName, manga.Name);
|
||||
}
|
||||
@@ -523,27 +452,14 @@ public class MangaController(MangaContext context) : Controller
|
||||
/// <param name="AuthorId"><see cref="Author"/>.Key</param>
|
||||
/// <response code="200"></response>
|
||||
/// <response code="404"><see cref="Author"/> with <paramref name="AuthorId"/></response>
|
||||
/// /// <response code="500">Error during Database Operation</response>
|
||||
[HttpGet("WithAuthorId/{AuthorId}")]
|
||||
[ProducesResponseType<List<Manga>>(Status200OK, "application/json")]
|
||||
[ProducesResponseType<string>(Status404NotFound, "text/plain")]
|
||||
public async Task<Results<Ok<List<Manga>>, NotFound<string>, InternalServerError>> GetMangaWithAuthorIds (string AuthorId)
|
||||
[ProducesResponseType<Manga[]>(Status200OK, "application/json")]
|
||||
public IActionResult GetMangaWithAuthorIds(string AuthorId)
|
||||
{
|
||||
if (await context.Authors.FirstOrDefaultAsync(a => a.Key == AuthorId, HttpContext.RequestAborted) is not { } _)
|
||||
return TypedResults.NotFound(nameof(AuthorId));
|
||||
if (context.Authors.Find(AuthorId) is not { } author)
|
||||
return NotFound();
|
||||
|
||||
if (await context.MangaIncludeAll().Where(m => m.Authors.Any(a => a.Key == AuthorId)).ToListAsync(HttpContext.RequestAborted) is not { } result)
|
||||
return TypedResults.InternalServerError();
|
||||
|
||||
return TypedResults.Ok(result.Select(m =>
|
||||
{
|
||||
IEnumerable<MangaConnectorId> ids = m.MangaConnectorIds.Select(id => new MangaConnectorId(id.Key, id.MangaConnectorName, id.ObjId, id.WebsiteUrl, id.UseForDownload));
|
||||
IEnumerable<Author> authors = m.Authors.Select(a => new Author(a.Key, a.AuthorName));
|
||||
IEnumerable<string> tags = m.MangaTags.Select(t => t.Tag);
|
||||
IEnumerable<Link> links = m.Links.Select(l => new Link(l.Key, l.LinkProvider, l.LinkUrl));
|
||||
IEnumerable<AltTitle> altTitles = m.AltTitles.Select(a => new AltTitle(a.Language, a.Title));
|
||||
return new Manga(m.Key, m.Name, m.Description, m.ReleaseStatus, ids, m.IgnoreChaptersBefore, m.Year, m.OriginalLanguage, m.ChapterIds, authors, tags, links, altTitles, m.LibraryId);
|
||||
}).ToList());
|
||||
return Ok(context.Mangas.Where(m => m.Authors.Contains(author)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -552,28 +468,13 @@ public class MangaController(MangaContext context) : Controller
|
||||
/// <param name="Tag"><see cref="Tag"/>.Tag</param>
|
||||
/// <response code="200"></response>
|
||||
/// <response code="404"><see cref="Tag"/> not found</response>
|
||||
/// <response code="500">Error during Database Operation</response>
|
||||
[HttpGet("WithTag/{Tag}")]
|
||||
[ProducesResponseType<Manga[]>(Status200OK, "application/json")]
|
||||
[ProducesResponseType<string>(Status404NotFound, "text/plain")]
|
||||
[ProducesResponseType(Status500InternalServerError)]
|
||||
public async Task<Results<Ok<List<Manga>>, NotFound<string>, InternalServerError>> GetMangasWithTag (string Tag)
|
||||
public IActionResult GetMangasWithTag(string Tag)
|
||||
{
|
||||
if (await context.Tags.FirstOrDefaultAsync(t => t.Tag == Tag, HttpContext.RequestAborted) is not { } tag)
|
||||
return TypedResults.NotFound(nameof(Tag));
|
||||
if (context.Tags.Find(Tag) is not { } tag)
|
||||
return NotFound();
|
||||
|
||||
|
||||
if (await context.MangaIncludeAll().Where(m => m.MangaTags.Any(t => t.Tag.Equals(tag))) .ToListAsync(HttpContext.RequestAborted) is not { } result)
|
||||
return TypedResults.InternalServerError();
|
||||
|
||||
return TypedResults.Ok(result.Select(m =>
|
||||
{
|
||||
IEnumerable<MangaConnectorId> ids = m.MangaConnectorIds.Select(id => new MangaConnectorId(id.Key, id.MangaConnectorName, id.ObjId, id.WebsiteUrl, id.UseForDownload));
|
||||
IEnumerable<Author> authors = m.Authors.Select(a => new Author(a.Key, a.AuthorName));
|
||||
IEnumerable<string> tags = m.MangaTags.Select(t => t.Tag);
|
||||
IEnumerable<Link> links = m.Links.Select(l => new Link(l.Key, l.LinkProvider, l.LinkUrl));
|
||||
IEnumerable<AltTitle> altTitles = m.AltTitles.Select(a => new AltTitle(a.Language, a.Title));
|
||||
return new Manga(m.Key, m.Name, m.Description, m.ReleaseStatus, ids, m.IgnoreChaptersBefore, m.Year, m.OriginalLanguage, m.ChapterIds, authors, tags, links, altTitles, m.LibraryId);
|
||||
}).ToList());
|
||||
return Ok(context.Mangas.Where(m => m.MangaTags.Contains(tag)));
|
||||
}
|
||||
}
|
@@ -1,10 +1,8 @@
|
||||
using API.Schema.MangaContext;
|
||||
using API.Schema.MangaContext.MetadataFetchers;
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Http.HttpResults;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using static Microsoft.AspNetCore.Http.StatusCodes;
|
||||
// ReSharper disable InconsistentNaming
|
||||
|
||||
@@ -20,26 +18,21 @@ public class MetadataFetcherController(MangaContext context) : Controller
|
||||
/// </summary>
|
||||
/// <response code="200">Names of <see cref="MetadataFetcher"/> (Metadata-Sites)</response>
|
||||
[HttpGet]
|
||||
[ProducesResponseType<List<string>>(Status200OK, "application/json")]
|
||||
public Ok<List<string>> GetConnectors ()
|
||||
[ProducesResponseType<string[]>(Status200OK, "application/json")]
|
||||
public IActionResult GetConnectors()
|
||||
{
|
||||
return TypedResults.Ok(Tranga.MetadataFetchers.Select(m => m.Name).ToList());
|
||||
return Ok(Tranga.MetadataFetchers.Select(m => m.Name).ToArray());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns all <see cref="MetadataEntry"/>
|
||||
/// </summary>
|
||||
/// <response code="200"></response>
|
||||
/// <response code="500">Error during Database Operation</response>
|
||||
[HttpGet("Links")]
|
||||
[ProducesResponseType<List<MetadataEntry>>(Status200OK, "application/json")]
|
||||
[ProducesResponseType(Status500InternalServerError)]
|
||||
public async Task<Results<Ok<List<MetadataEntry>>, InternalServerError>> GetLinkedEntries ()
|
||||
[ProducesResponseType<MetadataEntry[]>(Status200OK, "application/json")]
|
||||
public IActionResult GetLinkedEntries()
|
||||
{
|
||||
if (await context.MetadataEntries.ToListAsync() is not { } result)
|
||||
return TypedResults.InternalServerError();
|
||||
|
||||
return TypedResults.Ok(result);
|
||||
return Ok(context.MetadataEntries.ToArray());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -54,16 +47,16 @@ public class MetadataFetcherController(MangaContext context) : Controller
|
||||
[HttpPost("{MetadataFetcherName}/SearchManga/{MangaId}")]
|
||||
[ProducesResponseType<MetadataSearchResult[]>(Status200OK, "application/json")]
|
||||
[ProducesResponseType(Status400BadRequest)]
|
||||
[ProducesResponseType<string>(Status404NotFound, "text/plain")]
|
||||
public async Task<Results<Ok<List<MetadataSearchResult>>, BadRequest, NotFound<string>>> SearchMangaMetadata(string MangaId, string MetadataFetcherName, [FromBody (EmptyBodyBehavior = EmptyBodyBehavior.Allow)]string? searchTerm = null)
|
||||
[ProducesResponseType(Status404NotFound)]
|
||||
public IActionResult SearchMangaMetadata(string MangaId, string MetadataFetcherName, [FromBody(EmptyBodyBehavior = EmptyBodyBehavior.Allow)]string? searchTerm = null)
|
||||
{
|
||||
if (await context.Mangas.FirstOrDefaultAsync(m => m.Key == MangaId, HttpContext.RequestAborted) is not { } manga)
|
||||
return TypedResults.NotFound(nameof(MangaId));
|
||||
if(context.Mangas.Find(MangaId) is not { } manga)
|
||||
return NotFound();
|
||||
if(Tranga.MetadataFetchers.FirstOrDefault(f => f.Name == MetadataFetcherName) is not { } fetcher)
|
||||
return TypedResults.BadRequest();
|
||||
return BadRequest();
|
||||
|
||||
MetadataSearchResult[] searchResults = searchTerm is null ? fetcher.SearchMetadataEntry(manga) : fetcher.SearchMetadataEntry(searchTerm);
|
||||
return TypedResults.Ok(searchResults.ToList());
|
||||
return Ok(searchResults);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -79,21 +72,21 @@ public class MetadataFetcherController(MangaContext context) : Controller
|
||||
[HttpPost("{MetadataFetcherName}/Link/{MangaId}")]
|
||||
[ProducesResponseType<MetadataEntry>(Status200OK, "application/json")]
|
||||
[ProducesResponseType(Status400BadRequest)]
|
||||
[ProducesResponseType<string>(Status404NotFound, "text/plain")]
|
||||
[ProducesResponseType(Status404NotFound)]
|
||||
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
|
||||
public async Task<Results<Ok<MetadataEntry>, BadRequest, NotFound<string>, InternalServerError<string>>> LinkMangaMetadata (string MangaId, string MetadataFetcherName, [FromBody]string Identifier)
|
||||
public IActionResult LinkMangaMetadata(string MangaId, string MetadataFetcherName, [FromBody]string Identifier)
|
||||
{
|
||||
if (await context.Mangas.FirstOrDefaultAsync(m => m.Key == MangaId, HttpContext.RequestAborted) is not { } manga)
|
||||
return TypedResults.NotFound(nameof(MangaId));
|
||||
if(context.Mangas.Find(MangaId) is not { } manga)
|
||||
return NotFound();
|
||||
if(Tranga.MetadataFetchers.FirstOrDefault(f => f.Name == MetadataFetcherName) is not { } fetcher)
|
||||
return TypedResults.BadRequest();
|
||||
return BadRequest();
|
||||
|
||||
MetadataEntry entry = fetcher.CreateMetadataEntry(manga, Identifier);
|
||||
context.MetadataEntries.Add(entry);
|
||||
|
||||
if(await context.Sync(HttpContext.RequestAborted) is { success: false } result)
|
||||
return TypedResults.InternalServerError(result.exceptionMessage);
|
||||
return TypedResults.Ok(entry);
|
||||
if(context.Sync() is { } errorMessage)
|
||||
return StatusCode(Status500InternalServerError, errorMessage);
|
||||
return Ok(entry);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -107,23 +100,22 @@ public class MetadataFetcherController(MangaContext context) : Controller
|
||||
[HttpPost("{MetadataFetcherName}/Unlink/{MangaId}")]
|
||||
[ProducesResponseType(Status200OK)]
|
||||
[ProducesResponseType(Status400BadRequest)]
|
||||
[ProducesResponseType<string>(Status404NotFound, "text/plain")]
|
||||
[ProducesResponseType(Status412PreconditionFailed)]
|
||||
[ProducesResponseType(Status404NotFound)]
|
||||
[ProducesResponseType<string>(Status412PreconditionFailed, "text/plain")]
|
||||
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
|
||||
public async Task<Results<Ok, BadRequest, NotFound<string>, InternalServerError<string>, StatusCodeHttpResult>> UnlinkMangaMetadata (string MangaId, string MetadataFetcherName)
|
||||
public IActionResult UnlinkMangaMetadata(string MangaId, string MetadataFetcherName)
|
||||
{
|
||||
if (await context.Mangas.FirstOrDefaultAsync(m => m.Key == MangaId, HttpContext.RequestAborted) is not { } _)
|
||||
return TypedResults.NotFound(nameof(MangaId));
|
||||
if(context.Mangas.Find(MangaId) is null)
|
||||
return NotFound();
|
||||
if(Tranga.MetadataFetchers.FirstOrDefault(f => f.Name == MetadataFetcherName) is null)
|
||||
return TypedResults.BadRequest();
|
||||
if (context.MetadataEntries.FirstOrDefault(e =>
|
||||
e.MangaId == MangaId && e.MetadataFetcherName == MetadataFetcherName) is not { } entry)
|
||||
return TypedResults.StatusCode(Status412PreconditionFailed);
|
||||
return BadRequest();
|
||||
if(context.MetadataEntries.FirstOrDefault(e => e.MangaId == MangaId && e.MetadataFetcherName == MetadataFetcherName) is not { } entry)
|
||||
return StatusCode(Status412PreconditionFailed, "No entry found");
|
||||
|
||||
context.Remove(entry);
|
||||
|
||||
if(await context.Sync(HttpContext.RequestAborted) is { success: false } result)
|
||||
return TypedResults.InternalServerError(result.exceptionMessage);
|
||||
return TypedResults.Ok();
|
||||
if(context.Sync() is { success: false } result)
|
||||
return StatusCode(Status500InternalServerError, result.exceptionMessage);
|
||||
return Ok();
|
||||
}
|
||||
}
|
@@ -1,11 +1,9 @@
|
||||
using System.Text;
|
||||
using API.Controllers.DTOs;
|
||||
using API.Controllers.Requests;
|
||||
using API.APIEndpointRecords;
|
||||
using API.Schema.NotificationsContext;
|
||||
using API.Schema.NotificationsContext.NotificationConnectors;
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Http.HttpResults;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using static Microsoft.AspNetCore.Http.StatusCodes;
|
||||
// ReSharper disable InconsistentNaming
|
||||
|
||||
@@ -21,18 +19,12 @@ public class NotificationConnectorController(NotificationsContext context) : Con
|
||||
/// Gets all configured <see cref="NotificationConnector"/>
|
||||
/// </summary>
|
||||
/// <response code="200"></response>
|
||||
/// <response code="500">Error during Database Operation</response>
|
||||
[HttpGet]
|
||||
[ProducesResponseType<List<NotificationConnector>>(Status200OK, "application/json")]
|
||||
[ProducesResponseType(Status500InternalServerError)]
|
||||
public async Task<Results<Ok<List<NotificationConnector>>, InternalServerError>> GetAllConnectors ()
|
||||
[ProducesResponseType<NotificationConnector[]>(Status200OK, "application/json")]
|
||||
public IActionResult GetAllConnectors()
|
||||
{
|
||||
if(await context.NotificationConnectors.ToListAsync(HttpContext.RequestAborted) is not { } result)
|
||||
return TypedResults.InternalServerError();
|
||||
|
||||
List<NotificationConnector> notificationConnectors = result.Select(n => new NotificationConnector(n.Name, n.Url, n.HttpMethod, n.Body, n.Headers)).ToList();
|
||||
|
||||
return TypedResults.Ok(notificationConnectors);
|
||||
return Ok(context.NotificationConnectors.ToArray());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -43,15 +35,13 @@ public class NotificationConnectorController(NotificationsContext context) : Con
|
||||
/// <response code="404"><see cref="NotificationConnector"/> with <paramref name="Name"/> not found</response>
|
||||
[HttpGet("{Name}")]
|
||||
[ProducesResponseType<NotificationConnector>(Status200OK, "application/json")]
|
||||
[ProducesResponseType<string>(Status404NotFound, "text/plain")]
|
||||
public async Task<Results<Ok<NotificationConnector>, NotFound<string>>> GetConnector (string Name)
|
||||
[ProducesResponseType(Status404NotFound)]
|
||||
public IActionResult GetConnector(string Name)
|
||||
{
|
||||
if (await context.NotificationConnectors.FirstOrDefaultAsync(c => c.Name == Name, HttpContext.RequestAborted) is not { } connector)
|
||||
return TypedResults.NotFound(nameof(Name));
|
||||
if(context.NotificationConnectors.Find(Name) is not { } connector)
|
||||
return NotFound();
|
||||
|
||||
NotificationConnector notificationConnector = new NotificationConnector(connector.Name, connector.Url, connector.HttpMethod, connector.Body, connector.Headers);
|
||||
|
||||
return TypedResults.Ok(notificationConnector);
|
||||
return Ok(connector);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -61,20 +51,16 @@ public class NotificationConnectorController(NotificationsContext context) : Con
|
||||
/// <response code="200">ID of the new <see cref="NotificationConnector"/></response>
|
||||
/// <response code="500">Error during Database Operation</response>
|
||||
[HttpPut]
|
||||
[ProducesResponseType<string>(Status201Created, "text/plain")]
|
||||
[ProducesResponseType<string>(Status200OK, "text/plain")]
|
||||
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
|
||||
public async Task<Results<Created<string>, InternalServerError<string>>> CreateConnector ([FromBody]CreateNotificationConnectorRecord requestData)
|
||||
public IActionResult CreateConnector([FromBody]NotificationConnector notificationConnector)
|
||||
{
|
||||
// TODO validate data
|
||||
API.Schema.NotificationsContext.NotificationConnectors.NotificationConnector newConnector =
|
||||
new(requestData.Name, requestData.Url, requestData.Headers, requestData.HttpMethod, requestData.Body);
|
||||
context.NotificationConnectors.Add(notificationConnector);
|
||||
context.Notifications.Add(new ("Added new Notification Connector!", notificationConnector.Name, NotificationUrgency.High));
|
||||
|
||||
context.NotificationConnectors.Add(newConnector);
|
||||
context.Notifications.Add(new ("Added new Notification Connector!", newConnector.Name, NotificationUrgency.High));
|
||||
|
||||
if(await context.Sync(HttpContext.RequestAborted) is { success: false } result)
|
||||
return TypedResults.InternalServerError(result.exceptionMessage);
|
||||
return TypedResults.Created(string.Empty, newConnector.Name);
|
||||
if(context.Sync() is { success: false } result)
|
||||
return StatusCode(Status500InternalServerError, result.exceptionMessage);
|
||||
return Ok(notificationConnector.Name);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -84,17 +70,18 @@ public class NotificationConnectorController(NotificationsContext context) : Con
|
||||
/// <response code="200">ID of the new <see cref="NotificationConnector"/></response>
|
||||
/// <response code="500">Error during Database Operation</response>
|
||||
[HttpPut("Gotify")]
|
||||
[ProducesResponseType<string>(Status201Created, "text/plain")]
|
||||
[ProducesResponseType<string>(Status200OK, "text/plain")]
|
||||
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
|
||||
public async Task<Results<Created<string>, InternalServerError<string>>> CreateGotifyConnector ([FromBody]CreateGotifyConnectorRecord createGotifyConnectorData)
|
||||
public IActionResult CreateGotifyConnector([FromBody]GotifyRecord gotifyData)
|
||||
{
|
||||
//TODO Validate Data
|
||||
CreateNotificationConnectorRecord gotifyConnector = new (createGotifyConnectorData.Name,
|
||||
createGotifyConnectorData.Url,
|
||||
|
||||
NotificationConnector gotifyConnector = new (gotifyData.Name,
|
||||
gotifyData.Endpoint,
|
||||
new Dictionary<string, string>() { { "X-Gotify-Key", gotifyData.AppToken } },
|
||||
"POST",
|
||||
$"{{\"message\": \"%text\", \"title\": \"%title\", \"Priority\": {createGotifyConnectorData.Priority}}}",
|
||||
new () { { "X-Gotify-Key", createGotifyConnectorData.AppToken } });
|
||||
return await CreateConnector(gotifyConnector);
|
||||
$"{{\"message\": \"%text\", \"title\": \"%title\", \"Priority\": {gotifyData.Priority}}}");
|
||||
return CreateConnector(gotifyConnector);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -104,20 +91,24 @@ public class NotificationConnectorController(NotificationsContext context) : Con
|
||||
/// <response code="200">ID of the new <see cref="NotificationConnector"/></response>
|
||||
/// <response code="500">Error during Database Operation</response>
|
||||
[HttpPut("Ntfy")]
|
||||
[ProducesResponseType<string>(Status201Created, "text/plain")]
|
||||
[ProducesResponseType<string>(Status200OK, "text/plain")]
|
||||
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
|
||||
public async Task<Results<Created<string>, InternalServerError<string>>> CreateNtfyConnector ([FromBody]CreateNtfyConnectorRecord createNtfyConnectorRecord)
|
||||
public IActionResult CreateNtfyConnector([FromBody]NtfyRecord ntfyRecord)
|
||||
{
|
||||
//TODO Validate Data
|
||||
string authHeader = "Basic " + Convert.ToBase64String(Encoding.UTF8.GetBytes($"{createNtfyConnectorRecord.Username}:{createNtfyConnectorRecord.Password}"));
|
||||
|
||||
string authHeader = "Basic " + Convert.ToBase64String(Encoding.UTF8.GetBytes($"{ntfyRecord.Username}:{ntfyRecord.Password}"));
|
||||
string auth = Convert.ToBase64String(Encoding.UTF8.GetBytes(authHeader)).Replace("=","");
|
||||
|
||||
CreateNotificationConnectorRecord ntfyConnector = new (createNtfyConnectorRecord.Name,
|
||||
$"{createNtfyConnectorRecord.Url}?auth={auth}",
|
||||
NotificationConnector ntfyConnector = new (ntfyRecord.Name,
|
||||
$"{ntfyRecord.Endpoint}?auth={auth}",
|
||||
new Dictionary<string, string>()
|
||||
{
|
||||
{"Authorization", auth}
|
||||
},
|
||||
"POST",
|
||||
$"{{\"message\": \"%text\", \"title\": \"%title\", \"Priority\": {createNtfyConnectorRecord.Priority} \"Topic\": \"{createNtfyConnectorRecord.Topic}\"}}",
|
||||
new () {{"Authorization", auth}});
|
||||
return await CreateConnector(ntfyConnector);
|
||||
$"{{\"message\": \"%text\", \"title\": \"%title\", \"Priority\": {ntfyRecord.Priority} \"Topic\": \"{ntfyRecord.Topic}\"}}");
|
||||
return CreateConnector(ntfyConnector);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -127,17 +118,18 @@ public class NotificationConnectorController(NotificationsContext context) : Con
|
||||
/// <response code="200">ID of the new <see cref="NotificationConnector"/></response>
|
||||
/// <response code="500">Error during Database Operation</response>
|
||||
[HttpPut("Pushover")]
|
||||
[ProducesResponseType<string>(Status201Created, "text/plain")]
|
||||
[ProducesResponseType<string>(Status200OK, "text/plain")]
|
||||
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
|
||||
public async Task<Results<Created<string>, InternalServerError<string>>> CreatePushoverConnector ([FromBody]CreatePushoverConnectorRecord createPushoverConnectorRecord)
|
||||
public IActionResult CreatePushoverConnector([FromBody]PushoverRecord pushoverRecord)
|
||||
{
|
||||
//TODO Validate Data
|
||||
CreateNotificationConnectorRecord pushoverConnector = new (createPushoverConnectorRecord.Name,
|
||||
|
||||
NotificationConnector pushoverConnector = new (pushoverRecord.Name,
|
||||
$"https://api.pushover.net/1/messages.json",
|
||||
new Dictionary<string, string>(),
|
||||
"POST",
|
||||
$"{{\"token\": \"{createPushoverConnectorRecord.AppToken}\", \"user\": \"{createPushoverConnectorRecord.Username}\", \"message:\":\"%text\", \"%title\" }}",
|
||||
new ());
|
||||
return await CreateConnector(pushoverConnector);
|
||||
$"{{\"token\": \"{pushoverRecord.AppToken}\", \"user\": \"{pushoverRecord.User}\", \"message:\":\"%text\", \"%title\" }}");
|
||||
return CreateConnector(pushoverConnector);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -149,17 +141,17 @@ public class NotificationConnectorController(NotificationsContext context) : Con
|
||||
/// <response code="500">Error during Database Operation</response>
|
||||
[HttpDelete("{Name}")]
|
||||
[ProducesResponseType(Status200OK)]
|
||||
[ProducesResponseType<string>(Status404NotFound, "text/plain")]
|
||||
[ProducesResponseType(Status404NotFound)]
|
||||
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
|
||||
public async Task<Results<Ok, NotFound<string>, InternalServerError<string>>> DeleteConnector (string Name)
|
||||
public IActionResult DeleteConnector(string Name)
|
||||
{
|
||||
if (await context.NotificationConnectors.FirstOrDefaultAsync(c => c.Name == Name, HttpContext.RequestAborted) is not { } connector)
|
||||
return TypedResults.NotFound(nameof(Name));
|
||||
if(context.NotificationConnectors.Find(Name) is not { } connector)
|
||||
return NotFound();
|
||||
|
||||
context.NotificationConnectors.Remove(connector);
|
||||
|
||||
if(await context.Sync(HttpContext.RequestAborted) is { success: false } result)
|
||||
return TypedResults.InternalServerError(result.exceptionMessage);
|
||||
return TypedResults.Ok();
|
||||
if(context.Sync() is { success: false } result)
|
||||
return StatusCode(Status500InternalServerError, result.exceptionMessage);
|
||||
return Ok();
|
||||
}
|
||||
}
|
@@ -1,14 +1,9 @@
|
||||
using API.Controllers.DTOs;
|
||||
using API.MangaConnectors;
|
||||
using API.Schema.MangaContext;
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Http.HttpResults;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Soenneker.Utils.String.NeedlemanWunsch;
|
||||
using static Microsoft.AspNetCore.Http.StatusCodes;
|
||||
using Author = API.Controllers.DTOs.Author;
|
||||
using Chapter = API.Controllers.DTOs.Chapter;
|
||||
|
||||
// ReSharper disable InconsistentNaming
|
||||
|
||||
namespace API.Controllers;
|
||||
@@ -26,13 +21,13 @@ public class QueryController(MangaContext context) : Controller
|
||||
/// <response code="404"><see cref="Author"/> with <paramref name="AuthorId"/> not found</response>
|
||||
[HttpGet("Author/{AuthorId}")]
|
||||
[ProducesResponseType<Author>(Status200OK, "application/json")]
|
||||
[ProducesResponseType<string>(Status404NotFound, "text/plain")]
|
||||
public async Task<Results<Ok<Author>, NotFound<string>>> GetAuthor (string AuthorId)
|
||||
[ProducesResponseType(Status404NotFound)]
|
||||
public IActionResult GetAuthor(string AuthorId)
|
||||
{
|
||||
if (await context.Authors.FirstOrDefaultAsync(a => a.Key == AuthorId, HttpContext.RequestAborted) is not { } author)
|
||||
return TypedResults.NotFound(nameof(AuthorId));
|
||||
if (context.Authors.Find(AuthorId) is not { } author)
|
||||
return NotFound();
|
||||
|
||||
return TypedResults.Ok(new Author(author.Key, author.AuthorName));
|
||||
return Ok(author);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -43,15 +38,13 @@ public class QueryController(MangaContext context) : Controller
|
||||
/// <response code="404"><see cref="Chapter"/> with <paramref name="ChapterId"/> not found</response>
|
||||
[HttpGet("Chapter/{ChapterId}")]
|
||||
[ProducesResponseType<Chapter>(Status200OK, "application/json")]
|
||||
[ProducesResponseType<string>(Status404NotFound, "text/plain")]
|
||||
public async Task<Results<Ok<Chapter>, NotFound<string>>> GetChapter (string ChapterId)
|
||||
[ProducesResponseType(Status404NotFound)]
|
||||
public IActionResult GetChapter(string ChapterId)
|
||||
{
|
||||
if (await context.Chapters.FirstOrDefaultAsync(c => c.Key == ChapterId, HttpContext.RequestAborted) is not { } chapter)
|
||||
return TypedResults.NotFound(nameof(ChapterId));
|
||||
if (context.Chapters.Find(ChapterId) is not { } chapter)
|
||||
return NotFound();
|
||||
|
||||
IEnumerable<MangaConnectorId> ids = chapter.MangaConnectorIds.Select(id =>
|
||||
new MangaConnectorId(id.Key, id.MangaConnectorName, id.ObjId, id.WebsiteUrl, id.UseForDownload));
|
||||
return TypedResults.Ok(new Chapter(chapter.Key, chapter.ParentMangaId, chapter.VolumeNumber, chapter.ChapterNumber, chapter.Title,ids, chapter.Downloaded));
|
||||
return Ok(chapter);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -61,46 +54,49 @@ public class QueryController(MangaContext context) : Controller
|
||||
/// <response code="200"></response>
|
||||
/// <response code="404"><see cref="MangaConnectorId{Manga}"/> with <paramref name="MangaConnectorIdId"/> not found</response>
|
||||
[HttpGet("Manga/MangaConnectorId/{MangaConnectorIdId}")]
|
||||
[ProducesResponseType<MangaConnectorId>(Status200OK, "application/json")]
|
||||
[ProducesResponseType<string>(Status404NotFound, "text/plain")]
|
||||
public async Task<Results<Ok<MangaConnectorId>, NotFound<string>>> GetMangaMangaConnectorId (string MangaConnectorIdId)
|
||||
[ProducesResponseType<MangaConnectorId<Manga>>(Status200OK, "application/json")]
|
||||
[ProducesResponseType(Status404NotFound)]
|
||||
public IActionResult GetMangaMangaConnectorId(string MangaConnectorIdId)
|
||||
{
|
||||
if (await context.MangaConnectorToManga.FirstOrDefaultAsync(c => c.Key == MangaConnectorIdId, HttpContext.RequestAborted) is not { } mcIdManga)
|
||||
return TypedResults.NotFound(nameof(MangaConnectorIdId));
|
||||
if(context.MangaConnectorToManga.Find(MangaConnectorIdId) is not { } mcIdManga)
|
||||
return NotFound();
|
||||
|
||||
MangaConnectorId result = new (mcIdManga.Key, mcIdManga.MangaConnectorName, mcIdManga.ObjId, mcIdManga.WebsiteUrl, mcIdManga.UseForDownload);
|
||||
|
||||
return TypedResults.Ok(result);
|
||||
return Ok(mcIdManga);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns <see cref="Schema.MangaContext.Manga"/> with names similar to <see cref="Schema.MangaContext.Manga"/> (identified by <paramref name="MangaId"/>)
|
||||
/// Returns all <see cref="Manga"/> that are being downloaded from at least one <see cref="MangaConnector"/>
|
||||
/// </summary>
|
||||
/// <param name="MangaId">Key of <see cref="Schema.MangaContext.Manga"/></param>
|
||||
/// <response code="200"></response>
|
||||
/// <response code="404"><see cref="Schema.MangaContext.Manga"/> with <paramref name="MangaId"/> not found</response>
|
||||
/// <response code="500">Error during Database Operation</response>
|
||||
[HttpGet("Manga/{MangaId}/SimilarName")]
|
||||
[ProducesResponseType<List<string>>(Status200OK, "application/json")]
|
||||
[ProducesResponseType<string>(Status404NotFound, "text/plain")]
|
||||
[ProducesResponseType(Status500InternalServerError)]
|
||||
public async Task<Results<Ok<List<string>>, NotFound<string>, InternalServerError>> GetSimilarManga (string MangaId)
|
||||
[HttpGet("Manga/Downloading")]
|
||||
[ProducesResponseType<Manga[]>(Status200OK, "application/json")]
|
||||
public IActionResult GetMangaDownloading()
|
||||
{
|
||||
if (await context.Mangas.FirstOrDefaultAsync(m => m.Key == MangaId, HttpContext.RequestAborted) is not { } manga)
|
||||
return TypedResults.NotFound(nameof(MangaId));
|
||||
Manga[] ret = context.MangaIncludeAll()
|
||||
.Where(m => m.MangaConnectorIds.Any(id => id.UseForDownload))
|
||||
.ToArray();
|
||||
return Ok(ret);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns <see cref="Manga"/> with names similar to <see cref="Manga"/> (identified by <paramref name="MangaId"/>
|
||||
/// </summary>
|
||||
/// <param name="MangaId">Key of <see cref="Manga"/></param>
|
||||
/// <response code="200"></response>
|
||||
/// <response code="404"><see cref="Manga"/> with <paramref name="MangaId"/> not found</response>
|
||||
[HttpGet("Manga/{MangaId}/SimilarName")]
|
||||
[ProducesResponseType<string[]>(Status200OK, "application/json")]
|
||||
[ProducesResponseType(Status404NotFound)]
|
||||
public IActionResult GetSimilarManga(string MangaId)
|
||||
{
|
||||
if (context.Mangas.Find(MangaId) is not { } manga)
|
||||
return NotFound();
|
||||
string name = manga.Name;
|
||||
|
||||
if (await context.Mangas.Where(m => m.Key != MangaId)
|
||||
.ToDictionaryAsync(m => m.Key, m => m.Name, HttpContext.RequestAborted) is not { } mangaNames)
|
||||
return TypedResults.InternalServerError();
|
||||
|
||||
List<string> similarIds = mangaNames
|
||||
Dictionary<string, string> mangaNames = context.Mangas.Where(m => m.Key != MangaId).ToDictionary(m => m.Key, m => m.Name);
|
||||
string[] similarIds = mangaNames
|
||||
.Where(kv => NeedlemanWunschStringUtil.CalculateSimilarityPercentage(name, kv.Value) > 0.8)
|
||||
.Select(kv => kv.Key)
|
||||
.ToList();
|
||||
|
||||
return TypedResults.Ok(similarIds);
|
||||
.Select(kv => kv.Key).ToArray();
|
||||
return Ok(similarIds);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -110,15 +106,13 @@ public class QueryController(MangaContext context) : Controller
|
||||
/// <response code="200"></response>
|
||||
/// <response code="404"><see cref="MangaConnectorId{Chapter}"/> with <paramref name="MangaConnectorIdId"/> not found</response>
|
||||
[HttpGet("Chapter/MangaConnectorId/{MangaConnectorIdId}")]
|
||||
[ProducesResponseType<MangaConnectorId>(Status200OK, "application/json")]
|
||||
[ProducesResponseType<string>(Status404NotFound, "text/plain")]
|
||||
public async Task<Results<Ok<MangaConnectorId>, NotFound<string>>> GetChapterMangaConnectorId (string MangaConnectorIdId)
|
||||
[ProducesResponseType<MangaConnectorId<Chapter>>(Status200OK, "application/json")]
|
||||
[ProducesResponseType(Status404NotFound)]
|
||||
public IActionResult GetChapterMangaConnectorId(string MangaConnectorIdId)
|
||||
{
|
||||
if (await context.MangaConnectorToManga.FirstOrDefaultAsync(c => c.Key == MangaConnectorIdId, HttpContext.RequestAborted) is not { } mcIdChapter)
|
||||
return TypedResults.NotFound(nameof(MangaConnectorIdId));
|
||||
if(context.MangaConnectorToChapter.Find(MangaConnectorIdId) is not { } mcIdChapter)
|
||||
return NotFound();
|
||||
|
||||
MangaConnectorId result = new(mcIdChapter.Key, mcIdChapter.MangaConnectorName, mcIdChapter.ObjId, mcIdChapter.WebsiteUrl, mcIdChapter.UseForDownload);
|
||||
|
||||
return TypedResults.Ok(result);
|
||||
return Ok(mcIdChapter);
|
||||
}
|
||||
}
|
@@ -1,37 +0,0 @@
|
||||
using System.ComponentModel;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace API.Controllers.Requests;
|
||||
|
||||
public record CreateGotifyConnectorRecord(string Name, string Url, string AppToken, int Priority)
|
||||
{
|
||||
/// <summary>
|
||||
/// The Name of the Notification Connector
|
||||
/// </summary>
|
||||
[Required]
|
||||
[Description("The Name of the Notification Connector")]
|
||||
public string Name { get; init; } = Name;
|
||||
|
||||
/// <summary>
|
||||
/// The Url of the Instance
|
||||
/// </summary>
|
||||
/// <remarks>Formatting placeholders: "%title" and "%text" will be replaced when notifications are sent</remarks>
|
||||
[Required]
|
||||
[Url]
|
||||
[Description("The Url of the Instance")]
|
||||
public string Url { get; internal set; } = Url;
|
||||
|
||||
/// <summary>
|
||||
/// The Apptoken used for authentication
|
||||
/// </summary>
|
||||
[Required]
|
||||
[Description("The Apptoken used for authentication")]
|
||||
public string AppToken { get; init; } = AppToken;
|
||||
|
||||
/// <summary>
|
||||
/// The Priority of Notifications
|
||||
/// </summary>
|
||||
[Required]
|
||||
[Description("The Priority of Notifications")]
|
||||
public int Priority { get; init; } = Priority;
|
||||
}
|
@@ -1,37 +0,0 @@
|
||||
using System.ComponentModel;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using API.Schema.LibraryContext.LibraryConnectors;
|
||||
|
||||
namespace API.Controllers.Requests;
|
||||
|
||||
public sealed record CreateLibraryConnectorRecord(LibraryType LibraryType, string Url, string Username, string Password)
|
||||
{
|
||||
/// <summary>
|
||||
/// The <see cref="LibraryType"/>
|
||||
/// </summary>
|
||||
[Required]
|
||||
[Description("The Library Type")]
|
||||
public LibraryType LibraryType { get; init; } = LibraryType;
|
||||
|
||||
/// <summary>
|
||||
/// The Url of the Library instance
|
||||
/// </summary>
|
||||
[Required]
|
||||
[Url]
|
||||
[Description("The Url of the Library instance")]
|
||||
public string Url { get; init; } = Url;
|
||||
|
||||
/// <summary>
|
||||
/// The Username to authenticate to the Library instance
|
||||
/// </summary>
|
||||
[Required]
|
||||
[Description("The Username to authenticate to the Library instance")]
|
||||
public string Username { get; init; } = Username;
|
||||
|
||||
/// <summary>
|
||||
/// The Password to authenticate to the Library instance
|
||||
/// </summary>
|
||||
[Required]
|
||||
[Description("The Password to authenticate to the Library instance")]
|
||||
public string Password { get; init; } = Password;
|
||||
}
|
@@ -1,21 +0,0 @@
|
||||
using System.ComponentModel;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace API.Controllers.Requests;
|
||||
|
||||
public sealed record CreateLibraryRecord(string BasePath, string LibraryName)
|
||||
{
|
||||
/// <summary>
|
||||
/// The directory Path of the library
|
||||
/// </summary>
|
||||
[Required]
|
||||
[Description("The directory Path of the library")]
|
||||
public string BasePath { get; init; } = BasePath;
|
||||
|
||||
/// <summary>
|
||||
/// The Name of the library
|
||||
/// </summary>
|
||||
[Required]
|
||||
[Description("The Name of the library")]
|
||||
public string LibraryName { get; init; } = LibraryName;
|
||||
}
|
@@ -1,46 +0,0 @@
|
||||
using System.ComponentModel;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace API.Controllers.Requests;
|
||||
|
||||
public record CreateNotificationConnectorRecord(string Name, string Url, string HttpMethod, string Body, Dictionary<string, string> Headers)
|
||||
{
|
||||
/// <summary>
|
||||
/// The Name of the Notification Connector
|
||||
/// </summary>
|
||||
[Required]
|
||||
[Description("The Name of the Notification Connector")]
|
||||
public string Name { get; init; } = Name;
|
||||
|
||||
/// <summary>
|
||||
/// The Url of the Instance
|
||||
/// </summary>
|
||||
/// <remarks>Formatting placeholders: "%title" and "%text" will be replaced when notifications are sent</remarks>
|
||||
[Required]
|
||||
[Url]
|
||||
[Description("The Url of the Instance")]
|
||||
public string Url { get; internal set; } = Url;
|
||||
|
||||
/// <summary>
|
||||
/// The HTTP Request Method to use for notifications
|
||||
/// </summary>
|
||||
[Required]
|
||||
[Description("The HTTP Request Method to use for notifications")]
|
||||
public string HttpMethod { get; internal set; } = HttpMethod;
|
||||
|
||||
/// <summary>
|
||||
/// The Request Body to use to send notifications
|
||||
/// </summary>
|
||||
/// <remarks>Formatting placeholders: "%title" and "%text" will be replaced when notifications are sent</remarks>
|
||||
[Required]
|
||||
[Description("The Request Body to use to send notifications")]
|
||||
public string Body { get; internal set; } = Body;
|
||||
|
||||
/// <summary>
|
||||
/// The Request Headers to use to send notifications
|
||||
/// </summary>
|
||||
/// <remarks>Formatting placeholders: "%title" and "%text" will be replaced when notifications are sent</remarks>
|
||||
[Required]
|
||||
[Description("The Request Headers to use to send notifications")]
|
||||
public Dictionary<string, string> Headers { get; internal set; } = Headers;
|
||||
}
|
@@ -1,51 +0,0 @@
|
||||
using System.ComponentModel;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace API.Controllers.Requests;
|
||||
|
||||
public record CreateNtfyConnectorRecord(string Name, string Url, string Username, string Password, string Topic, int Priority)
|
||||
{
|
||||
/// <summary>
|
||||
/// The Name of the Notification Connector
|
||||
/// </summary>
|
||||
[Required]
|
||||
[Description("The Name of the Notification Connector")]
|
||||
public string Name { get; init; } = Name;
|
||||
|
||||
/// <summary>
|
||||
/// The Url of the Instance
|
||||
/// </summary>
|
||||
/// <remarks>Formatting placeholders: "%title" and "%text" will be replaced when notifications are sent</remarks>
|
||||
[Required]
|
||||
[Url]
|
||||
[Description("The Url of the Instance")]
|
||||
public string Url { get; internal set; } = Url;
|
||||
|
||||
/// <summary>
|
||||
/// The Priority of Notifications
|
||||
/// </summary>
|
||||
[Required]
|
||||
[Description("The Priority of Notifications")]
|
||||
public int Priority { get; init; } = Priority;
|
||||
|
||||
/// <summary>
|
||||
/// The Username used for authentication
|
||||
/// </summary>
|
||||
[Required]
|
||||
[Description("The Username used for authentication")]
|
||||
public string Username { get; init; } = Username;
|
||||
|
||||
/// <summary>
|
||||
/// The Password used for authentication
|
||||
/// </summary>
|
||||
[Required]
|
||||
[Description("The Password used for authentication")]
|
||||
public string Password { get; init; } = Password;
|
||||
|
||||
/// <summary>
|
||||
/// The Topic of Notifications
|
||||
/// </summary>
|
||||
[Required]
|
||||
[Description("The Topic of Notifications")]
|
||||
public string Topic { get; init; } = Topic;
|
||||
}
|
@@ -1,28 +0,0 @@
|
||||
using System.ComponentModel;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace API.Controllers.Requests;
|
||||
|
||||
public record CreatePushoverConnectorRecord(string Name, string AppToken, string Username)
|
||||
{
|
||||
/// <summary>
|
||||
/// The Name of the Notification Connector
|
||||
/// </summary>
|
||||
[Required]
|
||||
[Description("The Name of the Notification Connector")]
|
||||
public string Name { get; init; } = Name;
|
||||
|
||||
/// <summary>
|
||||
/// The Apptoken used for authentication
|
||||
/// </summary>
|
||||
[Required]
|
||||
[Description("The Apptoken used for authentication")]
|
||||
public string AppToken { get; init; } = AppToken;
|
||||
|
||||
/// <summary>
|
||||
/// The Username used for authentication
|
||||
/// </summary>
|
||||
[Required]
|
||||
[Description("The Username used for authentication")]
|
||||
public string Username { get; init; } = Username;
|
||||
}
|
@@ -1,11 +1,8 @@
|
||||
using API.Controllers.DTOs;
|
||||
using API.MangaConnectors;
|
||||
using API.Schema.MangaContext;
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Http.HttpResults;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using static Microsoft.AspNetCore.Http.StatusCodes;
|
||||
using Manga = API.Schema.MangaContext.Manga;
|
||||
|
||||
// ReSharper disable InconsistentNaming
|
||||
|
||||
namespace API.Controllers;
|
||||
@@ -16,64 +13,58 @@ namespace API.Controllers;
|
||||
public class SearchController(MangaContext context) : Controller
|
||||
{
|
||||
/// <summary>
|
||||
/// Initiate a search for a <see cref="Schema.MangaContext.Manga"/> on <see cref="MangaConnector"/> with searchTerm
|
||||
/// Initiate a search for a <see cref="Manga"/> on <see cref="MangaConnector"/> with searchTerm
|
||||
/// </summary>
|
||||
/// <param name="MangaConnectorName"><see cref="MangaConnector"/>.Name</param>
|
||||
/// <param name="Query">searchTerm</param>
|
||||
/// <response code="200"><see cref="MinimalManga"/> exert of <see cref="Schema.MangaContext.Manga"/></response>
|
||||
/// <response code="200"></response>
|
||||
/// <response code="404"><see cref="MangaConnector"/> with Name not found</response>
|
||||
/// <response code="412"><see cref="MangaConnector"/> with Name is disabled</response>
|
||||
[HttpGet("{MangaConnectorName}/{Query}")]
|
||||
[ProducesResponseType<List<MinimalManga>>(Status200OK, "application/json")]
|
||||
[ProducesResponseType<string>(Status404NotFound, "text/plain")]
|
||||
[ProducesResponseType<Manga[]>(Status200OK, "application/json")]
|
||||
[ProducesResponseType(Status404NotFound)]
|
||||
[ProducesResponseType(Status406NotAcceptable)]
|
||||
public Results<Ok<List<MinimalManga>>, NotFound<string>, StatusCodeHttpResult> SearchManga (string MangaConnectorName, string Query)
|
||||
public IActionResult SearchManga(string MangaConnectorName, string Query)
|
||||
{
|
||||
if(Tranga.MangaConnectors.FirstOrDefault(c => c.Name.Equals(MangaConnectorName, StringComparison.InvariantCultureIgnoreCase)) is not { } connector)
|
||||
return TypedResults.NotFound(nameof(MangaConnectorName));
|
||||
return NotFound();
|
||||
if (connector.Enabled is false)
|
||||
return TypedResults.StatusCode(Status412PreconditionFailed);
|
||||
return StatusCode(Status412PreconditionFailed);
|
||||
|
||||
(Manga manga, MangaConnectorId<Manga> id)[] mangas = connector.SearchManga(Query);
|
||||
|
||||
IEnumerable<(Manga manga, MangaConnectorId<Manga> id)> addedManga = mangas.Where(kv => context.AddMangaToContext(kv, HttpContext.RequestAborted).GetAwaiter().GetResult());
|
||||
|
||||
IEnumerable<MinimalManga> result = addedManga.Select(manga => manga.manga).Select(m =>
|
||||
(Manga, MangaConnectorId<Manga>)[] mangas = connector.SearchManga(Query);
|
||||
List<Manga> retMangas = new();
|
||||
foreach ((Manga manga, MangaConnectorId<Manga> mcId) manga in mangas)
|
||||
{
|
||||
IEnumerable<MangaConnectorId> ids = m.MangaConnectorIds.Select(id =>
|
||||
new MangaConnectorId(id.Key, id.MangaConnectorName, id.ObjId, id.WebsiteUrl, id.UseForDownload));
|
||||
return new MinimalManga(m.Key, m.Name, m.Description, m.ReleaseStatus, ids);
|
||||
});
|
||||
if(Tranga.AddMangaToContext(manga, context, out Manga? add))
|
||||
retMangas.Add(add);
|
||||
}
|
||||
|
||||
return TypedResults.Ok(result.ToList());
|
||||
return Ok(retMangas.ToArray());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns <see cref="Schema.MangaContext.Manga"/> from the <see cref="MangaConnector"/> associated with <paramref name="url"/>
|
||||
/// Returns <see cref="Manga"/> from the <see cref="MangaConnector"/> associated with <paramref name="url"/>
|
||||
/// </summary>
|
||||
/// <param name="url"></param>
|
||||
/// <response code="200"><see cref="MinimalManga"/> exert of <see cref="Schema.MangaContext.Manga"/>.</response>
|
||||
/// <response code="200"></response>
|
||||
/// <response code="300">Multiple <see cref="MangaConnector"/> found for URL</response>
|
||||
/// <response code="404"><see cref="Manga"/> not found</response>
|
||||
/// <response code="500">Error during Database Operation</response>
|
||||
[HttpPost("Url")]
|
||||
[ProducesResponseType<MinimalManga>(Status200OK, "application/json")]
|
||||
[ProducesResponseType<string>(Status404NotFound, "text/plain")]
|
||||
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
|
||||
public async Task<Results<Ok<MinimalManga>, NotFound<string>, InternalServerError<string>>> GetMangaFromUrl ([FromBody]string url)
|
||||
[ProducesResponseType<Manga>(Status200OK, "application/json")]
|
||||
[ProducesResponseType(Status404NotFound)]
|
||||
[ProducesResponseType(Status500InternalServerError)]
|
||||
public IActionResult GetMangaFromUrl([FromBody]string url)
|
||||
{
|
||||
if(Tranga.MangaConnectors.FirstOrDefault(c => c.Name.Equals("Global", StringComparison.InvariantCultureIgnoreCase)) is not { } connector)
|
||||
return TypedResults.InternalServerError("Could not find Global Connector.");
|
||||
return StatusCode(Status500InternalServerError, "Could not find Global Connector.");
|
||||
|
||||
if(connector.GetMangaFromUrl(url) is not ({ } m, not null) manga)
|
||||
return TypedResults.NotFound("Could not retrieve Manga");
|
||||
if(connector.GetMangaFromUrl(url) is not { } manga)
|
||||
return NotFound();
|
||||
|
||||
if(await context.AddMangaToContext(manga, HttpContext.RequestAborted) == false)
|
||||
return TypedResults.InternalServerError("Could not add Manga to context");
|
||||
if(Tranga.AddMangaToContext(manga, context, out Manga? add) == false)
|
||||
return StatusCode(Status500InternalServerError);
|
||||
|
||||
IEnumerable<MangaConnectorId> ids = m.MangaConnectorIds.Select(id =>
|
||||
new MangaConnectorId(id.Key, id.MangaConnectorName, id.ObjId, id.WebsiteUrl, id.UseForDownload));
|
||||
MinimalManga result = new (m.Key, m.Name, m.Description, m.ReleaseStatus, ids);
|
||||
|
||||
return TypedResults.Ok(result);
|
||||
return Ok(add);
|
||||
}
|
||||
}
|
@@ -1,6 +1,5 @@
|
||||
using API.MangaDownloadClients;
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Http.HttpResults;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using static Microsoft.AspNetCore.Http.StatusCodes;
|
||||
// ReSharper disable InconsistentNaming
|
||||
@@ -18,9 +17,9 @@ public class SettingsController() : Controller
|
||||
/// <response code="200"></response>
|
||||
[HttpGet]
|
||||
[ProducesResponseType<TrangaSettings>(Status200OK, "application/json")]
|
||||
public Ok<TrangaSettings> GetSettings()
|
||||
public IActionResult GetSettings()
|
||||
{
|
||||
return TypedResults.Ok(Tranga.Settings);
|
||||
return Ok(Tranga.Settings);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -29,9 +28,9 @@ public class SettingsController() : Controller
|
||||
/// <response code="200"></response>
|
||||
[HttpGet("UserAgent")]
|
||||
[ProducesResponseType<string>(Status200OK, "text/plain")]
|
||||
public Ok<string> GetUserAgent()
|
||||
public IActionResult GetUserAgent()
|
||||
{
|
||||
return TypedResults.Ok(Tranga.Settings.UserAgent);
|
||||
return Ok(Tranga.Settings.UserAgent);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -40,11 +39,11 @@ public class SettingsController() : Controller
|
||||
/// <response code="200"></response>
|
||||
[HttpPatch("UserAgent")]
|
||||
[ProducesResponseType(Status200OK)]
|
||||
public Ok SetUserAgent([FromBody]string userAgent)
|
||||
public IActionResult SetUserAgent([FromBody]string userAgent)
|
||||
{
|
||||
//TODO Validate
|
||||
Tranga.Settings.SetUserAgent(userAgent);
|
||||
return TypedResults.Ok();
|
||||
return Ok();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -53,10 +52,10 @@ public class SettingsController() : Controller
|
||||
/// <response code="200"></response>
|
||||
[HttpDelete("UserAgent")]
|
||||
[ProducesResponseType(Status200OK)]
|
||||
public Ok ResetUserAgent()
|
||||
public IActionResult ResetUserAgent()
|
||||
{
|
||||
Tranga.Settings.SetUserAgent(TrangaSettings.DefaultUserAgent);
|
||||
return TypedResults.Ok();
|
||||
return Ok();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -65,9 +64,9 @@ public class SettingsController() : Controller
|
||||
/// <response code="200"></response>
|
||||
[HttpGet("RequestLimits")]
|
||||
[ProducesResponseType<Dictionary<RequestType,int>>(Status200OK, "application/json")]
|
||||
public Ok<Dictionary<RequestType,int>> GetRequestLimits()
|
||||
public IActionResult GetRequestLimits()
|
||||
{
|
||||
return TypedResults.Ok(Tranga.Settings.RequestLimits);
|
||||
return Ok(Tranga.Settings.RequestLimits);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -76,9 +75,9 @@ public class SettingsController() : Controller
|
||||
/// <remarks><h1>NOT IMPLEMENTED</h1></remarks>
|
||||
[HttpPatch("RequestLimits")]
|
||||
[ProducesResponseType(Status501NotImplemented)]
|
||||
public StatusCodeHttpResult SetRequestLimits()
|
||||
public IActionResult SetRequestLimits()
|
||||
{
|
||||
return TypedResults.StatusCode(Status501NotImplemented);
|
||||
return StatusCode(501);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -91,12 +90,12 @@ public class SettingsController() : Controller
|
||||
[HttpPatch("RequestLimits/{RequestType}")]
|
||||
[ProducesResponseType(Status200OK)]
|
||||
[ProducesResponseType(Status400BadRequest)]
|
||||
public Results<Ok, BadRequest> SetRequestLimit(RequestType RequestType, [FromBody]int requestLimit)
|
||||
public IActionResult SetRequestLimit(RequestType RequestType, [FromBody]int requestLimit)
|
||||
{
|
||||
if (requestLimit <= 0)
|
||||
return TypedResults.BadRequest();
|
||||
return BadRequest();
|
||||
Tranga.Settings.SetRequestLimit(RequestType, requestLimit);
|
||||
return TypedResults.Ok();
|
||||
return Ok();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -105,10 +104,10 @@ public class SettingsController() : Controller
|
||||
/// <response code="200"></response>
|
||||
[HttpDelete("RequestLimits/{RequestType}")]
|
||||
[ProducesResponseType<string>(Status200OK)]
|
||||
public Ok ResetRequestLimits(RequestType RequestType)
|
||||
public IActionResult ResetRequestLimits(RequestType RequestType)
|
||||
{
|
||||
Tranga.Settings.SetRequestLimit(RequestType, TrangaSettings.DefaultRequestLimits[RequestType]);
|
||||
return TypedResults.Ok();
|
||||
return Ok();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -117,10 +116,10 @@ public class SettingsController() : Controller
|
||||
/// <response code="200"></response>
|
||||
[HttpDelete("RequestLimits")]
|
||||
[ProducesResponseType<string>(Status200OK)]
|
||||
public Ok ResetRequestLimits()
|
||||
public IActionResult ResetRequestLimits()
|
||||
{
|
||||
Tranga.Settings.ResetRequestLimits();
|
||||
return TypedResults.Ok();
|
||||
return Ok();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -129,9 +128,9 @@ public class SettingsController() : Controller
|
||||
/// <response code="200">JPEG ImageCompression-level as Integer</response>
|
||||
[HttpGet("ImageCompressionLevel")]
|
||||
[ProducesResponseType<int>(Status200OK, "text/plain")]
|
||||
public Ok<int> GetImageCompression()
|
||||
public IActionResult GetImageCompression()
|
||||
{
|
||||
return TypedResults.Ok(Tranga.Settings.ImageCompression);
|
||||
return Ok(Tranga.Settings.ImageCompression);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -143,12 +142,12 @@ public class SettingsController() : Controller
|
||||
[HttpPatch("ImageCompressionLevel/{level}")]
|
||||
[ProducesResponseType(Status200OK)]
|
||||
[ProducesResponseType(Status400BadRequest)]
|
||||
public Results<Ok, BadRequest> SetImageCompression(int level)
|
||||
public IActionResult SetImageCompression(int level)
|
||||
{
|
||||
if (level < 1 || level > 100)
|
||||
return TypedResults.BadRequest();
|
||||
return BadRequest();
|
||||
Tranga.Settings.UpdateImageCompression(level);
|
||||
return TypedResults.Ok();
|
||||
return Ok();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -157,9 +156,9 @@ public class SettingsController() : Controller
|
||||
/// <response code="200">True if enabled</response>
|
||||
[HttpGet("BWImages")]
|
||||
[ProducesResponseType<bool>(Status200OK, "text/plain")]
|
||||
public Ok<bool> GetBwImagesToggle()
|
||||
public IActionResult GetBwImagesToggle()
|
||||
{
|
||||
return TypedResults.Ok(Tranga.Settings.BlackWhiteImages);
|
||||
return Ok(Tranga.Settings.BlackWhiteImages);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -169,10 +168,10 @@ public class SettingsController() : Controller
|
||||
/// <response code="200"></response>
|
||||
[HttpPatch("BWImages/{enabled}")]
|
||||
[ProducesResponseType(Status200OK)]
|
||||
public Ok SetBwImagesToggle(bool enabled)
|
||||
public IActionResult SetBwImagesToggle(bool enabled)
|
||||
{
|
||||
Tranga.Settings.SetBlackWhiteImageEnabled(enabled);
|
||||
return TypedResults.Ok();
|
||||
return Ok();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -195,9 +194,9 @@ public class SettingsController() : Controller
|
||||
/// <response code="200"></response>
|
||||
[HttpGet("ChapterNamingScheme")]
|
||||
[ProducesResponseType<string>(Status200OK, "text/plain")]
|
||||
public Ok<string> GetCustomNamingScheme()
|
||||
public IActionResult GetCustomNamingScheme()
|
||||
{
|
||||
return TypedResults.Ok(Tranga.Settings.ChapterNamingScheme);
|
||||
return Ok(Tranga.Settings.ChapterNamingScheme);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -218,12 +217,12 @@ public class SettingsController() : Controller
|
||||
/// <response code="200"></response>
|
||||
[HttpPatch("ChapterNamingScheme")]
|
||||
[ProducesResponseType(Status200OK)]
|
||||
public Ok SetCustomNamingScheme([FromBody]string namingScheme)
|
||||
public IActionResult SetCustomNamingScheme([FromBody]string namingScheme)
|
||||
{
|
||||
//TODO Move old Chapters
|
||||
Tranga.Settings.SetChapterNamingScheme(namingScheme);
|
||||
|
||||
return TypedResults.Ok();
|
||||
return Ok();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -233,10 +232,10 @@ public class SettingsController() : Controller
|
||||
/// <response code="200"></response>
|
||||
[HttpPost("FlareSolverr/Url")]
|
||||
[ProducesResponseType(Status200OK)]
|
||||
public Ok SetFlareSolverrUrl([FromBody]string flareSolverrUrl)
|
||||
public IActionResult SetFlareSolverrUrl([FromBody]string flareSolverrUrl)
|
||||
{
|
||||
Tranga.Settings.SetFlareSolverrUrl(flareSolverrUrl);
|
||||
return TypedResults.Ok();
|
||||
return Ok();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -245,10 +244,10 @@ public class SettingsController() : Controller
|
||||
/// <response code="200"></response>
|
||||
[HttpDelete("FlareSolverr/Url")]
|
||||
[ProducesResponseType(Status200OK)]
|
||||
public Ok ClearFlareSolverrUrl()
|
||||
public IActionResult ClearFlareSolverrUrl()
|
||||
{
|
||||
Tranga.Settings.SetFlareSolverrUrl(string.Empty);
|
||||
return TypedResults.Ok();
|
||||
return Ok();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -259,12 +258,12 @@ public class SettingsController() : Controller
|
||||
[HttpPost("FlareSolverr/Test")]
|
||||
[ProducesResponseType(Status200OK)]
|
||||
[ProducesResponseType(Status500InternalServerError)]
|
||||
public Results<Ok, InternalServerError> TestFlareSolverrReachable()
|
||||
public IActionResult TestFlareSolverrReachable()
|
||||
{
|
||||
const string knownProtectedUrl = "https://prowlarr.servarr.com/v1/ping";
|
||||
FlareSolverrDownloadClient client = new();
|
||||
RequestResult result = client.MakeRequestInternal(knownProtectedUrl);
|
||||
return (int)result.statusCode >= 200 && (int)result.statusCode < 300 ? TypedResults.Ok() : TypedResults.InternalServerError();
|
||||
return (int)result.statusCode >= 200 && (int)result.statusCode < 300 ? Ok() : StatusCode(500, result.statusCode);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -273,9 +272,9 @@ public class SettingsController() : Controller
|
||||
/// <response code="200"></response>
|
||||
[HttpGet("DownloadLanguage")]
|
||||
[ProducesResponseType<string>(Status200OK, "text/plain")]
|
||||
public Ok<string> GetDownloadLanguage()
|
||||
public IActionResult GetDownloadLanguage()
|
||||
{
|
||||
return TypedResults.Ok(Tranga.Settings.DownloadLanguage);
|
||||
return Ok(Tranga.Settings.DownloadLanguage);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -284,10 +283,10 @@ public class SettingsController() : Controller
|
||||
/// <response code="200"></response>
|
||||
[HttpPatch("DownloadLanguage/{Language}")]
|
||||
[ProducesResponseType(Status200OK)]
|
||||
public Ok SetDownloadLanguage(string Language)
|
||||
public IActionResult SetDownloadLanguage(string Language)
|
||||
{
|
||||
//TODO Validation
|
||||
Tranga.Settings.SetDownloadLanguage(Language);
|
||||
return TypedResults.Ok();
|
||||
return Ok();
|
||||
}
|
||||
}
|
@@ -1,7 +1,6 @@
|
||||
using API.Controllers.DTOs;
|
||||
using API.APIEndpointRecords;
|
||||
using API.Workers;
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Http.HttpResults;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using static Microsoft.AspNetCore.Http.StatusCodes;
|
||||
// ReSharper disable InconsistentNaming
|
||||
@@ -11,30 +10,29 @@ namespace API.Controllers;
|
||||
[ApiVersion(2)]
|
||||
[ApiController]
|
||||
[Route("v{version:apiVersion}/[controller]")]
|
||||
public class WorkerController : Controller
|
||||
public class WorkerController() : Controller
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns all <see cref="BaseWorker"/>
|
||||
/// </summary>
|
||||
/// <response code="200"><see cref="Worker"/></response>
|
||||
[HttpGet]
|
||||
[ProducesResponseType<List<Worker>>(Status200OK, "application/json")]
|
||||
public Ok<List<Worker>> GetWorkers()
|
||||
{
|
||||
IEnumerable<Worker> result = Tranga.GetRunningWorkers().Select(w =>
|
||||
new Worker(w.Key, w.AllDependencies.Select(d => d.Key), w.MissingDependencies.Select(d => d.Key), w.AllDependenciesFulfilled, w.State));
|
||||
return TypedResults.Ok(result.ToList());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns all <see cref="BaseWorker"/>.Keys
|
||||
/// </summary>
|
||||
/// <response code="200"></response>
|
||||
[HttpGet("Keys")]
|
||||
/// <response code="200"><see cref="BaseWorker"/> Keys/IDs</response>
|
||||
[HttpGet]
|
||||
[ProducesResponseType<string[]>(Status200OK, "application/json")]
|
||||
public Ok<List<string>> GetWorkerIds()
|
||||
public IActionResult GetAllWorkers()
|
||||
{
|
||||
return TypedResults.Ok(Tranga.GetRunningWorkers().Select(w => w.Key).ToList());
|
||||
return Ok(Tranga.GetRunningWorkers().Select(w => w.Key).ToArray());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns <see cref="BaseWorker"/> with requested <paramref name="WorkerIds"/>
|
||||
/// </summary>
|
||||
/// <param name="WorkerIds">Array of <see cref="BaseWorker"/>.Key</param>
|
||||
/// <response code="200"></response>
|
||||
[HttpPost("WithIDs")]
|
||||
[ProducesResponseType<BaseWorker[]>(Status200OK, "application/json")]
|
||||
public IActionResult GetWorkers([FromBody]string[] WorkerIds)
|
||||
{
|
||||
return Ok(Tranga.GetRunningWorkers().Where(worker => WorkerIds.Contains(worker.Key)).ToArray());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -43,12 +41,10 @@ public class WorkerController : Controller
|
||||
/// <param name="State">Requested <see cref="WorkerExecutionState"/></param>
|
||||
/// <response code="200"></response>
|
||||
[HttpGet("State/{State}")]
|
||||
[ProducesResponseType<List<Worker>>(Status200OK, "application/json")]
|
||||
public Ok<List<Worker>> GetWorkersInState(WorkerExecutionState State)
|
||||
[ProducesResponseType<BaseWorker[]>(Status200OK, "application/json")]
|
||||
public IActionResult GetWorkersInState(WorkerExecutionState State)
|
||||
{
|
||||
IEnumerable<Worker> result = Tranga.GetRunningWorkers().Where(worker => worker.State == State).Select(w =>
|
||||
new Worker(w.Key, w.AllDependencies.Select(d => d.Key), w.MissingDependencies.Select(d => d.Key), w.AllDependenciesFulfilled, w.State));
|
||||
return TypedResults.Ok(result.ToList());
|
||||
return Ok(Tranga.GetRunningWorkers().Where(worker => worker.State == State).ToArray());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -58,16 +54,13 @@ public class WorkerController : Controller
|
||||
/// <response code="200"></response>
|
||||
/// <response code="404"><see cref="BaseWorker"/> with <paramref name="WorkerId"/> could not be found</response>
|
||||
[HttpGet("{WorkerId}")]
|
||||
[ProducesResponseType<Worker>(Status200OK, "application/json")]
|
||||
[ProducesResponseType<string>(Status404NotFound, "text/plain")]
|
||||
public Results<Ok<Worker>, NotFound<string>> GetWorker(string WorkerId)
|
||||
[ProducesResponseType<BaseWorker>(Status200OK, "application/json")]
|
||||
[ProducesResponseType(Status404NotFound)]
|
||||
public IActionResult GetWorker(string WorkerId)
|
||||
{
|
||||
if(Tranga.GetRunningWorkers().FirstOrDefault(w => w.Key == WorkerId) is not { } w)
|
||||
return TypedResults.NotFound(nameof(WorkerId));
|
||||
|
||||
Worker result = new (w.Key, w.AllDependencies.Select(d => d.Key), w.MissingDependencies.Select(d => d.Key), w.AllDependenciesFulfilled, w.State);
|
||||
|
||||
return TypedResults.Ok(result);
|
||||
if(Tranga.GetRunningWorkers().FirstOrDefault(w => w.Key == WorkerId) is not { } worker)
|
||||
return NotFound(nameof(WorkerId));
|
||||
return Ok(worker);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -78,13 +71,13 @@ public class WorkerController : Controller
|
||||
/// <response code="404"><see cref="BaseWorker"/> with <paramref name="WorkerId"/> could not be found</response>
|
||||
[HttpDelete("{WorkerId}")]
|
||||
[ProducesResponseType(Status200OK)]
|
||||
[ProducesResponseType<string>(Status404NotFound, "text/plain")]
|
||||
public Results<Ok, NotFound<string>> DeleteWorker(string WorkerId)
|
||||
[ProducesResponseType(Status404NotFound)]
|
||||
public IActionResult DeleteWorker(string WorkerId)
|
||||
{
|
||||
if(Tranga.GetRunningWorkers().FirstOrDefault(w => w.Key == WorkerId) is not { } worker)
|
||||
return TypedResults.NotFound(nameof(WorkerId));
|
||||
return NotFound(nameof(WorkerId));
|
||||
Tranga.StopWorker(worker);
|
||||
return TypedResults.Ok();
|
||||
return Ok();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -96,18 +89,18 @@ public class WorkerController : Controller
|
||||
/// <response code="412"><see cref="BaseWorker"/> was already running</response>
|
||||
[HttpPost("{WorkerId}/Start")]
|
||||
[ProducesResponseType(Status202Accepted)]
|
||||
[ProducesResponseType<string>(Status404NotFound, "text/plain")]
|
||||
[ProducesResponseType(Status412PreconditionFailed)]
|
||||
public Results<Ok, NotFound<string>, StatusCodeHttpResult> StartWorker(string WorkerId)
|
||||
[ProducesResponseType(Status404NotFound)]
|
||||
[ProducesResponseType<string>(Status412PreconditionFailed, "text/plain")]
|
||||
public IActionResult StartWorker(string WorkerId)
|
||||
{
|
||||
if(Tranga.GetRunningWorkers().FirstOrDefault(w => w.Key == WorkerId) is not { } worker)
|
||||
return TypedResults.NotFound(nameof(WorkerId));
|
||||
return NotFound(nameof(WorkerId));
|
||||
|
||||
if (worker.State >= WorkerExecutionState.Waiting)
|
||||
return TypedResults.StatusCode(Status412PreconditionFailed);
|
||||
return StatusCode(Status412PreconditionFailed, "Already running");
|
||||
|
||||
Tranga.StartWorker(worker);
|
||||
return TypedResults.Ok();
|
||||
return Ok();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -116,20 +109,18 @@ public class WorkerController : Controller
|
||||
/// <param name="WorkerId"><see cref="BaseWorker"/>.Key</param>
|
||||
/// <response code="200"></response>
|
||||
/// <response code="404"><see cref="BaseWorker"/> with <paramref name="WorkerId"/> could not be found</response>
|
||||
/// <response code="412"><see cref="BaseWorker"/> was already not running</response>
|
||||
/// <response code="208"><see cref="BaseWorker"/> was not running</response>
|
||||
[HttpPost("{WorkerId}/Stop")]
|
||||
[ProducesResponseType(Status202Accepted)]
|
||||
[ProducesResponseType<string>(Status404NotFound, "text/plain")]
|
||||
[ProducesResponseType(Status412PreconditionFailed)]
|
||||
public Results<Ok, NotFound<string>, StatusCodeHttpResult> StopWorker(string WorkerId)
|
||||
[ProducesResponseType(Status501NotImplemented)]
|
||||
public IActionResult StopWorker(string WorkerId)
|
||||
{
|
||||
if(Tranga.GetRunningWorkers().FirstOrDefault(w => w.Key == WorkerId) is not { } worker)
|
||||
return TypedResults.NotFound(nameof(WorkerId));
|
||||
return NotFound(nameof(WorkerId));
|
||||
|
||||
if(worker.State is < WorkerExecutionState.Running or >= WorkerExecutionState.Completed)
|
||||
return TypedResults.StatusCode(Status412PreconditionFailed);
|
||||
return StatusCode(Status208AlreadyReported, "Not running");
|
||||
|
||||
Tranga.StopWorker(worker);
|
||||
return TypedResults.Ok();
|
||||
return Ok();
|
||||
}
|
||||
}
|
@@ -1,23 +0,0 @@
|
||||
<log4net>
|
||||
<appender name="ConsoleAppender" type="log4net.Appender.ConsoleAppender">
|
||||
<layout type="log4net.Layout.PatternLayout">
|
||||
<conversionPattern value="%date [%thread] %-5level %logger - %message%newline" />
|
||||
</layout>
|
||||
</appender>
|
||||
<appender name="RollingLogFileAppender" type="log4net.Appender.RollingFileAppender">
|
||||
<file value="/usr/share/tranga-api/log/Tranga.log" />
|
||||
<appendToFile value="true" />
|
||||
<rollingStyle value="Composite" />
|
||||
<datePattern value="yyyyMMdd" />
|
||||
<maxSizeRollBackups value="4" />
|
||||
<maximumFileSize value="256MB" />
|
||||
<layout type="log4net.Layout.PatternLayout">
|
||||
<conversionPattern value="%date [%thread] %-5level %logger - %message%newline" />
|
||||
</layout>
|
||||
</appender>
|
||||
<root>
|
||||
<level value="DEBUG" />
|
||||
<appender-ref ref="ConsoleAppender" />
|
||||
<appender-ref ref="RollingLogFileAppender" />
|
||||
</root>
|
||||
</log4net>
|
@@ -4,7 +4,7 @@ namespace API.MangaConnectors;
|
||||
|
||||
public class Global : MangaConnector
|
||||
{
|
||||
public Global() : base("Global", ["all"], [""], "https://avatars.githubusercontent.com/u/13404778")
|
||||
public Global() : base("Global", ["all"], [""], "")
|
||||
{
|
||||
}
|
||||
|
||||
|
@@ -6,23 +6,33 @@ using API.Schema.MangaContext;
|
||||
using log4net;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Newtonsoft.Json;
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.Formats.Jpeg;
|
||||
using SixLabors.ImageSharp.Processing;
|
||||
|
||||
namespace API.MangaConnectors;
|
||||
|
||||
[PrimaryKey("Name")]
|
||||
public abstract class MangaConnector(string name, string[] supportedLanguages, string[] baseUris, string iconUrl)
|
||||
{
|
||||
[NotMapped] internal DownloadClient downloadClient { get; init; } = null!;
|
||||
[JsonIgnore]
|
||||
[NotMapped]
|
||||
internal DownloadClient downloadClient { get; init; } = null!;
|
||||
|
||||
[NotMapped] protected ILog Log { get; init; } = LogManager.GetLogger(name);
|
||||
[JsonIgnore]
|
||||
[NotMapped]
|
||||
protected ILog Log { get; init; } = LogManager.GetLogger(name);
|
||||
|
||||
[StringLength(32)] public string Name { get; init; } = name;
|
||||
[StringLength(8)] public string[] SupportedLanguages { get; init; } = supportedLanguages;
|
||||
[StringLength(2048)] public string IconUrl { get; init; } = iconUrl;
|
||||
[StringLength(256)] public string[] BaseUris { get; init; } = baseUris;
|
||||
[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, MangaConnectorId<Manga>)[] SearchManga(string mangaSearchName);
|
||||
@@ -46,46 +56,21 @@ public abstract class MangaConnector(string name, string[] supportedLanguages, s
|
||||
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(mangaId.Obj.CoverUrl);
|
||||
string filename = $"{match.Groups[1].Value}-{mangaId.ObjId}.{mangaId.MangaConnectorName}.{match.Groups[3].Value}";
|
||||
string saveImagePath = Path.Join(TrangaSettings.coverImageCacheOriginal, filename);
|
||||
string filename = $"{match.Groups[1].Value}-{mangaId.Key}.{match.Groups[3].Value}";
|
||||
string saveImagePath = Path.Join(TrangaSettings.coverImageCache, filename);
|
||||
|
||||
if (File.Exists(saveImagePath))
|
||||
return filename;
|
||||
return saveImagePath;
|
||||
|
||||
RequestResult coverResult = downloadClient.MakeRequest(mangaId.Obj.CoverUrl, RequestType.MangaCover, $"https://{match.Groups[1].Value}");
|
||||
if ((int)coverResult.statusCode < 200 || (int)coverResult.statusCode >= 300)
|
||||
return SaveCoverImageToCache(mangaId, --retries);
|
||||
|
||||
try
|
||||
{
|
||||
using MemoryStream ms = new();
|
||||
coverResult.result.CopyTo(ms);
|
||||
byte[] imageBytes = ms.ToArray();
|
||||
Directory.CreateDirectory(TrangaSettings.coverImageCacheOriginal);
|
||||
File.WriteAllBytes(saveImagePath, imageBytes);
|
||||
using MemoryStream ms = new();
|
||||
coverResult.result.CopyTo(ms);
|
||||
Directory.CreateDirectory(TrangaSettings.coverImageCache);
|
||||
File.WriteAllBytes(saveImagePath, ms.ToArray());
|
||||
|
||||
using Image image = Image.Load(imageBytes);
|
||||
Directory.CreateDirectory(TrangaSettings.coverImageCacheLarge);
|
||||
using Image large = image.Clone(x => x.Resize(new ResizeOptions
|
||||
{ Size = Constants.ImageLgSize, Mode = ResizeMode.Max }));
|
||||
large.SaveAsJpeg(Path.Join(TrangaSettings.coverImageCacheLarge, filename), new (){ Quality = 40 });
|
||||
|
||||
Directory.CreateDirectory(TrangaSettings.coverImageCacheMedium);
|
||||
using Image medium = image.Clone(x => x.Resize(new ResizeOptions
|
||||
{ Size = Constants.ImageMdSize, Mode = ResizeMode.Max }));
|
||||
medium.SaveAsJpeg(Path.Join(TrangaSettings.coverImageCacheMedium, filename), new (){ Quality = 40 });
|
||||
|
||||
Directory.CreateDirectory(TrangaSettings.coverImageCacheSmall);
|
||||
using Image small = image.Clone(x => x.Resize(new ResizeOptions
|
||||
{ Size = Constants.ImageSmSize, Mode = ResizeMode.Max }));
|
||||
small.SaveAsJpeg(Path.Join(TrangaSettings.coverImageCacheSmall, filename), new (){ Quality = 40 });
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e);
|
||||
}
|
||||
|
||||
|
||||
return filename;
|
||||
return saveImagePath;
|
||||
}
|
||||
}
|
@@ -324,7 +324,7 @@ public class MangaDex : MangaConnector
|
||||
{
|
||||
string? id = jToken.Value<string>("id");
|
||||
JToken? attributes = jToken["attributes"];
|
||||
string? chapterStr = attributes?.Value<string>("chapter") ?? "0";
|
||||
string? chapterStr = attributes?.Value<string>("chapter");
|
||||
string? volumeStr = attributes?.Value<string>("volume");
|
||||
int? volumeNumber = null;
|
||||
string? title = attributes?.Value<string>("title");
|
||||
|
151
API/MangaDownloadClients/ChromiumDownloadClient.cs
Normal file
151
API/MangaDownloadClients/ChromiumDownloadClient.cs
Normal file
@@ -0,0 +1,151 @@
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using HtmlAgilityPack;
|
||||
using log4net;
|
||||
using PuppeteerSharp;
|
||||
|
||||
namespace API.MangaDownloadClients;
|
||||
|
||||
internal class ChromiumDownloadClient : DownloadClient
|
||||
{
|
||||
private static IBrowser? _browser;
|
||||
private readonly HttpDownloadClient _httpDownloadClient;
|
||||
private readonly Thread _closeStalePagesThread;
|
||||
private readonly List<KeyValuePair<IPage, DateTime>> _openPages = new ();
|
||||
|
||||
private static async Task<IBrowser> StartBrowser(ILog log)
|
||||
{
|
||||
return await Puppeteer.LaunchAsync(new LaunchOptions
|
||||
{
|
||||
Headless = true,
|
||||
Args = new [] {
|
||||
"--disable-gpu",
|
||||
"--disable-dev-shm-usage",
|
||||
"--disable-setuid-sandbox",
|
||||
"--no-sandbox"},
|
||||
Timeout = 30000
|
||||
}, new LoggerFactory([new Provider(log)]));
|
||||
}
|
||||
|
||||
public ChromiumDownloadClient()
|
||||
{
|
||||
_httpDownloadClient = new();
|
||||
if(_browser is null)
|
||||
_browser = StartBrowser(Log).Result;
|
||||
_closeStalePagesThread = new Thread(CheckStalePages);
|
||||
_closeStalePagesThread.Start();
|
||||
}
|
||||
|
||||
private void CheckStalePages()
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
Thread.Sleep(TimeSpan.FromHours(1));
|
||||
Log.Debug("Removing stale pages");
|
||||
foreach ((IPage key, DateTime _) in _openPages.Where(kv => kv.Value.Subtract(DateTime.Now) > TimeSpan.FromHours(1)))
|
||||
{
|
||||
Log.Debug($"Closing {key.Url}");
|
||||
key.CloseAsync().Wait();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private readonly Regex _imageUrlRex = new(@"https?:\/\/.*\.(?:p?jpe?g|gif|a?png|bmp|avif|webp)(\?.*)?");
|
||||
internal override RequestResult MakeRequestInternal(string url, string? referrer = null, string? clickButton = null)
|
||||
{
|
||||
Log.Debug($"Requesting {url}");
|
||||
return _imageUrlRex.IsMatch(url)
|
||||
? _httpDownloadClient.MakeRequestInternal(url, referrer)
|
||||
: MakeRequestBrowser(url, referrer, clickButton);
|
||||
}
|
||||
|
||||
private RequestResult MakeRequestBrowser(string url, string? referrer = null, string? clickButton = null)
|
||||
{
|
||||
if (_browser is null)
|
||||
return new RequestResult(HttpStatusCode.InternalServerError, null, Stream.Null);
|
||||
IPage page = _browser.NewPageAsync().Result;
|
||||
_openPages.Add(new(page, DateTime.Now));
|
||||
page.SetExtraHttpHeadersAsync(new() { { "Referer", referrer } });
|
||||
page.DefaultTimeout = 30000;
|
||||
IResponse response;
|
||||
try
|
||||
{
|
||||
response = page.GoToAsync(url, WaitUntilNavigation.Networkidle0).Result;
|
||||
Log.Debug($"Page loaded. {url}");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Info($"Could not load Page {url}\n{e.Message}");
|
||||
page.CloseAsync();
|
||||
_openPages.Remove(_openPages.Find(i => i.Key == page));
|
||||
return new RequestResult(HttpStatusCode.InternalServerError, null, Stream.Null);
|
||||
}
|
||||
|
||||
Stream stream = Stream.Null;
|
||||
HtmlDocument? document = null;
|
||||
|
||||
if (response.Headers.TryGetValue("Content-Type", out string? content))
|
||||
{
|
||||
if (content.Contains("text/html"))
|
||||
{
|
||||
if (clickButton is not null && page.QuerySelectorAsync(clickButton).Result is not null)
|
||||
page.ClickAsync(clickButton).Wait();
|
||||
string htmlString = page.GetContentAsync().Result;
|
||||
stream = new MemoryStream(Encoding.Default.GetBytes(htmlString));
|
||||
document = new ();
|
||||
document.LoadHtml(htmlString);
|
||||
}else if (content.Contains("image"))
|
||||
{
|
||||
stream = new MemoryStream(response.BufferAsync().Result);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
page.CloseAsync().Wait();
|
||||
_openPages.Remove(_openPages.Find(i => i.Key == page));
|
||||
return new RequestResult(HttpStatusCode.InternalServerError, null, Stream.Null);
|
||||
}
|
||||
|
||||
page.CloseAsync().Wait();
|
||||
_openPages.Remove(_openPages.Find(i => i.Key == page));
|
||||
return new RequestResult(response.Status, document, stream, false, "");
|
||||
}
|
||||
|
||||
private class Provider(ILog log) : ILoggerProvider
|
||||
{
|
||||
public void Dispose()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public ILogger CreateLogger(string categoryName)
|
||||
{
|
||||
return new ChromiumLogger(log);
|
||||
}
|
||||
}
|
||||
|
||||
private class ChromiumLogger(ILog log) : ILogger
|
||||
{
|
||||
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
|
||||
{
|
||||
string message = formatter.Invoke(state, exception);
|
||||
switch(logLevel)
|
||||
{
|
||||
case LogLevel.Critical: log.Fatal(message); break;
|
||||
case LogLevel.Error: log.Error(message); break;
|
||||
case LogLevel.Warning: log.Warn(message); break;
|
||||
case LogLevel.Information: log.Info(message); break;
|
||||
case LogLevel.Debug: log.Debug(message); break;
|
||||
default: log.Info(message); break;
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsEnabled(LogLevel logLevel) => true;
|
||||
|
||||
public IDisposable? BeginScope<TState>(TState state) where TState : notnull
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
@@ -14,22 +14,18 @@ public abstract class DownloadClient
|
||||
this.Log = LogManager.GetLogger(GetType());
|
||||
}
|
||||
|
||||
// TODO Requests still go too fast across threads!
|
||||
public RequestResult MakeRequest(string url, RequestType requestType, string? referrer = null, string? clickButton = null)
|
||||
{
|
||||
Log.Debug($"Requesting {requestType} {url}");
|
||||
|
||||
// If we don't have a RequestLimit set for a Type, use the default one
|
||||
if (!Tranga.Settings.RequestLimits.ContainsKey(requestType))
|
||||
requestType = RequestType.Default;
|
||||
{
|
||||
return new RequestResult(HttpStatusCode.NotAcceptable, null, Stream.Null);
|
||||
}
|
||||
|
||||
int rateLimit = Tranga.Settings.RequestLimits[requestType];
|
||||
// TODO this probably needs a better check whether the useragent matches...
|
||||
// If the UserAgent is the default one, do not exceed the default request-limits.
|
||||
if (Tranga.Settings.UserAgent == TrangaSettings.DefaultUserAgent && rateLimit > TrangaSettings.DefaultRequestLimits[requestType])
|
||||
rateLimit = TrangaSettings.DefaultRequestLimits[requestType];
|
||||
int rateLimit = Tranga.Settings.UserAgent == TrangaSettings.DefaultUserAgent
|
||||
? TrangaSettings.DefaultRequestLimits[requestType]
|
||||
: Tranga.Settings.RequestLimits[requestType];
|
||||
|
||||
// Apply the delay
|
||||
TimeSpan timeBetweenRequests = TimeSpan.FromMinutes(1).Divide(rateLimit);
|
||||
DateTime now = DateTime.Now;
|
||||
LastExecutedRateLimit.TryAdd(requestType, now.Subtract(timeBetweenRequests));
|
||||
@@ -38,12 +34,11 @@ public abstract class DownloadClient
|
||||
Log.Debug($"Request limit {requestType} {rateLimit}/Minute timeBetweenRequests: {timeBetweenRequests:ss'.'fffff} Timeout: {rateLimitTimeout:ss'.'fffff}");
|
||||
|
||||
if (rateLimitTimeout > TimeSpan.Zero)
|
||||
{
|
||||
Thread.Sleep(rateLimitTimeout);
|
||||
}
|
||||
|
||||
// Make the request
|
||||
RequestResult result = MakeRequestInternal(url, referrer, clickButton);
|
||||
|
||||
// Update the time the last request was made
|
||||
LastExecutedRateLimit[requestType] = DateTime.UtcNow;
|
||||
Log.Debug($"Result {url}: {result}");
|
||||
return result;
|
||||
|
@@ -1,71 +0,0 @@
|
||||
// <auto-generated />
|
||||
using API.Schema.LibraryContext;
|
||||
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.Library
|
||||
{
|
||||
[DbContext(typeof(LibraryContext))]
|
||||
[Migration("20250901194308_KeyLengthChange")]
|
||||
partial class KeyLengthChange
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "9.0.8")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("API.Schema.LibraryContext.LibraryConnectors.LibraryConnector", b =>
|
||||
{
|
||||
b.Property<string>("Key")
|
||||
.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("Key");
|
||||
|
||||
b.ToTable("LibraryConnectors");
|
||||
|
||||
b.HasDiscriminator<byte>("LibraryType");
|
||||
|
||||
b.UseTphMappingStrategy();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.LibraryContext.LibraryConnectors.Kavita", b =>
|
||||
{
|
||||
b.HasBaseType("API.Schema.LibraryContext.LibraryConnectors.LibraryConnector");
|
||||
|
||||
b.HasDiscriminator().HasValue((byte)1);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.LibraryContext.LibraryConnectors.Komga", b =>
|
||||
{
|
||||
b.HasBaseType("API.Schema.LibraryContext.LibraryConnectors.LibraryConnector");
|
||||
|
||||
b.HasDiscriminator().HasValue((byte)0);
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,36 +0,0 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace API.Migrations.Library
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class KeyLengthChange : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "Key",
|
||||
table: "LibraryConnectors",
|
||||
type: "character varying(64)",
|
||||
maxLength: 64,
|
||||
nullable: false,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "text");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "Key",
|
||||
table: "LibraryConnectors",
|
||||
type: "text",
|
||||
nullable: false,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "character varying(64)",
|
||||
oldMaxLength: 64);
|
||||
}
|
||||
}
|
||||
}
|
@@ -16,7 +16,7 @@ namespace API.Migrations.Library
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "9.0.8")
|
||||
.HasAnnotation("ProductVersion", "9.0.5")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
@@ -24,8 +24,7 @@ namespace API.Migrations.Library
|
||||
modelBuilder.Entity("API.Schema.LibraryContext.LibraryConnectors.LibraryConnector", b =>
|
||||
{
|
||||
b.Property<string>("Key")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Auth")
|
||||
.IsRequired()
|
||||
|
@@ -1,535 +0,0 @@
|
||||
// <auto-generated />
|
||||
using API.Schema.MangaContext;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace API.Migrations.Manga
|
||||
{
|
||||
[DbContext(typeof(MangaContext))]
|
||||
[Migration("20250901194144_KeyLengthChange")]
|
||||
partial class KeyLengthChange
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "9.0.8")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("API.MangaConnectors.MangaConnector", b =>
|
||||
{
|
||||
b.Property<string>("Name")
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)");
|
||||
|
||||
b.PrimitiveCollection<string[]>("BaseUris")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("text[]");
|
||||
|
||||
b.Property<bool>("Enabled")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("IconUrl")
|
||||
.IsRequired()
|
||||
.HasMaxLength(2048)
|
||||
.HasColumnType("character varying(2048)");
|
||||
|
||||
b.PrimitiveCollection<string[]>("SupportedLanguages")
|
||||
.IsRequired()
|
||||
.HasMaxLength(8)
|
||||
.HasColumnType("text[]");
|
||||
|
||||
b.HasKey("Name");
|
||||
|
||||
b.ToTable("MangaConnector");
|
||||
|
||||
b.HasDiscriminator<string>("Name").HasValue("MangaConnector");
|
||||
|
||||
b.UseTphMappingStrategy();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.MangaContext.Author", b =>
|
||||
{
|
||||
b.Property<string>("Key")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<string>("AuthorName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("character varying(128)");
|
||||
|
||||
b.HasKey("Key");
|
||||
|
||||
b.ToTable("Authors");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.MangaContext.Chapter", b =>
|
||||
{
|
||||
b.Property<string>("Key")
|
||||
.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<int?>("VolumeNumber")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Key");
|
||||
|
||||
b.HasIndex("ParentMangaId");
|
||||
|
||||
b.ToTable("Chapters");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.MangaContext.FileLibrary", b =>
|
||||
{
|
||||
b.Property<string>("Key")
|
||||
.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("Key");
|
||||
|
||||
b.ToTable("FileLibraries");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.MangaContext.Manga", b =>
|
||||
{
|
||||
b.Property<string>("Key")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<string>("CoverFileNameInCache")
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("character varying(512)");
|
||||
|
||||
b.Property<string>("CoverUrl")
|
||||
.IsRequired()
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("character varying(512)");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("DirectoryName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)");
|
||||
|
||||
b.Property<float>("IgnoreChaptersBefore")
|
||||
.HasColumnType("real");
|
||||
|
||||
b.Property<string>("LibraryId")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("character varying(512)");
|
||||
|
||||
b.Property<string>("OriginalLanguage")
|
||||
.HasMaxLength(8)
|
||||
.HasColumnType("character varying(8)");
|
||||
|
||||
b.Property<byte>("ReleaseStatus")
|
||||
.HasColumnType("smallint");
|
||||
|
||||
b.Property<long?>("Year")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.HasKey("Key");
|
||||
|
||||
b.HasIndex("LibraryId");
|
||||
|
||||
b.ToTable("Mangas");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.MangaContext.MangaConnectorId<API.Schema.MangaContext.Chapter>", b =>
|
||||
{
|
||||
b.Property<string>("Key")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<string>("IdOnConnectorSite")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<string>("MangaConnectorName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)");
|
||||
|
||||
b.Property<string>("ObjId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<bool>("UseForDownload")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("WebsiteUrl")
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("character varying(512)");
|
||||
|
||||
b.HasKey("Key");
|
||||
|
||||
b.HasIndex("ObjId");
|
||||
|
||||
b.ToTable("MangaConnectorToChapter");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.MangaContext.MangaConnectorId<API.Schema.MangaContext.Manga>", b =>
|
||||
{
|
||||
b.Property<string>("Key")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<string>("IdOnConnectorSite")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<string>("MangaConnectorName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)");
|
||||
|
||||
b.Property<string>("ObjId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<bool>("UseForDownload")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("WebsiteUrl")
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("character varying(512)");
|
||||
|
||||
b.HasKey("Key");
|
||||
|
||||
b.HasIndex("ObjId");
|
||||
|
||||
b.ToTable("MangaConnectorToManga");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.MangaContext.MangaTag", b =>
|
||||
{
|
||||
b.Property<string>("Tag")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.HasKey("Tag");
|
||||
|
||||
b.ToTable("Tags");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.MangaContext.MetadataFetchers.MetadataEntry", b =>
|
||||
{
|
||||
b.Property<string>("MetadataFetcherName")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Identifier")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("MangaId")
|
||||
.IsRequired()
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.HasKey("MetadataFetcherName", "Identifier");
|
||||
|
||||
b.HasIndex("MangaId");
|
||||
|
||||
b.ToTable("MetadataEntries");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.MangaContext.MetadataFetchers.MetadataFetcher", b =>
|
||||
{
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("MetadataEntry")
|
||||
.IsRequired()
|
||||
.HasMaxLength(21)
|
||||
.HasColumnType("character varying(21)");
|
||||
|
||||
b.HasKey("Name");
|
||||
|
||||
b.ToTable("MetadataFetcher");
|
||||
|
||||
b.HasDiscriminator<string>("MetadataEntry").HasValue("MetadataFetcher");
|
||||
|
||||
b.UseTphMappingStrategy();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AuthorToManga", b =>
|
||||
{
|
||||
b.Property<string>("AuthorIds")
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<string>("MangaIds")
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.HasKey("AuthorIds", "MangaIds");
|
||||
|
||||
b.HasIndex("MangaIds");
|
||||
|
||||
b.ToTable("AuthorToManga");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MangaTagToManga", b =>
|
||||
{
|
||||
b.Property<string>("MangaTagIds")
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<string>("MangaIds")
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.HasKey("MangaTagIds", "MangaIds");
|
||||
|
||||
b.HasIndex("MangaIds");
|
||||
|
||||
b.ToTable("MangaTagToManga");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.MangaConnectors.ComickIo", b =>
|
||||
{
|
||||
b.HasBaseType("API.MangaConnectors.MangaConnector");
|
||||
|
||||
b.HasDiscriminator().HasValue("ComickIo");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.MangaConnectors.Global", b =>
|
||||
{
|
||||
b.HasBaseType("API.MangaConnectors.MangaConnector");
|
||||
|
||||
b.HasDiscriminator().HasValue("Global");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.MangaConnectors.MangaDex", b =>
|
||||
{
|
||||
b.HasBaseType("API.MangaConnectors.MangaConnector");
|
||||
|
||||
b.HasDiscriminator().HasValue("MangaDex");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.MangaContext.MetadataFetchers.MyAnimeList", b =>
|
||||
{
|
||||
b.HasBaseType("API.Schema.MangaContext.MetadataFetchers.MetadataFetcher");
|
||||
|
||||
b.HasDiscriminator().HasValue("MyAnimeList");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.MangaContext.Chapter", b =>
|
||||
{
|
||||
b.HasOne("API.Schema.MangaContext.Manga", "ParentManga")
|
||||
.WithMany("Chapters")
|
||||
.HasForeignKey("ParentMangaId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("ParentManga");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.MangaContext.Manga", b =>
|
||||
{
|
||||
b.HasOne("API.Schema.MangaContext.FileLibrary", "Library")
|
||||
.WithMany()
|
||||
.HasForeignKey("LibraryId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.OwnsMany("API.Schema.MangaContext.AltTitle", "AltTitles", b1 =>
|
||||
{
|
||||
b1.Property<string>("Key")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b1.Property<string>("Language")
|
||||
.IsRequired()
|
||||
.HasMaxLength(8)
|
||||
.HasColumnType("character varying(8)");
|
||||
|
||||
b1.Property<string>("MangaKey")
|
||||
.IsRequired()
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b1.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b1.HasKey("Key");
|
||||
|
||||
b1.HasIndex("MangaKey");
|
||||
|
||||
b1.ToTable("AltTitle");
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("MangaKey");
|
||||
});
|
||||
|
||||
b.OwnsMany("API.Schema.MangaContext.Link", "Links", b1 =>
|
||||
{
|
||||
b1.Property<string>("Key")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b1.Property<string>("LinkProvider")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b1.Property<string>("LinkUrl")
|
||||
.IsRequired()
|
||||
.HasMaxLength(2048)
|
||||
.HasColumnType("character varying(2048)");
|
||||
|
||||
b1.Property<string>("MangaKey")
|
||||
.IsRequired()
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b1.HasKey("Key");
|
||||
|
||||
b1.HasIndex("MangaKey");
|
||||
|
||||
b1.ToTable("Link");
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("MangaKey");
|
||||
});
|
||||
|
||||
b.Navigation("AltTitles");
|
||||
|
||||
b.Navigation("Library");
|
||||
|
||||
b.Navigation("Links");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.MangaContext.MangaConnectorId<API.Schema.MangaContext.Chapter>", b =>
|
||||
{
|
||||
b.HasOne("API.Schema.MangaContext.Chapter", "Obj")
|
||||
.WithMany("MangaConnectorIds")
|
||||
.HasForeignKey("ObjId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Obj");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.MangaContext.MangaConnectorId<API.Schema.MangaContext.Manga>", b =>
|
||||
{
|
||||
b.HasOne("API.Schema.MangaContext.Manga", "Obj")
|
||||
.WithMany("MangaConnectorIds")
|
||||
.HasForeignKey("ObjId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Obj");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.MangaContext.MetadataFetchers.MetadataEntry", b =>
|
||||
{
|
||||
b.HasOne("API.Schema.MangaContext.Manga", "Manga")
|
||||
.WithMany()
|
||||
.HasForeignKey("MangaId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("API.Schema.MangaContext.MetadataFetchers.MetadataFetcher", "MetadataFetcher")
|
||||
.WithMany()
|
||||
.HasForeignKey("MetadataFetcherName")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Manga");
|
||||
|
||||
b.Navigation("MetadataFetcher");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AuthorToManga", b =>
|
||||
{
|
||||
b.HasOne("API.Schema.MangaContext.Author", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("AuthorIds")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("API.Schema.MangaContext.Manga", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("MangaIds")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MangaTagToManga", b =>
|
||||
{
|
||||
b.HasOne("API.Schema.MangaContext.Manga", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("MangaIds")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("API.Schema.MangaContext.MangaTag", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("MangaTagIds")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.MangaContext.Chapter", b =>
|
||||
{
|
||||
b.Navigation("MangaConnectorIds");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.MangaContext.Manga", b =>
|
||||
{
|
||||
b.Navigation("Chapters");
|
||||
|
||||
b.Navigation("MangaConnectorIds");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,258 +0,0 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace API.Migrations.Manga
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class KeyLengthChange : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "MangaId",
|
||||
table: "MetadataEntries",
|
||||
type: "character varying(64)",
|
||||
nullable: false,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "text");
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "MangaIds",
|
||||
table: "MangaTagToManga",
|
||||
type: "character varying(64)",
|
||||
nullable: false,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "text");
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "Key",
|
||||
table: "Mangas",
|
||||
type: "character varying(64)",
|
||||
maxLength: 64,
|
||||
nullable: false,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "text");
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "Key",
|
||||
table: "MangaConnectorToManga",
|
||||
type: "character varying(64)",
|
||||
maxLength: 64,
|
||||
nullable: false,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "text");
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "Key",
|
||||
table: "MangaConnectorToChapter",
|
||||
type: "character varying(64)",
|
||||
maxLength: 64,
|
||||
nullable: false,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "text");
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "MangaKey",
|
||||
table: "Link",
|
||||
type: "character varying(64)",
|
||||
nullable: false,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "text");
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "Key",
|
||||
table: "Link",
|
||||
type: "character varying(64)",
|
||||
maxLength: 64,
|
||||
nullable: false,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "text");
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "Key",
|
||||
table: "FileLibraries",
|
||||
type: "character varying(64)",
|
||||
maxLength: 64,
|
||||
nullable: false,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "text");
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "Key",
|
||||
table: "Chapters",
|
||||
type: "character varying(64)",
|
||||
maxLength: 64,
|
||||
nullable: false,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "text");
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "MangaIds",
|
||||
table: "AuthorToManga",
|
||||
type: "character varying(64)",
|
||||
nullable: false,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "text");
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "AuthorIds",
|
||||
table: "AuthorToManga",
|
||||
type: "character varying(64)",
|
||||
nullable: false,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "text");
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "Key",
|
||||
table: "Authors",
|
||||
type: "character varying(64)",
|
||||
maxLength: 64,
|
||||
nullable: false,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "text");
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "MangaKey",
|
||||
table: "AltTitle",
|
||||
type: "character varying(64)",
|
||||
nullable: false,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "text");
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "Key",
|
||||
table: "AltTitle",
|
||||
type: "character varying(64)",
|
||||
maxLength: 64,
|
||||
nullable: false,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "text");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "MangaId",
|
||||
table: "MetadataEntries",
|
||||
type: "text",
|
||||
nullable: false,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "character varying(64)");
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "MangaIds",
|
||||
table: "MangaTagToManga",
|
||||
type: "text",
|
||||
nullable: false,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "character varying(64)");
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "Key",
|
||||
table: "Mangas",
|
||||
type: "text",
|
||||
nullable: false,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "character varying(64)",
|
||||
oldMaxLength: 64);
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "Key",
|
||||
table: "MangaConnectorToManga",
|
||||
type: "text",
|
||||
nullable: false,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "character varying(64)",
|
||||
oldMaxLength: 64);
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "Key",
|
||||
table: "MangaConnectorToChapter",
|
||||
type: "text",
|
||||
nullable: false,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "character varying(64)",
|
||||
oldMaxLength: 64);
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "MangaKey",
|
||||
table: "Link",
|
||||
type: "text",
|
||||
nullable: false,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "character varying(64)");
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "Key",
|
||||
table: "Link",
|
||||
type: "text",
|
||||
nullable: false,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "character varying(64)",
|
||||
oldMaxLength: 64);
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "Key",
|
||||
table: "FileLibraries",
|
||||
type: "text",
|
||||
nullable: false,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "character varying(64)",
|
||||
oldMaxLength: 64);
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "Key",
|
||||
table: "Chapters",
|
||||
type: "text",
|
||||
nullable: false,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "character varying(64)",
|
||||
oldMaxLength: 64);
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "MangaIds",
|
||||
table: "AuthorToManga",
|
||||
type: "text",
|
||||
nullable: false,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "character varying(64)");
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "AuthorIds",
|
||||
table: "AuthorToManga",
|
||||
type: "text",
|
||||
nullable: false,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "character varying(64)");
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "Key",
|
||||
table: "Authors",
|
||||
type: "text",
|
||||
nullable: false,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "character varying(64)",
|
||||
oldMaxLength: 64);
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "MangaKey",
|
||||
table: "AltTitle",
|
||||
type: "text",
|
||||
nullable: false,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "character varying(64)");
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "Key",
|
||||
table: "AltTitle",
|
||||
type: "text",
|
||||
nullable: false,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "character varying(64)",
|
||||
oldMaxLength: 64);
|
||||
}
|
||||
}
|
||||
}
|
@@ -16,7 +16,7 @@ namespace API.Migrations.Manga
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "9.0.8")
|
||||
.HasAnnotation("ProductVersion", "9.0.5")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
@@ -57,8 +57,7 @@ namespace API.Migrations.Manga
|
||||
modelBuilder.Entity("API.Schema.MangaContext.Author", b =>
|
||||
{
|
||||
b.Property<string>("Key")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("AuthorName")
|
||||
.IsRequired()
|
||||
@@ -73,8 +72,7 @@ namespace API.Migrations.Manga
|
||||
modelBuilder.Entity("API.Schema.MangaContext.Chapter", b =>
|
||||
{
|
||||
b.Property<string>("Key")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("ChapterNumber")
|
||||
.IsRequired()
|
||||
@@ -111,8 +109,7 @@ namespace API.Migrations.Manga
|
||||
modelBuilder.Entity("API.Schema.MangaContext.FileLibrary", b =>
|
||||
{
|
||||
b.Property<string>("Key")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("BasePath")
|
||||
.IsRequired()
|
||||
@@ -132,8 +129,7 @@ namespace API.Migrations.Manga
|
||||
modelBuilder.Entity("API.Schema.MangaContext.Manga", b =>
|
||||
{
|
||||
b.Property<string>("Key")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("CoverFileNameInCache")
|
||||
.HasMaxLength(512)
|
||||
@@ -185,8 +181,7 @@ namespace API.Migrations.Manga
|
||||
modelBuilder.Entity("API.Schema.MangaContext.MangaConnectorId<API.Schema.MangaContext.Chapter>", b =>
|
||||
{
|
||||
b.Property<string>("Key")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("IdOnConnectorSite")
|
||||
.IsRequired()
|
||||
@@ -220,8 +215,7 @@ namespace API.Migrations.Manga
|
||||
modelBuilder.Entity("API.Schema.MangaContext.MangaConnectorId<API.Schema.MangaContext.Manga>", b =>
|
||||
{
|
||||
b.Property<string>("Key")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("IdOnConnectorSite")
|
||||
.IsRequired()
|
||||
@@ -273,7 +267,7 @@ namespace API.Migrations.Manga
|
||||
|
||||
b.Property<string>("MangaId")
|
||||
.IsRequired()
|
||||
.HasColumnType("character varying(64)");
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("MetadataFetcherName", "Identifier");
|
||||
|
||||
@@ -304,10 +298,10 @@ namespace API.Migrations.Manga
|
||||
modelBuilder.Entity("AuthorToManga", b =>
|
||||
{
|
||||
b.Property<string>("AuthorIds")
|
||||
.HasColumnType("character varying(64)");
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("MangaIds")
|
||||
.HasColumnType("character varying(64)");
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("AuthorIds", "MangaIds");
|
||||
|
||||
@@ -322,7 +316,7 @@ namespace API.Migrations.Manga
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<string>("MangaIds")
|
||||
.HasColumnType("character varying(64)");
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("MangaTagIds", "MangaIds");
|
||||
|
||||
@@ -380,8 +374,7 @@ namespace API.Migrations.Manga
|
||||
b.OwnsMany("API.Schema.MangaContext.AltTitle", "AltTitles", b1 =>
|
||||
{
|
||||
b1.Property<string>("Key")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
.HasColumnType("text");
|
||||
|
||||
b1.Property<string>("Language")
|
||||
.IsRequired()
|
||||
@@ -390,7 +383,7 @@ namespace API.Migrations.Manga
|
||||
|
||||
b1.Property<string>("MangaKey")
|
||||
.IsRequired()
|
||||
.HasColumnType("character varying(64)");
|
||||
.HasColumnType("text");
|
||||
|
||||
b1.Property<string>("Title")
|
||||
.IsRequired()
|
||||
@@ -410,8 +403,7 @@ namespace API.Migrations.Manga
|
||||
b.OwnsMany("API.Schema.MangaContext.Link", "Links", b1 =>
|
||||
{
|
||||
b1.Property<string>("Key")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
.HasColumnType("text");
|
||||
|
||||
b1.Property<string>("LinkProvider")
|
||||
.IsRequired()
|
||||
@@ -425,7 +417,7 @@ namespace API.Migrations.Manga
|
||||
|
||||
b1.Property<string>("MangaKey")
|
||||
.IsRequired()
|
||||
.HasColumnType("character varying(64)");
|
||||
.HasColumnType("text");
|
||||
|
||||
b1.HasKey("Key");
|
||||
|
||||
|
@@ -1,92 +0,0 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using API.Schema.NotificationsContext;
|
||||
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.Notifications
|
||||
{
|
||||
[DbContext(typeof(NotificationsContext))]
|
||||
[Migration("20250901194327_KeyLengthChange")]
|
||||
partial class KeyLengthChange
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "9.0.8")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "hstore");
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("API.Schema.NotificationsContext.Notification", b =>
|
||||
{
|
||||
b.Property<string>("Key")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<DateTime>("Date")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<bool>("IsSent")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
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("Key");
|
||||
|
||||
b.ToTable("Notifications");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.NotificationsContext.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");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,36 +0,0 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace API.Migrations.Notifications
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class KeyLengthChange : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "Key",
|
||||
table: "Notifications",
|
||||
type: "character varying(64)",
|
||||
maxLength: 64,
|
||||
nullable: false,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "text");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "Key",
|
||||
table: "Notifications",
|
||||
type: "text",
|
||||
nullable: false,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "character varying(64)",
|
||||
oldMaxLength: 64);
|
||||
}
|
||||
}
|
||||
}
|
@@ -18,7 +18,7 @@ namespace API.Migrations.Notifications
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "9.0.8")
|
||||
.HasAnnotation("ProductVersion", "9.0.5")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "hstore");
|
||||
@@ -27,8 +27,7 @@ namespace API.Migrations.Notifications
|
||||
modelBuilder.Entity("API.Schema.NotificationsContext.Notification", b =>
|
||||
{
|
||||
b.Property<string>("Key")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTime>("Date")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
@@ -1,5 +1,6 @@
|
||||
using System.Reflection;
|
||||
using API;
|
||||
using API.MangaConnectors;
|
||||
using API.Schema.LibraryContext;
|
||||
using API.Schema.MangaContext;
|
||||
using API.Schema.NotificationsContext;
|
||||
@@ -75,7 +76,7 @@ builder.Services.AddControllers().AddNewtonsoftJson(opts =>
|
||||
opts.SerializerSettings.Converters.Add(new StringEnumConverter());
|
||||
opts.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore;
|
||||
});
|
||||
builder.Services.AddScoped<ILog>(_ => LogManager.GetLogger("API"));
|
||||
builder.Services.AddScoped<ILog>(opts => LogManager.GetLogger("API"));
|
||||
|
||||
builder.WebHost.UseUrls("http://*:6531");
|
||||
|
||||
@@ -111,7 +112,7 @@ using (IServiceScope scope = app.Services.CreateScope())
|
||||
if (!context.FileLibraries.Any())
|
||||
context.FileLibraries.Add(new FileLibrary(Tranga.Settings.DownloadLocation, "Default FileLibrary"));
|
||||
|
||||
await context.Sync(CancellationToken.None);
|
||||
context.Sync();
|
||||
}
|
||||
|
||||
using (IServiceScope scope = app.Services.CreateScope())
|
||||
@@ -123,7 +124,7 @@ using (IServiceScope scope = app.Services.CreateScope())
|
||||
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));
|
||||
|
||||
await context.Sync(CancellationToken.None);
|
||||
context.Sync();
|
||||
}
|
||||
|
||||
using (IServiceScope scope = app.Services.CreateScope())
|
||||
@@ -131,11 +132,11 @@ using (IServiceScope scope = app.Services.CreateScope())
|
||||
LibraryContext context = scope.ServiceProvider.GetRequiredService<LibraryContext>();
|
||||
context.Database.Migrate();
|
||||
|
||||
await context.Sync(CancellationToken.None);
|
||||
context.Sync();
|
||||
}
|
||||
|
||||
Tranga.SetServiceProvider(app.Services);
|
||||
Tranga.StartLogger(new FileInfo("Log4Net.config.xml"));
|
||||
Tranga.StartLogger();
|
||||
Tranga.AddDefaultWorkers();
|
||||
|
||||
app.UseCors("AllowAll");
|
||||
|
@@ -1,4 +1,3 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace API.Schema;
|
||||
@@ -6,18 +5,16 @@ namespace API.Schema;
|
||||
[PrimaryKey("Key")]
|
||||
public abstract class Identifiable
|
||||
{
|
||||
protected Identifiable()
|
||||
public Identifiable()
|
||||
{
|
||||
this.Key = TokenGen.CreateToken(this.GetType());
|
||||
}
|
||||
|
||||
protected Identifiable(string key)
|
||||
public Identifiable(string key)
|
||||
{
|
||||
this.Key = key;
|
||||
}
|
||||
|
||||
[Required]
|
||||
[StringLength(TokenGen.MaximumLength, MinimumLength = TokenGen.MinimumLength)]
|
||||
public string Key { get; init; }
|
||||
|
||||
public override string ToString() => Key;
|
||||
|
@@ -9,10 +9,19 @@ namespace API.Schema.LibraryContext.LibraryConnectors;
|
||||
[PrimaryKey("Key")]
|
||||
public abstract class LibraryConnector : Identifiable
|
||||
{
|
||||
[Required]
|
||||
public LibraryType LibraryType { get; init; }
|
||||
[StringLength(256)] [Url] public string BaseUrl { get; init; }
|
||||
[StringLength(256)] public string Auth { get; init; }
|
||||
[NotMapped] protected ILog Log { get; init; }
|
||||
[StringLength(256)]
|
||||
[Required]
|
||||
[Url]
|
||||
public string BaseUrl { get; init; }
|
||||
[StringLength(256)]
|
||||
[Required]
|
||||
public string Auth { get; init; }
|
||||
|
||||
[JsonIgnore]
|
||||
[NotMapped]
|
||||
protected ILog Log { get; init; }
|
||||
|
||||
protected LibraryConnector(LibraryType libraryType, string baseUrl, string auth)
|
||||
: base()
|
||||
|
@@ -7,6 +7,7 @@ namespace API.Schema.MangaContext;
|
||||
public class Author(string authorName) : Identifiable(TokenGen.CreateToken(typeof(Author), authorName))
|
||||
{
|
||||
[StringLength(128)]
|
||||
[Required]
|
||||
public string AuthorName { get; init; } = authorName;
|
||||
|
||||
public override string ToString() => $"{base.ToString()} {AuthorName}";
|
||||
|
@@ -4,27 +4,28 @@ using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Xml.Linq;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace API.Schema.MangaContext;
|
||||
|
||||
[PrimaryKey("Key")]
|
||||
public class Chapter : Identifiable, IComparable<Chapter>
|
||||
{
|
||||
[StringLength(64)] public string ParentMangaId { get; init; } = null!;
|
||||
public Manga ParentManga = null!;
|
||||
[StringLength(64)] [Required] public string ParentMangaId { get; init; } = null!;
|
||||
[JsonIgnore] public Manga ParentManga = null!;
|
||||
|
||||
[NotMapped] public Dictionary<string, string> IdsOnMangaConnectors =>
|
||||
MangaConnectorIds.ToDictionary(id => id.MangaConnectorName, id => id.IdOnConnectorSite);
|
||||
public ICollection<MangaConnectorId<Chapter>> MangaConnectorIds = null!;
|
||||
[JsonIgnore] public ICollection<MangaConnectorId<Chapter>> MangaConnectorIds = null!;
|
||||
|
||||
public int? VolumeNumber { get; private set; }
|
||||
[StringLength(10)] public string ChapterNumber { get; private set; }
|
||||
[StringLength(10)] [Required] public string ChapterNumber { get; private set; }
|
||||
|
||||
[StringLength(256)] public string? Title { get; private set; }
|
||||
|
||||
[StringLength(256)] public string FileName { get; private set; }
|
||||
[StringLength(256)] [Required] public string FileName { get; private set; }
|
||||
|
||||
public bool Downloaded { get; internal set; }
|
||||
[Required] public bool Downloaded { get; internal set; }
|
||||
[NotMapped] public string FullArchiveFilePath => Path.Join(ParentManga.FullDirectoryPath, FileName);
|
||||
|
||||
public Chapter(Manga parentManga, string chapterNumber,
|
||||
@@ -66,27 +67,11 @@ public class Chapter : Identifiable, IComparable<Chapter>
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Checks the filesystem if an archive at the ArchiveFilePath exists
|
||||
/// </summary>
|
||||
/// <param name="context"></param>
|
||||
/// <param name="token"></param>
|
||||
/// <returns>True if archive exists on disk</returns>
|
||||
/// <exception cref="KeyNotFoundException">Unable to load Chapter, Parent or Library</exception>
|
||||
public async Task<bool> CheckDownloaded(MangaContext context, CancellationToken? token = null)
|
||||
{
|
||||
if(await context.Chapters
|
||||
.Include(c => c.ParentManga)
|
||||
.ThenInclude(p => p.Library)
|
||||
.FirstOrDefaultAsync(c => c.Key == this.Key, token??CancellationToken.None) is not { } chapter)
|
||||
throw new KeyNotFoundException("Unable to find chapter");
|
||||
|
||||
//TODO Log here
|
||||
this.Downloaded = File.Exists(chapter.FullArchiveFilePath);
|
||||
await context.Sync(token??CancellationToken.None);
|
||||
return this.Downloaded;
|
||||
}
|
||||
public bool CheckDownloaded() => File.Exists(FullArchiveFilePath);
|
||||
|
||||
/// Placeholders:
|
||||
/// %M Obj Name
|
||||
@@ -156,20 +141,6 @@ public class Chapter : Identifiable, IComparable<Chapter>
|
||||
return stringBuilder.ToString();
|
||||
}
|
||||
|
||||
public class ChapterComparer : IComparer<Chapter>
|
||||
{
|
||||
public int Compare(Chapter? x, Chapter? y)
|
||||
{
|
||||
if (x is null && y is null)
|
||||
return 0;
|
||||
if(x is null)
|
||||
return -1;
|
||||
if (y is null)
|
||||
return 1;
|
||||
return CompareChapterNumbers(x.ChapterNumber, y.ChapterNumber);
|
||||
}
|
||||
}
|
||||
|
||||
private static int CompareChapterNumbers(string ch1, string ch2)
|
||||
{
|
||||
int[] ch1Arr = ch1.Split('.').Select(c => int.TryParse(c, out int result) ? result : -1).ToArray();
|
||||
|
@@ -7,9 +7,13 @@ namespace API.Schema.MangaContext;
|
||||
public class FileLibrary(string basePath, string libraryName)
|
||||
: Identifiable(TokenGen.CreateToken(typeof(FileLibrary), basePath))
|
||||
{
|
||||
[StringLength(256)] public string BasePath { get; internal set; } = basePath;
|
||||
[StringLength(256)]
|
||||
[Required]
|
||||
public string BasePath { get; internal set; } = basePath;
|
||||
|
||||
[StringLength(512)] public string LibraryName { get; internal set; } = libraryName;
|
||||
[StringLength(512)]
|
||||
[Required]
|
||||
public string LibraryName { get; internal set; } = libraryName;
|
||||
|
||||
public override string ToString() => $"{base.ToString()} {LibraryName} - {BasePath}";
|
||||
}
|
@@ -7,8 +7,10 @@ namespace API.Schema.MangaContext;
|
||||
public class Link(string linkProvider, string linkUrl) : Identifiable(TokenGen.CreateToken(typeof(Link), linkProvider, linkUrl))
|
||||
{
|
||||
[StringLength(64)]
|
||||
[Required]
|
||||
public string LinkProvider { get; init; } = linkProvider;
|
||||
[StringLength(2048)]
|
||||
[Required]
|
||||
[Url]
|
||||
public string LinkUrl { get; init; } = linkUrl;
|
||||
|
||||
|
@@ -4,10 +4,8 @@ using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using API.Workers;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.Formats.Jpeg;
|
||||
using SixLabors.ImageSharp.Processing;
|
||||
using SixLabors.ImageSharp.Processing.Processors.Transforms;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Newtonsoft.Json;
|
||||
using static System.IO.UnixFileMode;
|
||||
|
||||
namespace API.Schema.MangaContext;
|
||||
@@ -15,31 +13,34 @@ namespace API.Schema.MangaContext;
|
||||
[PrimaryKey("Key")]
|
||||
public class Manga : Identifiable
|
||||
{
|
||||
[StringLength(512)] public string Name { get; internal set; }
|
||||
[StringLength(512)] [Required] public string Name { get; internal set; }
|
||||
[Required] public string Description { get; internal set; }
|
||||
[Url] [StringLength(512)] public string CoverUrl { get; internal set; }
|
||||
public MangaReleaseStatus ReleaseStatus { get; internal set; }
|
||||
[JsonIgnore] [Url] [StringLength(512)] public string CoverUrl { get; internal set; }
|
||||
[Required] public MangaReleaseStatus ReleaseStatus { get; internal set; }
|
||||
[StringLength(64)] public string? LibraryId { get; private set; }
|
||||
public FileLibrary? Library = null!;
|
||||
[JsonIgnore] public FileLibrary? Library = null!;
|
||||
|
||||
public ICollection<Author> Authors { get; internal set; } = null!;
|
||||
public ICollection<MangaTag> MangaTags { get; internal set; } = null!;
|
||||
public ICollection<Link> Links { get; internal set; } = null!;
|
||||
public ICollection<AltTitle> AltTitles { get; internal set; } = null!;
|
||||
public float IgnoreChaptersBefore { get; internal set; }
|
||||
[Required] public float IgnoreChaptersBefore { get; internal set; }
|
||||
[StringLength(1024)] [Required] public string DirectoryName { get; private set; }
|
||||
[StringLength(512)] public string? CoverFileNameInCache { get; internal set; }
|
||||
[JsonIgnore] [StringLength(512)] public string? CoverFileNameInCache { get; internal set; }
|
||||
public uint? Year { get; internal init; }
|
||||
[StringLength(8)] public string? OriginalLanguage { get; internal init; }
|
||||
|
||||
[NotMapped] public string? FullDirectoryPath => Library is not null ? Path.Join(Library.BasePath, DirectoryName) : null;
|
||||
[JsonIgnore]
|
||||
[NotMapped]
|
||||
public string? FullDirectoryPath => Library is not null ? Path.Join(Library.BasePath, DirectoryName) : null;
|
||||
|
||||
[NotMapped] public ICollection<string> ChapterIds => Chapters.Select(c => c.Key).ToList();
|
||||
public ICollection<Chapter> Chapters = null!;
|
||||
[JsonIgnore] public ICollection<Chapter> Chapters = null!;
|
||||
|
||||
[NotMapped] public Dictionary<string, string> IdsOnMangaConnectors => MangaConnectorIds.ToDictionary(id => id.MangaConnectorName, id => id.IdOnConnectorSite);
|
||||
[NotMapped] public Dictionary<string, string> IdsOnMangaConnectors =>
|
||||
MangaConnectorIds.ToDictionary(id => id.MangaConnectorName, id => id.IdOnConnectorSite);
|
||||
[NotMapped] public ICollection<string> MangaConnectorIdsIds => MangaConnectorIds.Select(id => id.Key).ToList();
|
||||
public ICollection<MangaConnectorId<Manga>> MangaConnectorIds = null!;
|
||||
[JsonIgnore] public ICollection<MangaConnectorId<Manga>> MangaConnectorIds = null!;
|
||||
|
||||
public Manga(string name, string description, string coverUrl, MangaReleaseStatus releaseStatus,
|
||||
ICollection<Author> authors, ICollection<MangaTag> mangaTags, ICollection<Link> links, ICollection<AltTitle> altTitles,
|
||||
@@ -159,18 +160,6 @@ public class Manga : Identifiable
|
||||
return newJobs.ToArray();
|
||||
}
|
||||
|
||||
public async Task<(MemoryStream stream, FileInfo fileInfo)?> GetCoverImage(string cachePath, CancellationToken ct)
|
||||
{
|
||||
string fullPath = Path.Join(cachePath, CoverFileNameInCache);
|
||||
if (!File.Exists(fullPath))
|
||||
return null;
|
||||
|
||||
FileInfo fileInfo = new(fullPath);
|
||||
MemoryStream stream = new (await File.ReadAllBytesAsync(fullPath, ct));
|
||||
|
||||
return (stream, fileInfo);
|
||||
}
|
||||
|
||||
public override string ToString() => $"{base.ToString()} {Name}";
|
||||
}
|
||||
|
||||
|
@@ -1,18 +1,19 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using API.MangaConnectors;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace API.Schema.MangaContext;
|
||||
|
||||
[PrimaryKey("Key")]
|
||||
public class MangaConnectorId<T> : Identifiable where T : Identifiable
|
||||
{
|
||||
public T Obj = null!;
|
||||
[StringLength(64)] public string ObjId { get; internal set; }
|
||||
[StringLength(64)] [Required] public string ObjId { get; internal set; }
|
||||
[JsonIgnore] public T Obj = null!;
|
||||
|
||||
[StringLength(32)] public string MangaConnectorName { get; private set; }
|
||||
[StringLength(32)] [Required] public string MangaConnectorName { get; private set; }
|
||||
|
||||
[StringLength(256)] public string IdOnConnectorSite { get; init; }
|
||||
[StringLength(256)] [Required] public string IdOnConnectorSite { get; init; }
|
||||
[Url] [StringLength(512)] public string? WebsiteUrl { get; internal init; }
|
||||
public bool UseForDownload { get; internal set; }
|
||||
|
||||
|
@@ -104,24 +104,22 @@ public class MangaContext(DbContextOptions<MangaContext> options) : TrangaBaseCo
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
}
|
||||
|
||||
public async Task<Manga?> FindMangaLike(Manga other, CancellationToken token)
|
||||
public Manga? FindMangaLike(Manga other)
|
||||
{
|
||||
if (await MangaIncludeAll().FirstOrDefaultAsync(m => m.Key == other.Key, token) is { } f)
|
||||
if (MangaIncludeAll().FirstOrDefault(m => m.Key == other.Key) is { } f)
|
||||
return f;
|
||||
|
||||
return await MangaIncludeAll()
|
||||
.FirstOrDefaultAsync(m =>
|
||||
m.Links.Any(l => l.Key == other.Key) ||
|
||||
m.AltTitles.Any(t => other.AltTitles.Select(ot => ot.Title).Any(s => s.Equals(t.Title))), token);
|
||||
return MangaIncludeAll()
|
||||
.FirstOrDefault(m => m.Links.Any(l => l.Key == other.Key) ||
|
||||
m.AltTitles.Any(t => other.AltTitles.Select(ot => ot.Title)
|
||||
.Any(s => s.Equals(t.Title))));
|
||||
}
|
||||
|
||||
public IIncludableQueryable<Manga, ICollection<MangaConnectorId<Manga>>> MangaIncludeAll() =>
|
||||
Mangas
|
||||
.Include(m => m.Library)
|
||||
.Include(m => m.Authors)
|
||||
.Include(m => m.MangaTags)
|
||||
.Include(m => m.Links)
|
||||
.Include(m => m.AltTitles)
|
||||
.Include(m => m.Chapters)
|
||||
.Include(m => m.MangaConnectorIds);
|
||||
public IIncludableQueryable<Manga, ICollection<MangaConnectorId<Manga>>> MangaIncludeAll() => Mangas.Include(m => m.Library)
|
||||
.Include(m => m.Authors)
|
||||
.Include(m => m.MangaTags)
|
||||
.Include(m => m.Links)
|
||||
.Include(m => m.AltTitles)
|
||||
.Include(m => m.Chapters)
|
||||
.Include(m => m.MangaConnectorIds);
|
||||
}
|
@@ -32,6 +32,4 @@ public class MetadataEntry
|
||||
this.Identifier = identifier;
|
||||
this.MetadataFetcherName = metadataFetcherName;
|
||||
}
|
||||
|
||||
public override string ToString() => $"{GetType().FullName} {MangaId} {MetadataFetcherName}";
|
||||
}
|
@@ -31,5 +31,5 @@ public abstract class MetadataFetcher
|
||||
/// <summary>
|
||||
/// Updates the Manga linked in the MetadataEntry
|
||||
/// </summary>
|
||||
public abstract Task UpdateMetadata(MetadataEntry metadataEntry, MangaContext dbContext, CancellationToken token);
|
||||
public abstract void UpdateMetadata(MetadataEntry metadataEntry, MangaContext dbContext);
|
||||
}
|
@@ -43,49 +43,42 @@ public class MyAnimeList : MetadataFetcher
|
||||
/// </summary>
|
||||
/// <param name="metadataEntry"></param>
|
||||
/// <param name="dbContext"></param>
|
||||
/// <param name="token"></param>
|
||||
/// <exception cref="FormatException"></exception>
|
||||
/// <exception cref="DbUpdateException"></exception>
|
||||
public override async Task UpdateMetadata(MetadataEntry metadataEntry, MangaContext dbContext, CancellationToken token)
|
||||
public override void UpdateMetadata(MetadataEntry metadataEntry, MangaContext dbContext)
|
||||
{
|
||||
Manga? dbManga = metadataEntry.Manga; //Might be null!
|
||||
if (dbManga is null)
|
||||
{
|
||||
if (await dbContext.Mangas.FirstOrDefaultAsync(m => m.Key == metadataEntry.MangaId, token) is not
|
||||
{ } update)
|
||||
throw new DbUpdateException("Manga not found");
|
||||
dbManga = update;
|
||||
}
|
||||
Manga dbManga = dbContext.Mangas.Find(metadataEntry.MangaId)!;
|
||||
|
||||
// Load all collections (tags, links, authors)...
|
||||
foreach (CollectionEntry collectionEntry in dbContext.Entry(dbManga).Collections)
|
||||
{
|
||||
if(!collectionEntry.IsLoaded)
|
||||
await collectionEntry.LoadAsync(token);
|
||||
}
|
||||
await dbContext.Entry(dbManga).Reference(m => m.Library).LoadAsync(token);
|
||||
collectionEntry.Load();
|
||||
dbContext.Entry(dbManga).Navigation(nameof(Manga.Library)).Load();
|
||||
|
||||
MangaFull resultData;
|
||||
try
|
||||
{
|
||||
long id = long.Parse(metadataEntry.Identifier);
|
||||
if(await Jikan.GetMangaFullDataAsync(id, token) is not { } response)
|
||||
throw new DbUpdateException("Manga Data not found");
|
||||
resultData = response.Data;
|
||||
resultData = Jikan.GetMangaFullDataAsync(id).Result.Data;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
throw new FormatException("ID was not in correct format");
|
||||
}
|
||||
|
||||
dbManga.Name = resultData.Titles.First().Title;
|
||||
dbManga.Description = resultData.Synopsis;
|
||||
dbManga.AltTitles.Clear();
|
||||
dbManga.AltTitles = resultData.Titles.Select(t => new AltTitle(t.Type, t.Title)).ToList();
|
||||
dbManga.Authors.Clear();
|
||||
dbManga.Authors = resultData.Authors.Select(a => new Author(a.Name)).ToList();
|
||||
try
|
||||
{
|
||||
dbManga.Name = resultData.Titles.First().Title;
|
||||
dbManga.Description = resultData.Synopsis;
|
||||
dbManga.AltTitles.Clear();
|
||||
dbManga.AltTitles = resultData.Titles.Select(t => new AltTitle(t.Type, t.Title)).ToList();
|
||||
dbManga.Authors.Clear();
|
||||
dbManga.Authors = resultData.Authors.Select(a => new Author(a.Name)).ToList();
|
||||
|
||||
await dbContext.Sync(token);
|
||||
dbContext.Sync();
|
||||
}
|
||||
catch (DbUpdateException e)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -10,22 +10,36 @@ namespace API.Schema.NotificationsContext.NotificationConnectors;
|
||||
[PrimaryKey("Name")]
|
||||
public class NotificationConnector(string name, string url, Dictionary<string, string> headers, string httpMethod, string body)
|
||||
{
|
||||
[StringLength(64)] public string Name { get; init; } = name;
|
||||
[StringLength(64)]
|
||||
[Required]
|
||||
public string Name { get; init; } = name;
|
||||
|
||||
[StringLength(2048)] [Url] public string Url { get; internal set; } = url;
|
||||
[StringLength(2048)]
|
||||
[Required]
|
||||
[Url]
|
||||
public string Url { get; internal set; } = url;
|
||||
|
||||
[Required] public Dictionary<string, string> Headers { get; internal set; } = headers;
|
||||
[Required]
|
||||
public Dictionary<string, string> Headers { get; internal set; } = headers;
|
||||
|
||||
[StringLength(8)] public string HttpMethod { get; internal set; } = httpMethod;
|
||||
[StringLength(8)]
|
||||
[Required]
|
||||
public string HttpMethod { get; internal set; } = httpMethod;
|
||||
|
||||
[StringLength(4096)] public string Body { get; internal set; } = body;
|
||||
[StringLength(4096)]
|
||||
[Required]
|
||||
public string Body { get; internal set; } = body;
|
||||
|
||||
[NotMapped] private readonly HttpClient Client = new()
|
||||
[JsonIgnore]
|
||||
[NotMapped]
|
||||
private readonly HttpClient Client = new()
|
||||
{
|
||||
DefaultRequestHeaders = { { "User-Agent", Tranga.Settings.UserAgent } }
|
||||
};
|
||||
|
||||
[JsonIgnore] protected ILog Log = LogManager.GetLogger(name);
|
||||
[JsonIgnore]
|
||||
[NotMapped]
|
||||
protected ILog Log = LogManager.GetLogger(name);
|
||||
|
||||
public void SendNotification(string title, string notificationText)
|
||||
{
|
||||
|
@@ -22,11 +22,11 @@ public abstract class TrangaBaseContext<T> : DbContext where T : DbContext
|
||||
}, Array.Empty<string>(), LogLevel.Warning, DbContextLoggerOptions.Level | DbContextLoggerOptions.Category | DbContextLoggerOptions.UtcTime);
|
||||
}
|
||||
|
||||
internal async Task<(bool success, string? exceptionMessage)> Sync(CancellationToken token)
|
||||
internal (bool success, string? exceptionMessage) Sync()
|
||||
{
|
||||
try
|
||||
{
|
||||
await this.SaveChangesAsync(token);
|
||||
this.SaveChanges();
|
||||
return (true, null);
|
||||
}
|
||||
catch (Exception e)
|
||||
|
@@ -5,8 +5,8 @@ namespace API;
|
||||
|
||||
public static class TokenGen
|
||||
{
|
||||
public const int MinimumLength = 16;
|
||||
public const int MaximumLength = 64;
|
||||
private const int MinimumLength = 16;
|
||||
private const int MaximumLength = 64;
|
||||
private const string Chars = "abcdefghijklmnopqrstuvwxyz0123456789";
|
||||
|
||||
public static string CreateToken(Type t, params string[] identifiers) => CreateToken(t.Name, identifiers);
|
||||
|
@@ -15,6 +15,16 @@ namespace API;
|
||||
|
||||
public static class Tranga
|
||||
{
|
||||
|
||||
// ReSharper disable once InconsistentNaming
|
||||
private const string TRANGA =
|
||||
"\n\n" +
|
||||
" _______ v2\n" +
|
||||
"|_ _|.----..---.-..-----..-----..---.-.\n" +
|
||||
" | | | _|| _ || || _ || _ |\n" +
|
||||
" |___| |__| |___._||__|__||___ ||___._|\n" +
|
||||
" |_____| \n\n";
|
||||
|
||||
private static IServiceProvider? ServiceProvider;
|
||||
|
||||
private static readonly ILog Log = LogManager.GetLogger(typeof(Tranga));
|
||||
@@ -31,11 +41,11 @@ public static class Tranga
|
||||
internal static readonly RemoveOldNotificationsWorker RemoveOldNotificationsWorker = new();
|
||||
internal static readonly UpdateCoversWorker UpdateCoversWorker = new();
|
||||
|
||||
internal static void StartLogger(FileInfo loggerConfigFile)
|
||||
internal static void StartLogger()
|
||||
{
|
||||
XmlConfigurator.ConfigureAndWatch(loggerConfigFile);
|
||||
BasicConfigurator.Configure();
|
||||
Log.Info("Logger Configured.");
|
||||
Log.Info(Constants.TRANGA);
|
||||
Log.Info(TRANGA);
|
||||
}
|
||||
|
||||
internal static void AddDefaultWorkers()
|
||||
@@ -116,12 +126,6 @@ public static class Tranga
|
||||
}
|
||||
Action afterWorkCallback = AfterWork(worker, callback);
|
||||
|
||||
while (RunningWorkers.Count > Settings.MaxConcurrentWorkers)
|
||||
{
|
||||
Log.Warn($"{worker}: Max worker concurrency reached ({Settings.MaxConcurrentWorkers})! Waiting {Settings.WorkCycleTimeoutMs}ms...");
|
||||
Thread.Sleep(Settings.WorkCycleTimeoutMs);
|
||||
}
|
||||
|
||||
if (worker is BaseWorkerWithContext<MangaContext> mangaContextWorker)
|
||||
{
|
||||
mangaContextWorker.SetScope(ServiceProvider.CreateScope());
|
||||
@@ -141,24 +145,6 @@ public static class Tranga
|
||||
private static Action AfterWork(BaseWorker worker, Action? callback = null) => () =>
|
||||
{
|
||||
Log.Debug($"AfterWork {worker}");
|
||||
if (RunningWorkers.TryGetValue(worker, out Task<BaseWorker[]>? task))
|
||||
{
|
||||
Log.Debug($"Waiting for Children to exit {worker}");
|
||||
task.Wait();
|
||||
if (task.IsCompleted)
|
||||
{
|
||||
try
|
||||
{
|
||||
BaseWorker[] newWorkers = task.Result;
|
||||
Log.Debug($"{worker} created {newWorkers.Length} new Workers.");
|
||||
AddWorkers(newWorkers);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e);
|
||||
}
|
||||
}else Log.Warn($"Children failed: {worker}");
|
||||
}
|
||||
RunningWorkers.Remove(worker, out _);
|
||||
callback?.Invoke();
|
||||
};
|
||||
@@ -172,14 +158,12 @@ public static class Tranga
|
||||
RunningWorkers.Remove(worker, out _);
|
||||
}
|
||||
|
||||
internal static async Task<bool> AddMangaToContext(this MangaContext context, (Manga, MangaConnectorId<Manga>) addManga, CancellationToken token) =>
|
||||
await AddMangaToContext(context, addManga.Item1, addManga.Item2, token);
|
||||
internal static bool AddMangaToContext((Manga, MangaConnectorId<Manga>) addManga, MangaContext context, [NotNullWhen(true)]out Manga? manga) => AddMangaToContext(addManga.Item1, addManga.Item2, context, out manga);
|
||||
|
||||
internal static async Task<bool> AddMangaToContext(this MangaContext context, Manga addManga, MangaConnectorId<Manga> addMcId,
|
||||
CancellationToken token)
|
||||
internal static bool AddMangaToContext(Manga addManga, MangaConnectorId<Manga> addMcId, MangaContext context, [NotNullWhen(true)]out Manga? manga)
|
||||
{
|
||||
context.ChangeTracker.Clear();
|
||||
Manga? manga = await context.FindMangaLike(addManga, token);
|
||||
manga = context.FindMangaLike(addManga);
|
||||
if (manga is not null)
|
||||
{
|
||||
foreach (MangaConnectorId<Manga> mcId in addManga.MangaConnectorIds)
|
||||
@@ -193,8 +177,6 @@ public static class Tranga
|
||||
manga.AltTitles = manga.AltTitles.UnionBy(addManga.AltTitles, altTitle => altTitle.Key).ToList();
|
||||
manga.Chapters = manga.Chapters.UnionBy(addManga.Chapters, chapter => chapter.Key).ToList();
|
||||
manga.MangaConnectorIds = manga.MangaConnectorIds.UnionBy(addManga.MangaConnectorIds, id => id.MangaConnectorName).ToList();
|
||||
|
||||
addManga = manga;
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -216,7 +198,7 @@ public static class Tranga
|
||||
context.Mangas.Add(manga);
|
||||
}
|
||||
|
||||
if (await context.Sync(token) is { success: false })
|
||||
if (context.Sync() is { success: false })
|
||||
return false;
|
||||
|
||||
DownloadCoverFromMangaconnectorWorker downloadCoverWorker = new (addMcId);
|
||||
@@ -224,4 +206,28 @@ public static class Tranga
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
internal static bool AddChapterToContext((Chapter, MangaConnectorId<Chapter>) addChapter, MangaContext context,
|
||||
[NotNullWhen(true)] out Chapter? chapter) => AddChapterToContext(addChapter.Item1, addChapter.Item2, context, out chapter);
|
||||
|
||||
internal static bool AddChapterToContext(Chapter addChapter, MangaConnectorId<Chapter> addChId, MangaContext context, [NotNullWhen(true)] out Chapter? chapter)
|
||||
{
|
||||
chapter = context.Chapters.Where(ch => ch.Key == addChapter.Key)
|
||||
.Include(ch => ch.ParentManga)
|
||||
.Include(ch => ch.MangaConnectorIds)
|
||||
.FirstOrDefault();
|
||||
if (chapter is not null)
|
||||
{
|
||||
chapter.MangaConnectorIds = chapter.MangaConnectorIds.UnionBy(addChapter.MangaConnectorIds, id => id.Key).ToList();
|
||||
}
|
||||
else
|
||||
{
|
||||
context.Chapters.Add(addChapter);
|
||||
chapter = addChapter;
|
||||
}
|
||||
|
||||
if (context.Sync() is { success: false })
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
}
|
@@ -11,11 +11,8 @@ public struct TrangaSettings()
|
||||
public static string workingDirectory => Path.Join(RuntimeInformation.IsOSPlatform(OSPlatform.Linux) ? "/usr/share" : Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "tranga-api");
|
||||
[JsonIgnore]
|
||||
public static string settingsFilePath => Path.Join(workingDirectory, "settings.json");
|
||||
[JsonIgnore] public static string coverImageCache => Path.Join(workingDirectory, "imageCache");
|
||||
[JsonIgnore] public static string coverImageCacheOriginal => Path.Join(coverImageCache, "original");
|
||||
[JsonIgnore] public static string coverImageCacheLarge => Path.Join(coverImageCache, "large");
|
||||
[JsonIgnore] public static string coverImageCacheMedium => Path.Join(coverImageCache, "medium");
|
||||
[JsonIgnore] public static string coverImageCacheSmall => Path.Join(coverImageCache, "small");
|
||||
[JsonIgnore]
|
||||
public static string coverImageCache => Path.Join(workingDirectory, "imageCache");
|
||||
public string DownloadLocation => RuntimeInformation.IsOSPlatform(OSPlatform.Linux) ? "/Manga" : Path.Join(Directory.GetCurrentDirectory(), "Manga");
|
||||
[JsonIgnore]
|
||||
internal static readonly string DefaultUserAgent = $"Tranga/2.0 ({Enum.GetName(Environment.OSVersion.Platform)}; {(Environment.Is64BitOperatingSystem ? "x64" : "")})";
|
||||
@@ -42,21 +39,17 @@ public struct TrangaSettings()
|
||||
[JsonIgnore]
|
||||
internal static readonly Dictionary<RequestType, int> DefaultRequestLimits = new ()
|
||||
{
|
||||
{RequestType.MangaInfo, 360},
|
||||
{RequestType.MangaDexFeed, 360},
|
||||
{RequestType.MangaInfo, 60},
|
||||
{RequestType.MangaDexFeed, 60},
|
||||
{RequestType.MangaDexImage, 60},
|
||||
{RequestType.MangaImage, 240},
|
||||
{RequestType.MangaCover, 60},
|
||||
{RequestType.Default, 360}
|
||||
{RequestType.Default, 60}
|
||||
};
|
||||
public Dictionary<RequestType, int> RequestLimits { get; set; } = DefaultRequestLimits;
|
||||
|
||||
public string DownloadLanguage { get; set; } = "en";
|
||||
|
||||
public int MaxConcurrentDownloads { get; set; } = 5;
|
||||
|
||||
public int MaxConcurrentWorkers { get; set; } = 10;
|
||||
|
||||
public static TrangaSettings Load()
|
||||
{
|
||||
if (!File.Exists(settingsFilePath))
|
||||
@@ -116,10 +109,4 @@ public struct TrangaSettings()
|
||||
this.DownloadLanguage = language;
|
||||
Save();
|
||||
}
|
||||
|
||||
public void SetMaxConcurrentDownloads(int value)
|
||||
{
|
||||
this.MaxConcurrentDownloads = value;
|
||||
Save();
|
||||
}
|
||||
}
|
@@ -8,23 +8,22 @@ public abstract class BaseWorker : Identifiable
|
||||
/// <summary>
|
||||
/// Workers this Worker depends on being completed before running.
|
||||
/// </summary>
|
||||
private BaseWorker[] DependsOn { get; init; }
|
||||
public BaseWorker[] DependsOn { get; init; }
|
||||
/// <summary>
|
||||
/// Dependencies and dependencies of dependencies. See also <see cref="DependsOn"/>.
|
||||
/// </summary>
|
||||
internal IEnumerable<BaseWorker> AllDependencies => DependsOn.Select(d => d.AllDependencies).SelectMany(x => x);
|
||||
public IEnumerable<BaseWorker> AllDependencies => DependsOn.Select(d => d.AllDependencies).SelectMany(x => x);
|
||||
/// <summary>
|
||||
/// <see cref="AllDependencies"/> and Self.
|
||||
/// </summary>
|
||||
internal IEnumerable<BaseWorker> DependenciesAndSelf => AllDependencies.Append(this);
|
||||
public IEnumerable<BaseWorker> DependenciesAndSelf => AllDependencies.Append(this);
|
||||
/// <summary>
|
||||
/// <see cref="DependsOn"/> where <see cref="WorkerExecutionState"/> is less than Completed.
|
||||
/// </summary>
|
||||
internal IEnumerable<BaseWorker> MissingDependencies => DependsOn.Where(d => d.State < WorkerExecutionState.Completed);
|
||||
public IEnumerable<BaseWorker> MissingDependencies => DependsOn.Where(d => d.State < WorkerExecutionState.Completed);
|
||||
public bool AllDependenciesFulfilled => DependsOn.All(d => d.State >= WorkerExecutionState.Completed);
|
||||
internal WorkerExecutionState State { get; private set; }
|
||||
private CancellationTokenSource _cancellationTokenSource = new ();
|
||||
protected CancellationToken CancellationToken => _cancellationTokenSource.Token;
|
||||
private CancellationTokenSource? CancellationTokenSource = null;
|
||||
protected ILog Log { get; init; }
|
||||
|
||||
/// <summary>
|
||||
@@ -34,7 +33,7 @@ public abstract class BaseWorker : Identifiable
|
||||
{
|
||||
Log.Debug($"Cancelled {this}");
|
||||
this.State = WorkerExecutionState.Cancelled;
|
||||
_cancellationTokenSource.Cancel();
|
||||
CancellationTokenSource?.Cancel();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -44,10 +43,10 @@ public abstract class BaseWorker : Identifiable
|
||||
{
|
||||
Log.Debug($"Failed {this}");
|
||||
this.State = WorkerExecutionState.Failed;
|
||||
_cancellationTokenSource.Cancel();
|
||||
CancellationTokenSource?.Cancel();
|
||||
}
|
||||
|
||||
protected BaseWorker(IEnumerable<BaseWorker>? dependsOn = null)
|
||||
public BaseWorker(IEnumerable<BaseWorker>? dependsOn = null)
|
||||
{
|
||||
this.DependsOn = dependsOn?.ToArray() ?? [];
|
||||
this.Log = LogManager.GetLogger(GetType());
|
||||
@@ -73,23 +72,24 @@ public abstract class BaseWorker : Identifiable
|
||||
{
|
||||
// Start the worker
|
||||
Log.Debug($"Checking {this}");
|
||||
_cancellationTokenSource = new(TimeSpan.FromMinutes(10));
|
||||
State = WorkerExecutionState.Waiting;
|
||||
this.CancellationTokenSource = new(TimeSpan.FromMinutes(10));
|
||||
this.State = WorkerExecutionState.Waiting;
|
||||
|
||||
// Wait for dependencies, start them if necessary
|
||||
BaseWorker[] missingDependenciesThatNeedStarting = MissingDependencies.Where(d => d.State < WorkerExecutionState.Waiting).ToArray();
|
||||
if(missingDependenciesThatNeedStarting.Any())
|
||||
return new (() => missingDependenciesThatNeedStarting);
|
||||
return new Task<BaseWorker[]>(() => missingDependenciesThatNeedStarting);
|
||||
|
||||
if (MissingDependencies.Any())
|
||||
return new (WaitForDependencies);
|
||||
return new Task<BaseWorker[]>(WaitForDependencies);
|
||||
|
||||
// Run the actual work
|
||||
Log.Info($"Running {this}");
|
||||
DateTime startTime = DateTime.UtcNow;
|
||||
State = WorkerExecutionState.Running;
|
||||
Task<BaseWorker[]> task = DoWorkInternal();
|
||||
Task<BaseWorker[]> task = new (DoWorkInternal, CancellationTokenSource.Token);
|
||||
task.GetAwaiter().OnCompleted(Finish(startTime, callback));
|
||||
this.State = WorkerExecutionState.Running;
|
||||
task.Start();
|
||||
return task;
|
||||
}
|
||||
|
||||
@@ -103,12 +103,12 @@ public abstract class BaseWorker : Identifiable
|
||||
callback?.Invoke();
|
||||
};
|
||||
|
||||
protected abstract Task<BaseWorker[]> DoWorkInternal();
|
||||
protected abstract BaseWorker[] DoWorkInternal();
|
||||
|
||||
private BaseWorker[] WaitForDependencies()
|
||||
{
|
||||
Log.Info($"Waiting for {MissingDependencies.Count()} Dependencies {this}:\n\t{string.Join("\n\t", MissingDependencies.Select(d => d.ToString()))}");
|
||||
while (_cancellationTokenSource.IsCancellationRequested == false && MissingDependencies.Any())
|
||||
while (CancellationTokenSource?.IsCancellationRequested == false && MissingDependencies.Any())
|
||||
{
|
||||
Thread.Sleep(Tranga.Settings.WorkCycleTimeoutMs);
|
||||
}
|
||||
|
@@ -3,7 +3,6 @@ using System.Runtime.InteropServices;
|
||||
using API.MangaConnectors;
|
||||
using API.MangaDownloadClients;
|
||||
using API.Schema.MangaContext;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.ChangeTracking;
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.Formats.Jpeg;
|
||||
@@ -13,49 +12,28 @@ using static System.IO.UnixFileMode;
|
||||
|
||||
namespace API.Workers;
|
||||
|
||||
/// <summary>
|
||||
/// Downloads single chapter for Manga from Mangaconnector
|
||||
/// </summary>
|
||||
/// <param name="chId"></param>
|
||||
/// <param name="dependsOn"></param>
|
||||
public class DownloadChapterFromMangaconnectorWorker(MangaConnectorId<Chapter> chId, IEnumerable<BaseWorker>? dependsOn = null)
|
||||
: BaseWorkerWithContext<MangaContext>(dependsOn)
|
||||
{
|
||||
internal readonly string MangaConnectorIdId = chId.Key;
|
||||
protected override async Task<BaseWorker[]> DoWorkInternal()
|
||||
protected override BaseWorker[] DoWorkInternal()
|
||||
{
|
||||
Log.Debug($"Downloading chapter for MangaConnectorId {MangaConnectorIdId}...");
|
||||
// Getting MangaConnector info
|
||||
if (await DbContext.MangaConnectorToChapter
|
||||
.Include(id => id.Obj)
|
||||
.ThenInclude(c => c.ParentManga)
|
||||
.ThenInclude(m => m.Library)
|
||||
.FirstOrDefaultAsync(c => c.Key == MangaConnectorIdId, CancellationToken) is not { } mangaConnectorId)
|
||||
{
|
||||
Log.Error("Could not get MangaConnectorId.");
|
||||
if (DbContext.MangaConnectorToChapter.Find(MangaConnectorIdId) is not { } mangaConnectorId)
|
||||
return []; //TODO Exception?
|
||||
}
|
||||
|
||||
// Check if Chapter already exists...
|
||||
if (await mangaConnectorId.Obj.CheckDownloaded(DbContext, CancellationToken))
|
||||
{
|
||||
Log.Warn("Chapter already exists!");
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!Tranga.TryGetMangaConnector(mangaConnectorId.MangaConnectorName, out MangaConnector? mangaConnector))
|
||||
{
|
||||
Log.Error("Could not get MangaConnector.");
|
||||
return []; //TODO Exception?
|
||||
}
|
||||
Log.Debug($"Downloading chapter for MangaConnectorId {mangaConnectorId}...");
|
||||
|
||||
DbContext.Entry(mangaConnectorId).Navigation(nameof(MangaConnectorId<Chapter>.Obj)).Load();
|
||||
Chapter chapter = mangaConnectorId.Obj;
|
||||
if (chapter.Downloaded)
|
||||
{
|
||||
Log.Info("Chapter was already downloaded.");
|
||||
return [];
|
||||
}
|
||||
|
||||
DbContext.Entry(chapter).Navigation(nameof(Chapter.ParentManga)).Load();
|
||||
DbContext.Entry(chapter.ParentManga).Navigation(nameof(Manga.Library)).Load();
|
||||
|
||||
if (chapter.ParentManga.LibraryId is null)
|
||||
{
|
||||
Log.Info($"Library is not set for {chapter.ParentManga} {chapter}");
|
||||
@@ -66,9 +44,6 @@ public class DownloadChapterFromMangaconnectorWorker(MangaConnectorId<Chapter> c
|
||||
if (imageUrls.Length < 1)
|
||||
{
|
||||
Log.Info($"No imageUrls for chapter {chapter}");
|
||||
mangaConnectorId.UseForDownload = false; // Do not try to download from this again
|
||||
if(await DbContext.Sync(CancellationToken) is { success: false } result)
|
||||
Log.Error(result.exceptionMessage);
|
||||
return [];
|
||||
}
|
||||
string saveArchiveFilePath = chapter.FullArchiveFilePath;
|
||||
@@ -117,12 +92,13 @@ public class DownloadChapterFromMangaconnectorWorker(MangaConnectorId<Chapter> c
|
||||
}
|
||||
}
|
||||
|
||||
await CopyCoverFromCacheToDownloadLocation(chapter.ParentManga);
|
||||
CopyCoverFromCacheToDownloadLocation(chapter.ParentManga);
|
||||
|
||||
Log.Debug($"Creating ComicInfo.xml {chapter}");
|
||||
foreach (CollectionEntry collectionEntry in DbContext.Entry(chapter.ParentManga).Collections)
|
||||
await collectionEntry.LoadAsync(CancellationToken);
|
||||
await File.WriteAllTextAsync(Path.Join(tempFolder, "ComicInfo.xml"), chapter.GetComicInfoXmlString(), CancellationToken);
|
||||
collectionEntry.Load();
|
||||
DbContext.Entry(chapter.ParentManga).Navigation(nameof(Manga.Library)).Load();
|
||||
File.WriteAllText(Path.Join(tempFolder, "ComicInfo.xml"), chapter.GetComicInfoXmlString());
|
||||
|
||||
Log.Debug($"Packaging images to archive {chapter}");
|
||||
//ZIP-it and ship-it
|
||||
@@ -131,11 +107,8 @@ public class DownloadChapterFromMangaconnectorWorker(MangaConnectorId<Chapter> c
|
||||
File.SetUnixFileMode(saveArchiveFilePath, UserRead | UserWrite | UserExecute | GroupRead | GroupWrite | GroupExecute | OtherRead | OtherExecute);
|
||||
Directory.Delete(tempFolder, true); //Cleanup
|
||||
|
||||
DbContext.Entry(chapter).Property(c => c.Downloaded).CurrentValue = true;
|
||||
if(await DbContext.Sync(CancellationToken) is { success: false } e)
|
||||
Log.Error($"Failed to save database changes: {e.exceptionMessage}");
|
||||
|
||||
Log.Debug($"Downloaded chapter {chapter}.");
|
||||
chapter.Downloaded = true;
|
||||
DbContext.Sync();
|
||||
|
||||
return [];
|
||||
}
|
||||
@@ -178,7 +151,7 @@ public class DownloadChapterFromMangaconnectorWorker(MangaConnectorId<Chapter> c
|
||||
}
|
||||
}
|
||||
|
||||
private async Task CopyCoverFromCacheToDownloadLocation(Manga manga)
|
||||
private void CopyCoverFromCacheToDownloadLocation(Manga manga)
|
||||
{
|
||||
//Check if Publication already has a Folder and cover
|
||||
string publicationFolder = manga.CreatePublicationFolder();
|
||||
@@ -190,7 +163,7 @@ public class DownloadChapterFromMangaconnectorWorker(MangaConnectorId<Chapter> c
|
||||
}
|
||||
|
||||
//TODO MangaConnector Selection
|
||||
await DbContext.Entry(manga).Collection(m => m.MangaConnectorIds).LoadAsync(CancellationToken);
|
||||
DbContext.Entry(manga).Collection(m => m.MangaConnectorIds).Load();
|
||||
MangaConnectorId<Manga> mangaConnectorId = manga.MangaConnectorIds.First();
|
||||
if (!Tranga.TryGetMangaConnector(mangaConnectorId.MangaConnectorName, out MangaConnector? mangaConnector))
|
||||
{
|
||||
@@ -199,20 +172,19 @@ public class DownloadChapterFromMangaconnectorWorker(MangaConnectorId<Chapter> c
|
||||
}
|
||||
|
||||
Log.Info($"Copying cover to {publicationFolder}");
|
||||
await DbContext.Entry(mangaConnectorId).Navigation(nameof(MangaConnectorId<Manga>.Obj)).LoadAsync(CancellationToken);
|
||||
string? coverFileNameInCache = manga.CoverFileNameInCache ?? mangaConnector.SaveCoverImageToCache(mangaConnectorId);
|
||||
if (coverFileNameInCache is null)
|
||||
DbContext.Entry(mangaConnectorId).Navigation(nameof(MangaConnectorId<Manga>.Obj)).Load();
|
||||
string? fileInCache = manga.CoverFileNameInCache ?? mangaConnector.SaveCoverImageToCache(mangaConnectorId);
|
||||
if (fileInCache is null)
|
||||
{
|
||||
Log.Error($"File {coverFileNameInCache} does not exist");
|
||||
Log.Error($"File {fileInCache} does not exist");
|
||||
return;
|
||||
}
|
||||
|
||||
string fullCoverPath = Path.Join(TrangaSettings.coverImageCacheOriginal, coverFileNameInCache);
|
||||
string newFilePath = Path.Join(publicationFolder, $"cover.{Path.GetFileName(coverFileNameInCache).Split('.')[^1]}" );
|
||||
File.Copy(fullCoverPath, newFilePath, true);
|
||||
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 | OtherRead | OtherWrite);
|
||||
Log.Debug($"Copied cover from {fullCoverPath} to {newFilePath}");
|
||||
Log.Debug($"Copied cover from {fileInCache} to {newFilePath}");
|
||||
}
|
||||
|
||||
private bool DownloadImage(string imageUrl, string savePath)
|
||||
|
@@ -1,45 +1,25 @@
|
||||
using API.MangaConnectors;
|
||||
using API.Schema.MangaContext;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace API.Workers;
|
||||
|
||||
/// <summary>
|
||||
/// Downloads the cover for Manga from Mangaconnector
|
||||
/// </summary>
|
||||
public class DownloadCoverFromMangaconnectorWorker(MangaConnectorId<Manga> mcId, IEnumerable<BaseWorker>? dependsOn = null)
|
||||
: BaseWorkerWithContext<MangaContext>(dependsOn)
|
||||
{
|
||||
internal readonly string MangaConnectorIdId = mcId.Key;
|
||||
protected override async Task<BaseWorker[]> DoWorkInternal()
|
||||
protected override BaseWorker[] DoWorkInternal()
|
||||
{
|
||||
Log.Debug($"Getting Cover for MangaConnectorId {MangaConnectorIdId}...");
|
||||
// Getting MangaConnector info
|
||||
if (await DbContext.MangaConnectorToManga
|
||||
.Include(id => id.Obj)
|
||||
.FirstOrDefaultAsync(c => c.Key == MangaConnectorIdId, CancellationToken) is not { } mangaConnectorId)
|
||||
{
|
||||
Log.Error("Could not get MangaConnectorId.");
|
||||
if (DbContext.MangaConnectorToManga.Find(MangaConnectorIdId) is not { } mangaConnectorId)
|
||||
return []; //TODO Exception?
|
||||
}
|
||||
if (!Tranga.TryGetMangaConnector(mangaConnectorId.MangaConnectorName, out MangaConnector? mangaConnector))
|
||||
{
|
||||
Log.Error("Could not get MangaConnector.");
|
||||
return []; //TODO Exception?
|
||||
}
|
||||
Log.Debug($"Getting Cover for MangaConnectorId {mangaConnectorId}...");
|
||||
|
||||
string? coverFileName = mangaConnector.SaveCoverImageToCache(mangaConnectorId);
|
||||
if (coverFileName is null)
|
||||
{
|
||||
Log.Error($"Could not get Cover for MangaConnectorId {mangaConnectorId}.");
|
||||
return [];
|
||||
}
|
||||
DbContext.Entry(mangaConnectorId.Obj).Property(m => m.CoverFileNameInCache).CurrentValue = coverFileName;
|
||||
DbContext.Entry(mangaConnectorId).Navigation(nameof(MangaConnectorId<Manga>.Obj)).Load();
|
||||
Manga manga = mangaConnectorId.Obj;
|
||||
|
||||
if(await DbContext.Sync(CancellationToken) is { success: false } e)
|
||||
Log.Error($"Failed to save database changes: {e.exceptionMessage}");
|
||||
manga.CoverFileNameInCache = mangaConnector.SaveCoverImageToCache(mangaConnectorId);
|
||||
|
||||
DbContext.Sync();
|
||||
return [];
|
||||
}
|
||||
|
||||
|
@@ -1,80 +1,37 @@
|
||||
using API.MangaConnectors;
|
||||
using API.Schema.MangaContext;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace API.Workers;
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the metadata of available chapters on the Mangaconnector
|
||||
/// </summary>
|
||||
/// <param name="mcId"></param>
|
||||
/// <param name="language"></param>
|
||||
/// <param name="dependsOn"></param>
|
||||
public class RetrieveMangaChaptersFromMangaconnectorWorker(MangaConnectorId<Manga> mcId, string language, IEnumerable<BaseWorker>? dependsOn = null)
|
||||
: BaseWorkerWithContext<MangaContext>(dependsOn)
|
||||
{
|
||||
internal readonly string MangaConnectorIdId = mcId.Key;
|
||||
protected override async Task<BaseWorker[]> DoWorkInternal()
|
||||
protected override BaseWorker[] DoWorkInternal()
|
||||
{
|
||||
Log.Debug($"Getting Chapters for MangaConnectorId {MangaConnectorIdId}...");
|
||||
// Getting MangaConnector info
|
||||
if (await DbContext.MangaConnectorToManga
|
||||
.Include(id => id.Obj)
|
||||
.ThenInclude(m => m.Chapters)
|
||||
.ThenInclude(ch => ch.MangaConnectorIds)
|
||||
.FirstOrDefaultAsync(c => c.Key == MangaConnectorIdId, CancellationToken) is not { } mangaConnectorId)
|
||||
{
|
||||
Log.Error("Could not get MangaConnectorId.");
|
||||
if (DbContext.MangaConnectorToManga.Find(MangaConnectorIdId) is not { } mangaConnectorId)
|
||||
return []; //TODO Exception?
|
||||
}
|
||||
if (!Tranga.TryGetMangaConnector(mangaConnectorId.MangaConnectorName, out MangaConnector? mangaConnector))
|
||||
{
|
||||
Log.Error("Could not get MangaConnector.");
|
||||
return []; //TODO Exception?
|
||||
}
|
||||
Log.Debug($"Getting Chapters for MangaConnectorId {mangaConnectorId}...");
|
||||
|
||||
DbContext.Entry(mangaConnectorId).Navigation(nameof(MangaConnectorId<Manga>.Obj)).Load();
|
||||
Manga manga = mangaConnectorId.Obj;
|
||||
DbContext.Entry(manga).Collection(m => m.Chapters).Load();
|
||||
|
||||
// Retrieve available Chapters from Connector
|
||||
(Chapter chapter, MangaConnectorId<Chapter> chapterId)[] allChapters =
|
||||
// This gets all chapters that are not downloaded
|
||||
(Chapter, MangaConnectorId<Chapter>)[] allChapters =
|
||||
mangaConnector.GetChapters(mangaConnectorId, language).DistinctBy(c => c.Item1.Key).ToArray();
|
||||
Log.Debug($"Got {allChapters.Length} chapters from connector.");
|
||||
|
||||
// Filter for new Chapters
|
||||
List<(Chapter chapter, MangaConnectorId<Chapter> chapterId)> newChapters = allChapters.Where<(Chapter chapter, MangaConnectorId<Chapter> chapterId)>(ch =>
|
||||
manga.Chapters.All(c => c.Key != ch.chapter.Key)).ToList();
|
||||
Log.Debug($"Got {newChapters.Count} new chapters.");
|
||||
|
||||
// Add Chapters to Manga
|
||||
manga.Chapters = manga.Chapters.Union(newChapters.Select(ch => ch.chapter)).ToList();
|
||||
|
||||
// Filter for new ChapterIds
|
||||
List<MangaConnectorId<Chapter>> existingChapterIds = manga.Chapters.SelectMany(c => c.MangaConnectorIds).ToList();
|
||||
List<MangaConnectorId<Chapter>> newIds = allChapters.Select(ch => ch.chapterId)
|
||||
.Where(newCh => !existingChapterIds.Any(existing =>
|
||||
existing.MangaConnectorName == newCh.MangaConnectorName &&
|
||||
existing.IdOnConnectorSite == newCh.IdOnConnectorSite))
|
||||
.ToList();
|
||||
// Match tracked entities of Chapters
|
||||
foreach (MangaConnectorId<Chapter> newId in newIds)
|
||||
newId.Obj = manga.Chapters.First(ch => ch.Key == newId.ObjId);
|
||||
Log.Debug($"Got {newIds.Count} new download-Ids.");
|
||||
|
||||
// Add new ChapterIds to Database
|
||||
DbContext.MangaConnectorToChapter.AddRange(newIds);
|
||||
|
||||
// If Manga is marked for Download from Connector, mark the new Chapters as UseForDownload
|
||||
if (mangaConnectorId.UseForDownload)
|
||||
int addedChapters = 0;
|
||||
foreach ((Chapter chapter, MangaConnectorId<Chapter> mcId) newChapter in allChapters)
|
||||
{
|
||||
foreach ((Chapter _, MangaConnectorId<Chapter> chapterId) in newChapters)
|
||||
{
|
||||
chapterId.UseForDownload = mangaConnectorId.UseForDownload;
|
||||
}
|
||||
if (Tranga.AddChapterToContext(newChapter, DbContext, out Chapter? addedChapter) == false)
|
||||
continue;
|
||||
manga.Chapters.Add(addedChapter);
|
||||
}
|
||||
Log.Info($"{manga.Chapters.Count} existing + {addedChapters} new chapters.");
|
||||
|
||||
if(await DbContext.Sync(CancellationToken) is { success: false } e)
|
||||
Log.Error($"Failed to save database changes: {e.exceptionMessage}");
|
||||
DbContext.Sync();
|
||||
|
||||
return [];
|
||||
}
|
||||
|
@@ -6,7 +6,7 @@ public class MoveFileOrFolderWorker(string toLocation, string fromLocation, IEnu
|
||||
public readonly string FromLocation = fromLocation;
|
||||
public readonly string ToLocation = toLocation;
|
||||
|
||||
protected override Task<BaseWorker[]> DoWorkInternal()
|
||||
protected override BaseWorker[] DoWorkInternal()
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -14,13 +14,13 @@ public class MoveFileOrFolderWorker(string toLocation, string fromLocation, IEnu
|
||||
if (!fi.Exists)
|
||||
{
|
||||
Log.Error($"File does not exist at {FromLocation}");
|
||||
return new Task<BaseWorker[]>(() => []);
|
||||
return [];
|
||||
}
|
||||
|
||||
if (File.Exists(ToLocation))//Do not override existing
|
||||
{
|
||||
Log.Error($"File already exists at {ToLocation}");
|
||||
return new Task<BaseWorker[]>(() => []);
|
||||
return [];
|
||||
}
|
||||
if(fi.Attributes.HasFlag(FileAttributes.Directory))
|
||||
MoveDirectory(fi, ToLocation);
|
||||
@@ -32,7 +32,7 @@ public class MoveFileOrFolderWorker(string toLocation, string fromLocation, IEnu
|
||||
Log.Error(e);
|
||||
}
|
||||
|
||||
return new Task<BaseWorker[]>(() => []);
|
||||
return [];
|
||||
}
|
||||
|
||||
private void MoveDirectory(FileInfo from, string toLocation)
|
||||
|
@@ -1,44 +1,28 @@
|
||||
using API.Schema.MangaContext;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace API.Workers;
|
||||
|
||||
/// <summary>
|
||||
/// Moves a Manga to a different Library
|
||||
/// </summary>
|
||||
public class MoveMangaLibraryWorker(Manga manga, FileLibrary toLibrary, IEnumerable<BaseWorker>? dependsOn = null)
|
||||
: BaseWorkerWithContext<MangaContext>(dependsOn)
|
||||
{
|
||||
internal readonly string MangaId = manga.Key;
|
||||
internal readonly string LibraryId = toLibrary.Key;
|
||||
protected override async Task<BaseWorker[]> DoWorkInternal()
|
||||
protected override BaseWorker[] DoWorkInternal()
|
||||
{
|
||||
Log.Debug("Moving Manga...");
|
||||
// Get Manga (with Chapters and Library)
|
||||
if (await DbContext.Mangas
|
||||
.Include(m => m.Chapters)
|
||||
.Include(m => m.Library)
|
||||
.FirstOrDefaultAsync(m => m.Key == MangaId, CancellationToken) is not { } manga)
|
||||
{
|
||||
Log.Error("Could not find Manga.");
|
||||
if (DbContext.Mangas.Find(MangaId) is not { } manga)
|
||||
return []; //TODO Exception?
|
||||
}
|
||||
// Get new Library
|
||||
if (await DbContext.FileLibraries.FirstOrDefaultAsync(l => l.Key == LibraryId, CancellationToken) is not { } toLibrary)
|
||||
{
|
||||
Log.Error("Could not find Library.");
|
||||
if (DbContext.FileLibraries.Find(LibraryId) is not { } toLibrary)
|
||||
return []; //TODO Exception?
|
||||
}
|
||||
|
||||
// Save old Path (to later move chapters)
|
||||
DbContext.Entry(manga).Collection(m => m.Chapters).Load();
|
||||
DbContext.Entry(manga).Navigation(nameof(Manga.Library)).Load();
|
||||
|
||||
Dictionary<Chapter, string> oldPath = manga.Chapters.ToDictionary(c => c, c => c.FullArchiveFilePath);
|
||||
// Set new Path
|
||||
manga.Library = toLibrary;
|
||||
|
||||
if (await DbContext.Sync(CancellationToken) is { success: false })
|
||||
if (DbContext.Sync() is { success: false })
|
||||
return [];
|
||||
|
||||
// Create Jobs to move chapters from old to new Path
|
||||
return manga.Chapters.Select(c => new MoveFileOrFolderWorker(c.FullArchiveFilePath, oldPath[c])).ToArray<BaseWorker>();
|
||||
}
|
||||
|
||||
|
@@ -3,26 +3,21 @@ using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace API.Workers;
|
||||
|
||||
/// <summary>
|
||||
/// Creates Jobs to update available Chapters for all Manga that are marked for Download
|
||||
/// </summary>
|
||||
public class CheckForNewChaptersWorker(TimeSpan? interval = null, IEnumerable<BaseWorker>? dependsOn = null)
|
||||
: BaseWorkerWithContext<MangaContext>(dependsOn), IPeriodic
|
||||
{
|
||||
public DateTime LastExecution { get; set; } = DateTime.UnixEpoch;
|
||||
public TimeSpan Interval { get; set; } = interval??TimeSpan.FromMinutes(60);
|
||||
|
||||
protected override async Task<BaseWorker[]> DoWorkInternal()
|
||||
protected override BaseWorker[] DoWorkInternal()
|
||||
{
|
||||
Log.Debug("Checking for new chapters...");
|
||||
List<MangaConnectorId<Manga>> connectorIdsManga = await DbContext.MangaConnectorToManga
|
||||
IQueryable<MangaConnectorId<Manga>> connectorIdsManga = DbContext.MangaConnectorToManga
|
||||
.Include(id => id.Obj)
|
||||
.Where(id => id.UseForDownload)
|
||||
.ToListAsync(CancellationToken);
|
||||
Log.Debug($"Creating {connectorIdsManga.Count} update jobs...");
|
||||
.Where(id => id.UseForDownload);
|
||||
|
||||
List<BaseWorker> newWorkers = connectorIdsManga.Select(id => new RetrieveMangaChaptersFromMangaconnectorWorker(id, Tranga.Settings.DownloadLanguage))
|
||||
.ToList<BaseWorker>();
|
||||
List<BaseWorker> newWorkers = new();
|
||||
foreach (MangaConnectorId<Manga> mangaConnectorId in connectorIdsManga)
|
||||
newWorkers.Add(new RetrieveMangaChaptersFromMangaconnectorWorker(mangaConnectorId, Tranga.Settings.DownloadLanguage));
|
||||
|
||||
return newWorkers.ToArray();
|
||||
}
|
||||
|
@@ -8,25 +8,14 @@ public class CleanupMangaCoversWorker(TimeSpan? interval = null, IEnumerable<Bas
|
||||
public DateTime LastExecution { get; set; } = DateTime.UnixEpoch;
|
||||
public TimeSpan Interval { get; set; } = interval ?? TimeSpan.FromHours(24);
|
||||
|
||||
protected override Task<BaseWorker[]> DoWorkInternal()
|
||||
protected override BaseWorker[] DoWorkInternal()
|
||||
{
|
||||
Log.Info("Removing stale files...");
|
||||
if (!Directory.Exists(TrangaSettings.coverImageCache))
|
||||
return [];
|
||||
string[] usedFiles = DbContext.Mangas.Select(m => m.CoverFileNameInCache).Where(s => s != null).ToArray()!;
|
||||
CleanupImageCache(usedFiles, TrangaSettings.coverImageCacheOriginal);
|
||||
CleanupImageCache(usedFiles, TrangaSettings.coverImageCacheLarge);
|
||||
CleanupImageCache(usedFiles, TrangaSettings.coverImageCacheMedium);
|
||||
CleanupImageCache(usedFiles, TrangaSettings.coverImageCacheSmall);
|
||||
return new Task<BaseWorker[]>(() => []);
|
||||
}
|
||||
|
||||
private void CleanupImageCache(string[] retainFilenames, string imageCachePath)
|
||||
{
|
||||
DirectoryInfo directory = new(imageCachePath);
|
||||
if (!directory.Exists)
|
||||
return;
|
||||
string[] extraneousFiles = directory
|
||||
.GetFiles()
|
||||
.Where(f => retainFilenames.Contains(f.Name) == false)
|
||||
string[] extraneousFiles = new DirectoryInfo(TrangaSettings.coverImageCache).GetFiles()
|
||||
.Where(f => usedFiles.Contains(f.FullName) == false)
|
||||
.Select(f => f.FullName)
|
||||
.ToArray();
|
||||
foreach (string path in extraneousFiles)
|
||||
@@ -34,5 +23,7 @@ public class CleanupMangaCoversWorker(TimeSpan? interval = null, IEnumerable<Bas
|
||||
Log.Info($"Deleting {path}");
|
||||
File.Delete(path);
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
}
|
@@ -1,27 +1,18 @@
|
||||
using API.Schema.NotificationsContext;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace API.Workers.MaintenanceWorkers;
|
||||
|
||||
/// <summary>
|
||||
/// Removes sent notifications from database
|
||||
/// </summary>
|
||||
public class RemoveOldNotificationsWorker(TimeSpan? interval = null, IEnumerable<BaseWorker>? dependsOn = null)
|
||||
: BaseWorkerWithContext<NotificationsContext>(dependsOn), IPeriodic
|
||||
{
|
||||
public DateTime LastExecution { get; set; } = DateTime.UnixEpoch;
|
||||
public TimeSpan Interval { get; set; } = interval ?? TimeSpan.FromHours(1);
|
||||
|
||||
protected override async Task<BaseWorker[]> DoWorkInternal()
|
||||
protected override BaseWorker[] DoWorkInternal()
|
||||
{
|
||||
Log.Debug("Removing old notifications...");
|
||||
List<Notification> toRemove = await DbContext.Notifications.Where(n => n.IsSent).ToListAsync(CancellationToken);
|
||||
Log.Debug($"Removing {toRemove.Count} old notifications...");
|
||||
IQueryable<Notification> toRemove = DbContext.Notifications.Where(n => n.IsSent || DateTime.UtcNow - n.Date > Interval);
|
||||
DbContext.RemoveRange(toRemove);
|
||||
|
||||
if(await DbContext.Sync(CancellationToken) is { success: false } e)
|
||||
Log.Error($"Failed to save database changes: {e.exceptionMessage}");
|
||||
|
||||
DbContext.Sync();
|
||||
return [];
|
||||
}
|
||||
|
||||
|
@@ -1,41 +1,28 @@
|
||||
using API.Schema.NotificationsContext;
|
||||
using API.Schema.NotificationsContext.NotificationConnectors;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace API.Workers;
|
||||
|
||||
/// <summary>
|
||||
/// Send notifications to NotificationConnectors
|
||||
/// </summary>
|
||||
/// <param name="interval"></param>
|
||||
/// <param name="dependsOn"></param>
|
||||
public class SendNotificationsWorker(TimeSpan? interval = null, IEnumerable<BaseWorker>? dependsOn = null)
|
||||
: BaseWorkerWithContext<NotificationsContext>(dependsOn), IPeriodic
|
||||
{
|
||||
public DateTime LastExecution { get; set; } = DateTime.UnixEpoch;
|
||||
public TimeSpan Interval { get; set; } = interval??TimeSpan.FromMinutes(1);
|
||||
protected override async Task<BaseWorker[]> DoWorkInternal()
|
||||
protected override BaseWorker[] DoWorkInternal()
|
||||
{
|
||||
Log.Debug("Sending notifications...");
|
||||
List<NotificationConnector> connectors = await DbContext.NotificationConnectors.ToListAsync(CancellationToken);
|
||||
List<Notification> unsentNotifications = await DbContext.Notifications.Where(n => n.IsSent == false).ToListAsync(CancellationToken);
|
||||
NotificationConnector[] connectors = DbContext.NotificationConnectors.ToArray();
|
||||
Notification[] notifications = DbContext.Notifications.Where(n => n.IsSent == false).ToArray();
|
||||
|
||||
Log.Debug($"Sending {unsentNotifications.Count} notifications to {connectors.Count} connectors...");
|
||||
|
||||
unsentNotifications.ForEach(notification =>
|
||||
foreach (Notification notification in notifications)
|
||||
{
|
||||
connectors.ForEach(connector =>
|
||||
foreach (NotificationConnector connector in connectors)
|
||||
{
|
||||
connector.SendNotification(notification.Title, notification.Message);
|
||||
DbContext.Entry(notification).Property(n => n.IsSent).CurrentValue = true;
|
||||
});
|
||||
});
|
||||
|
||||
Log.Debug("Notifications sent.");
|
||||
|
||||
if(await DbContext.Sync(CancellationToken) is { success: false } e)
|
||||
Log.Error($"Failed to save database changes: {e.exceptionMessage}");
|
||||
notification.IsSent = true;
|
||||
}
|
||||
}
|
||||
|
||||
DbContext.Sync();
|
||||
return [];
|
||||
}
|
||||
|
||||
|
@@ -3,36 +3,21 @@ using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace API.Workers;
|
||||
|
||||
/// <summary>
|
||||
/// Create new Workers for Chapters on Manga marked for Download, that havent been downloaded yet.
|
||||
/// </summary>
|
||||
public class StartNewChapterDownloadsWorker(TimeSpan? interval = null, IEnumerable<BaseWorker>? dependsOn = null)
|
||||
: BaseWorkerWithContext<MangaContext>(dependsOn), IPeriodic
|
||||
{
|
||||
|
||||
public DateTime LastExecution { get; set; } = DateTime.UnixEpoch;
|
||||
public TimeSpan Interval { get; set; } = interval ?? TimeSpan.FromMinutes(1);
|
||||
protected override async Task<BaseWorker[]> DoWorkInternal()
|
||||
protected override BaseWorker[] DoWorkInternal()
|
||||
{
|
||||
Log.Debug("Checking for missing chapters...");
|
||||
|
||||
// Get missing chapters
|
||||
List<MangaConnectorId<Chapter>> missingChapters = await DbContext.MangaConnectorToChapter
|
||||
IQueryable<MangaConnectorId<Chapter>> mangaConnectorIds = DbContext.MangaConnectorToChapter
|
||||
.Include(id => id.Obj)
|
||||
.Where(id => id.Obj.Downloaded == false && id.UseForDownload)
|
||||
.ToListAsync(CancellationToken);
|
||||
.Where(id => id.Obj.Downloaded == false && id.UseForDownload);
|
||||
|
||||
Log.Debug($"Found {missingChapters.Count} missing downloads.");
|
||||
|
||||
// Maximum Concurrent workers
|
||||
int downloadWorkers = Tranga.GetRunningWorkers().Count(w => w.GetType() == typeof(DownloadChapterFromMangaconnectorWorker));
|
||||
int amountNewWorkers = Math.Min(Tranga.Settings.MaxConcurrentDownloads, Tranga.Settings.MaxConcurrentDownloads - downloadWorkers);
|
||||
|
||||
Log.Debug($"{downloadWorkers} running download Workers. {amountNewWorkers} new download Workers.");
|
||||
IEnumerable<MangaConnectorId<Chapter>> newDownloadChapters = missingChapters.OrderBy(ch => ch.Obj, new Chapter.ChapterComparer()).Take(amountNewWorkers);
|
||||
|
||||
// Create new jobs
|
||||
List<BaseWorker> newWorkers = newDownloadChapters.Select(mcId => new DownloadChapterFromMangaconnectorWorker(mcId)).ToList<BaseWorker>();
|
||||
List<BaseWorker> newWorkers = new();
|
||||
foreach (MangaConnectorId<Chapter> mangaConnectorId in mangaConnectorIds)
|
||||
newWorkers.Add(new DownloadChapterFromMangaconnectorWorker(mangaConnectorId));
|
||||
|
||||
return newWorkers.ToArray();
|
||||
}
|
||||
|
@@ -3,34 +3,17 @@ using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace API.Workers;
|
||||
|
||||
/// <summary>
|
||||
/// Updates the database to reflect changes made on disk
|
||||
/// </summary>
|
||||
public class UpdateChaptersDownloadedWorker(TimeSpan? interval = null, IEnumerable<BaseWorker>? dependsOn = null)
|
||||
: BaseWorkerWithContext<MangaContext>(dependsOn), IPeriodic
|
||||
{
|
||||
public DateTime LastExecution { get; set; } = DateTime.UnixEpoch;
|
||||
public TimeSpan Interval { get; set; } = interval??TimeSpan.FromMinutes(60);
|
||||
protected override async Task<BaseWorker[]> DoWorkInternal()
|
||||
protected override BaseWorker[] DoWorkInternal()
|
||||
{
|
||||
Log.Debug("Checking chapter files...");
|
||||
List<Chapter> chapters = await DbContext.Chapters.ToListAsync(CancellationToken);
|
||||
Log.Debug($"Checking {chapters.Count} chapters...");
|
||||
foreach (Chapter chapter in chapters)
|
||||
{
|
||||
try
|
||||
{
|
||||
chapter.Downloaded = await chapter.CheckDownloaded(DbContext, CancellationToken);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Log.Error(exception);
|
||||
}
|
||||
}
|
||||
|
||||
if(await DbContext.Sync(CancellationToken) is { success: false } e)
|
||||
Log.Error($"Failed to save database changes: {e.exceptionMessage}");
|
||||
foreach (Chapter dbContextChapter in DbContext.Chapters.Include(c => c.ParentManga))
|
||||
dbContextChapter.Downloaded = dbContextChapter.CheckDownloaded();
|
||||
|
||||
DbContext.Sync();
|
||||
return [];
|
||||
}
|
||||
}
|
@@ -1,13 +1,7 @@
|
||||
using API.Schema.MangaContext;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace API.Workers;
|
||||
|
||||
/// <summary>
|
||||
/// Creates Workers to update covers for Manga
|
||||
/// </summary>
|
||||
/// <param name="interval"></param>
|
||||
/// <param name="dependsOn"></param>
|
||||
public class UpdateCoversWorker(TimeSpan? interval = null, IEnumerable<BaseWorker>? dependsOn = null)
|
||||
: BaseWorkerWithContext<MangaContext>(dependsOn), IPeriodic
|
||||
{
|
||||
@@ -15,10 +9,11 @@ public class UpdateCoversWorker(TimeSpan? interval = null, IEnumerable<BaseWorke
|
||||
public DateTime LastExecution { get; set; } = DateTime.UnixEpoch;
|
||||
public TimeSpan Interval { get; set; } = interval ?? TimeSpan.FromHours(6);
|
||||
|
||||
protected override async Task<BaseWorker[]> DoWorkInternal()
|
||||
protected override BaseWorker[] DoWorkInternal()
|
||||
{
|
||||
List<MangaConnectorId<Manga>> manga = await DbContext.MangaConnectorToManga.Where(mcId => mcId.UseForDownload).ToListAsync(CancellationToken);
|
||||
List<BaseWorker> newWorkers = manga.Select(m => new DownloadCoverFromMangaconnectorWorker(m)).ToList<BaseWorker>();
|
||||
return newWorkers.ToArray();
|
||||
List<BaseWorker> workers = new();
|
||||
foreach (MangaConnectorId<Manga> mangaConnectorId in DbContext.MangaConnectorToManga)
|
||||
workers.Add(new DownloadCoverFromMangaconnectorWorker(mangaConnectorId));
|
||||
return workers.ToArray();
|
||||
}
|
||||
}
|
@@ -4,11 +4,6 @@ using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace API.Workers;
|
||||
|
||||
/// <summary>
|
||||
/// Updates Metadata for all Manga
|
||||
/// </summary>
|
||||
/// <param name="interval"></param>
|
||||
/// <param name="dependsOn"></param>
|
||||
public class UpdateMetadataWorker(TimeSpan? interval = null, IEnumerable<BaseWorker>? dependsOn = null)
|
||||
: BaseWorkerWithContext<MangaContext>(dependsOn), IPeriodic
|
||||
{
|
||||
@@ -16,29 +11,20 @@ public class UpdateMetadataWorker(TimeSpan? interval = null, IEnumerable<BaseWor
|
||||
public DateTime LastExecution { get; set; } = DateTime.UnixEpoch;
|
||||
public TimeSpan Interval { get; set; } = interval ?? TimeSpan.FromHours(12);
|
||||
|
||||
protected override async Task<BaseWorker[]> DoWorkInternal()
|
||||
protected override BaseWorker[] DoWorkInternal()
|
||||
{
|
||||
Log.Debug("Updating metadata...");
|
||||
// Get MetadataEntries of Manga marked for download
|
||||
List<MetadataEntry> metadataEntriesToUpdate = await DbContext.MangaConnectorToManga
|
||||
.Where(m => m.UseForDownload) // Get marked Manga
|
||||
.Join(
|
||||
DbContext.MetadataEntries.Include(e => e.MetadataFetcher).Include(e => e.Manga),
|
||||
mcId => mcId.ObjId,
|
||||
e => e.MangaId,
|
||||
(mcId, e) => e) // return MetadataEntry
|
||||
.ToListAsync(CancellationToken);
|
||||
Log.Debug($"Updating metadata of {metadataEntriesToUpdate.Count} manga...");
|
||||
IQueryable<string> mangaIds = DbContext.MangaConnectorToManga
|
||||
.Where(m => m.UseForDownload)
|
||||
.Select(m => m.ObjId);
|
||||
IQueryable<MetadataEntry> metadataEntriesToUpdate = DbContext.MetadataEntries
|
||||
.Include(e => e.MetadataFetcher)
|
||||
.Where(e =>
|
||||
mangaIds.Any(id => id == e.MangaId));
|
||||
|
||||
foreach (MetadataEntry metadataEntry in metadataEntriesToUpdate)
|
||||
{
|
||||
Log.Debug($"Updating metadata of {metadataEntry}...");
|
||||
await metadataEntry.MetadataFetcher.UpdateMetadata(metadataEntry, DbContext, CancellationToken);
|
||||
}
|
||||
Log.Debug("Updated metadata.");
|
||||
metadataEntry.MetadataFetcher.UpdateMetadata(metadataEntry, DbContext);
|
||||
|
||||
if(await DbContext.Sync(CancellationToken) is { success: false } e)
|
||||
Log.Error($"Failed to save database changes: {e.exceptionMessage}");
|
||||
DbContext.Sync();
|
||||
|
||||
return [];
|
||||
}
|
||||
|
BIN
DB-Layout.png
BIN
DB-Layout.png
Binary file not shown.
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 37 KiB |
279
DB-Layout.uxf
279
DB-Layout.uxf
@@ -4,8 +4,8 @@
|
||||
<element>
|
||||
<id>UMLClass</id>
|
||||
<coordinates>
|
||||
<x>930</x>
|
||||
<y>590</y>
|
||||
<x>1160</x>
|
||||
<y>680</y>
|
||||
<w>100</w>
|
||||
<h>40</h>
|
||||
</coordinates>
|
||||
@@ -15,8 +15,8 @@
|
||||
<element>
|
||||
<id>UMLClass</id>
|
||||
<coordinates>
|
||||
<x>670</x>
|
||||
<y>590</y>
|
||||
<x>900</x>
|
||||
<y>680</y>
|
||||
<w>140</w>
|
||||
<h>40</h>
|
||||
</coordinates>
|
||||
@@ -26,53 +26,53 @@
|
||||
<element>
|
||||
<id>UMLClass</id>
|
||||
<coordinates>
|
||||
<x>250</x>
|
||||
<y>710</y>
|
||||
<w>120</w>
|
||||
<x>500</x>
|
||||
<y>800</y>
|
||||
<w>80</w>
|
||||
<h>40</h>
|
||||
</coordinates>
|
||||
<panel_attributes>/BaseWorker/</panel_attributes>
|
||||
<panel_attributes>/Job/</panel_attributes>
|
||||
<additional_attributes/>
|
||||
</element>
|
||||
<element>
|
||||
<id>UMLClass</id>
|
||||
<coordinates>
|
||||
<x>450</x>
|
||||
<y>710</y>
|
||||
<x>680</x>
|
||||
<y>800</y>
|
||||
<w>160</w>
|
||||
<h>40</h>
|
||||
</coordinates>
|
||||
<panel_attributes>/BaseWorkerWithContext/</panel_attributes>
|
||||
<panel_attributes>/JobWithDownload/</panel_attributes>
|
||||
<additional_attributes/>
|
||||
</element>
|
||||
<element>
|
||||
<id>UMLClass</id>
|
||||
<coordinates>
|
||||
<x>440</x>
|
||||
<y>590</y>
|
||||
<w>180</w>
|
||||
<x>680</x>
|
||||
<y>680</y>
|
||||
<w>160</w>
|
||||
<h>40</h>
|
||||
</coordinates>
|
||||
<panel_attributes>DownloadChapterFrom
|
||||
MangaConnectorWorker</panel_attributes>
|
||||
<panel_attributes>RetrieveChaptersJob
|
||||
</panel_attributes>
|
||||
<additional_attributes/>
|
||||
</element>
|
||||
<element>
|
||||
<id>Relation</id>
|
||||
<coordinates>
|
||||
<x>360</x>
|
||||
<y>720</y>
|
||||
<w>110</w>
|
||||
<x>570</x>
|
||||
<y>810</y>
|
||||
<w>130</w>
|
||||
<h>30</h>
|
||||
</coordinates>
|
||||
<panel_attributes>lt=<<-</panel_attributes>
|
||||
<additional_attributes>10.0;10.0;90.0;10.0</additional_attributes>
|
||||
<additional_attributes>10.0;10.0;110.0;10.0</additional_attributes>
|
||||
</element>
|
||||
<element>
|
||||
<id>Relation</id>
|
||||
<coordinates>
|
||||
<x>520</x>
|
||||
<y>620</y>
|
||||
<x>750</x>
|
||||
<y>710</y>
|
||||
<w>30</w>
|
||||
<h>110</h>
|
||||
</coordinates>
|
||||
@@ -82,8 +82,8 @@ MangaConnectorWorker</panel_attributes>
|
||||
<element>
|
||||
<id>Relation</id>
|
||||
<coordinates>
|
||||
<x>940</x>
|
||||
<y>620</y>
|
||||
<x>1170</x>
|
||||
<y>710</y>
|
||||
<w>30</w>
|
||||
<h>230</h>
|
||||
</coordinates>
|
||||
@@ -93,8 +93,8 @@ MangaConnectorWorker</panel_attributes>
|
||||
<element>
|
||||
<id>Relation</id>
|
||||
<coordinates>
|
||||
<x>800</x>
|
||||
<y>730</y>
|
||||
<x>1030</x>
|
||||
<y>820</y>
|
||||
<w>150</w>
|
||||
<h>140</h>
|
||||
</coordinates>
|
||||
@@ -104,8 +104,8 @@ MangaConnectorWorker</panel_attributes>
|
||||
<element>
|
||||
<id>Relation</id>
|
||||
<coordinates>
|
||||
<x>730</x>
|
||||
<y>620</y>
|
||||
<x>960</x>
|
||||
<y>710</y>
|
||||
<w>30</w>
|
||||
<h>110</h>
|
||||
</coordinates>
|
||||
@@ -115,8 +115,8 @@ MangaConnectorWorker</panel_attributes>
|
||||
<element>
|
||||
<id>Relation</id>
|
||||
<coordinates>
|
||||
<x>600</x>
|
||||
<y>720</y>
|
||||
<x>830</x>
|
||||
<y>810</y>
|
||||
<w>90</w>
|
||||
<h>30</h>
|
||||
</coordinates>
|
||||
@@ -126,8 +126,8 @@ MangaConnectorWorker</panel_attributes>
|
||||
<element>
|
||||
<id>UMLClass</id>
|
||||
<coordinates>
|
||||
<x>1180</x>
|
||||
<y>590</y>
|
||||
<x>1410</x>
|
||||
<y>680</y>
|
||||
<w>100</w>
|
||||
<h>40</h>
|
||||
</coordinates>
|
||||
@@ -137,8 +137,8 @@ MangaConnectorWorker</panel_attributes>
|
||||
<element>
|
||||
<id>Relation</id>
|
||||
<coordinates>
|
||||
<x>1020</x>
|
||||
<y>600</y>
|
||||
<x>1250</x>
|
||||
<y>690</y>
|
||||
<w>180</w>
|
||||
<h>30</h>
|
||||
</coordinates>
|
||||
@@ -148,8 +148,8 @@ MangaConnectorWorker</panel_attributes>
|
||||
<element>
|
||||
<id>UMLClass</id>
|
||||
<coordinates>
|
||||
<x>1180</x>
|
||||
<y>530</y>
|
||||
<x>1410</x>
|
||||
<y>620</y>
|
||||
<w>100</w>
|
||||
<h>40</h>
|
||||
</coordinates>
|
||||
@@ -159,8 +159,8 @@ MangaConnectorWorker</panel_attributes>
|
||||
<element>
|
||||
<id>UMLClass</id>
|
||||
<coordinates>
|
||||
<x>1180</x>
|
||||
<y>470</y>
|
||||
<x>1410</x>
|
||||
<y>560</y>
|
||||
<w>100</w>
|
||||
<h>40</h>
|
||||
</coordinates>
|
||||
@@ -170,8 +170,8 @@ MangaConnectorWorker</panel_attributes>
|
||||
<element>
|
||||
<id>UMLClass</id>
|
||||
<coordinates>
|
||||
<x>1180</x>
|
||||
<y>410</y>
|
||||
<x>1410</x>
|
||||
<y>500</y>
|
||||
<w>100</w>
|
||||
<h>40</h>
|
||||
</coordinates>
|
||||
@@ -181,8 +181,8 @@ MangaConnectorWorker</panel_attributes>
|
||||
<element>
|
||||
<id>Relation</id>
|
||||
<coordinates>
|
||||
<x>970</x>
|
||||
<y>420</y>
|
||||
<x>1200</x>
|
||||
<y>510</y>
|
||||
<w>230</w>
|
||||
<h>190</h>
|
||||
</coordinates>
|
||||
@@ -192,8 +192,8 @@ MangaConnectorWorker</panel_attributes>
|
||||
<element>
|
||||
<id>Relation</id>
|
||||
<coordinates>
|
||||
<x>1000</x>
|
||||
<y>480</y>
|
||||
<x>1230</x>
|
||||
<y>570</y>
|
||||
<w>200</w>
|
||||
<h>130</h>
|
||||
</coordinates>
|
||||
@@ -203,8 +203,8 @@ MangaConnectorWorker</panel_attributes>
|
||||
<element>
|
||||
<id>Relation</id>
|
||||
<coordinates>
|
||||
<x>1020</x>
|
||||
<y>540</y>
|
||||
<x>1250</x>
|
||||
<y>630</y>
|
||||
<w>180</w>
|
||||
<h>70</h>
|
||||
</coordinates>
|
||||
@@ -214,8 +214,8 @@ MangaConnectorWorker</panel_attributes>
|
||||
<element>
|
||||
<id>UMLClass</id>
|
||||
<coordinates>
|
||||
<x>1180</x>
|
||||
<y>350</y>
|
||||
<x>1410</x>
|
||||
<y>440</y>
|
||||
<w>100</w>
|
||||
<h>40</h>
|
||||
</coordinates>
|
||||
@@ -225,8 +225,8 @@ MangaConnectorWorker</panel_attributes>
|
||||
<element>
|
||||
<id>Relation</id>
|
||||
<coordinates>
|
||||
<x>940</x>
|
||||
<y>360</y>
|
||||
<x>1170</x>
|
||||
<y>450</y>
|
||||
<w>260</w>
|
||||
<h>250</h>
|
||||
</coordinates>
|
||||
@@ -236,8 +236,8 @@ MangaConnectorWorker</panel_attributes>
|
||||
<element>
|
||||
<id>UMLClass</id>
|
||||
<coordinates>
|
||||
<x>1150</x>
|
||||
<y>710</y>
|
||||
<x>1380</x>
|
||||
<y>800</y>
|
||||
<w>160</w>
|
||||
<h>40</h>
|
||||
</coordinates>
|
||||
@@ -247,8 +247,8 @@ MangaConnectorWorker</panel_attributes>
|
||||
<element>
|
||||
<id>Relation</id>
|
||||
<coordinates>
|
||||
<x>1000</x>
|
||||
<y>620</y>
|
||||
<x>1230</x>
|
||||
<y>710</y>
|
||||
<w>170</w>
|
||||
<h>130</h>
|
||||
</coordinates>
|
||||
@@ -258,8 +258,8 @@ MangaConnectorWorker</panel_attributes>
|
||||
<element>
|
||||
<id>UMLClass</id>
|
||||
<coordinates>
|
||||
<x>1400</x>
|
||||
<y>710</y>
|
||||
<x>1650</x>
|
||||
<y>800</y>
|
||||
<w>140</w>
|
||||
<h>40</h>
|
||||
</coordinates>
|
||||
@@ -269,19 +269,19 @@ MangaConnectorWorker</panel_attributes>
|
||||
<element>
|
||||
<id>Relation</id>
|
||||
<coordinates>
|
||||
<x>1300</x>
|
||||
<y>720</y>
|
||||
<w>120</w>
|
||||
<x>1530</x>
|
||||
<y>810</y>
|
||||
<w>140</w>
|
||||
<h>30</h>
|
||||
</coordinates>
|
||||
<panel_attributes>lt=-</panel_attributes>
|
||||
<additional_attributes>100.0;10.0;10.0;10.0</additional_attributes>
|
||||
<additional_attributes>120.0;10.0;10.0;10.0</additional_attributes>
|
||||
</element>
|
||||
<element>
|
||||
<id>UMLUseCase</id>
|
||||
<coordinates>
|
||||
<x>1410</x>
|
||||
<y>590</y>
|
||||
<x>1660</x>
|
||||
<y>680</y>
|
||||
<w>120</w>
|
||||
<h>40</h>
|
||||
</coordinates>
|
||||
@@ -291,19 +291,19 @@ MangaConnectorWorker</panel_attributes>
|
||||
<element>
|
||||
<id>Relation</id>
|
||||
<coordinates>
|
||||
<x>1270</x>
|
||||
<y>600</y>
|
||||
<w>160</w>
|
||||
<x>1500</x>
|
||||
<y>690</y>
|
||||
<w>180</w>
|
||||
<h>30</h>
|
||||
</coordinates>
|
||||
<panel_attributes>lt=-</panel_attributes>
|
||||
<additional_attributes>140.0;10.0;10.0;10.0</additional_attributes>
|
||||
<additional_attributes>160.0;10.0;10.0;10.0</additional_attributes>
|
||||
</element>
|
||||
<element>
|
||||
<id>UMLClass</id>
|
||||
<coordinates>
|
||||
<x>670</x>
|
||||
<y>710</y>
|
||||
<x>900</x>
|
||||
<y>800</y>
|
||||
<w>140</w>
|
||||
<h>40</h>
|
||||
</coordinates>
|
||||
@@ -313,8 +313,8 @@ MangaConnectorWorker</panel_attributes>
|
||||
<element>
|
||||
<id>Relation</id>
|
||||
<coordinates>
|
||||
<x>800</x>
|
||||
<y>600</y>
|
||||
<x>1030</x>
|
||||
<y>690</y>
|
||||
<w>150</w>
|
||||
<h>140</h>
|
||||
</coordinates>
|
||||
@@ -324,8 +324,8 @@ MangaConnectorWorker</panel_attributes>
|
||||
<element>
|
||||
<id>UMLClass</id>
|
||||
<coordinates>
|
||||
<x>930</x>
|
||||
<y>830</y>
|
||||
<x>1160</x>
|
||||
<y>920</y>
|
||||
<w>100</w>
|
||||
<h>40</h>
|
||||
</coordinates>
|
||||
@@ -336,20 +336,20 @@ MangaConnectorWorker</panel_attributes>
|
||||
<element>
|
||||
<id>UMLClass</id>
|
||||
<coordinates>
|
||||
<x>230</x>
|
||||
<y>590</y>
|
||||
<x>460</x>
|
||||
<y>680</y>
|
||||
<w>160</w>
|
||||
<h>40</h>
|
||||
</coordinates>
|
||||
<panel_attributes>MoveFileOrFolder
|
||||
Worker</panel_attributes>
|
||||
<panel_attributes>UpdateChapters
|
||||
DownloadedJob</panel_attributes>
|
||||
<additional_attributes/>
|
||||
</element>
|
||||
<element>
|
||||
<id>Relation</id>
|
||||
<coordinates>
|
||||
<x>300</x>
|
||||
<y>620</y>
|
||||
<x>530</x>
|
||||
<y>710</y>
|
||||
<w>30</w>
|
||||
<h>110</h>
|
||||
</coordinates>
|
||||
@@ -359,8 +359,8 @@ Worker</panel_attributes>
|
||||
<element>
|
||||
<id>UMLClass</id>
|
||||
<coordinates>
|
||||
<x>1630</x>
|
||||
<y>550</y>
|
||||
<x>1970</x>
|
||||
<y>640</y>
|
||||
<w>110</w>
|
||||
<h>40</h>
|
||||
</coordinates>
|
||||
@@ -371,8 +371,8 @@ Komga</panel_attributes>
|
||||
<element>
|
||||
<id>UMLClass</id>
|
||||
<coordinates>
|
||||
<x>1630</x>
|
||||
<y>620</y>
|
||||
<x>1970</x>
|
||||
<y>710</y>
|
||||
<w>110</w>
|
||||
<h>40</h>
|
||||
</coordinates>
|
||||
@@ -383,8 +383,8 @@ Kavita</panel_attributes>
|
||||
<element>
|
||||
<id>UMLPackage</id>
|
||||
<coordinates>
|
||||
<x>1570</x>
|
||||
<y>510</y>
|
||||
<x>1930</x>
|
||||
<y>600</y>
|
||||
<w>190</w>
|
||||
<h>170</h>
|
||||
</coordinates>
|
||||
@@ -394,21 +394,21 @@ Kavita</panel_attributes>
|
||||
<element>
|
||||
<id>Relation</id>
|
||||
<coordinates>
|
||||
<x>1520</x>
|
||||
<y>600</y>
|
||||
<w>70</w>
|
||||
<x>1770</x>
|
||||
<y>690</y>
|
||||
<w>180</w>
|
||||
<h>30</h>
|
||||
</coordinates>
|
||||
<panel_attributes>lt=-</panel_attributes>
|
||||
<additional_attributes>50.0;10.0;10.0;10.0</additional_attributes>
|
||||
<additional_attributes>160.0;10.0;10.0;10.0</additional_attributes>
|
||||
</element>
|
||||
<element>
|
||||
<id>UMLClass</id>
|
||||
<coordinates>
|
||||
<x>480</x>
|
||||
<y>820</y>
|
||||
<x>710</x>
|
||||
<y>910</y>
|
||||
<w>100</w>
|
||||
<h>40</h>
|
||||
<h>30</h>
|
||||
</coordinates>
|
||||
<panel_attributes>/Identifiable/</panel_attributes>
|
||||
<additional_attributes/>
|
||||
@@ -416,8 +416,8 @@ Kavita</panel_attributes>
|
||||
<element>
|
||||
<id>Relation</id>
|
||||
<coordinates>
|
||||
<x>520</x>
|
||||
<y>740</y>
|
||||
<x>750</x>
|
||||
<y>830</y>
|
||||
<w>30</w>
|
||||
<h>100</h>
|
||||
</coordinates>
|
||||
@@ -427,8 +427,8 @@ Kavita</panel_attributes>
|
||||
<element>
|
||||
<id>Relation</id>
|
||||
<coordinates>
|
||||
<x>300</x>
|
||||
<y>740</y>
|
||||
<x>530</x>
|
||||
<y>830</y>
|
||||
<w>200</w>
|
||||
<h>120</h>
|
||||
</coordinates>
|
||||
@@ -438,92 +438,23 @@ Kavita</panel_attributes>
|
||||
<element>
|
||||
<id>Relation</id>
|
||||
<coordinates>
|
||||
<x>520</x>
|
||||
<y>850</y>
|
||||
<x>750</x>
|
||||
<y>930</y>
|
||||
<w>480</w>
|
||||
<h>100</h>
|
||||
</coordinates>
|
||||
<panel_attributes>lt=<<-</panel_attributes>
|
||||
<additional_attributes>10.0;10.0;10.0;80.0;460.0;80.0;460.0;20.0</additional_attributes>
|
||||
</element>
|
||||
<element>
|
||||
<id>Relation</id>
|
||||
<coordinates>
|
||||
<x>570</x>
|
||||
<y>610</y>
|
||||
<w>380</w>
|
||||
<h>250</h>
|
||||
</coordinates>
|
||||
<panel_attributes>lt=<<-</panel_attributes>
|
||||
<additional_attributes>10.0;230.0;340.0;230.0;340.0;10.0;360.0;10.0</additional_attributes>
|
||||
</element>
|
||||
<element>
|
||||
<id>UMLPackage</id>
|
||||
<coordinates>
|
||||
<x>0</x>
|
||||
<y>510</y>
|
||||
<w>630</w>
|
||||
<h>460</h>
|
||||
</coordinates>
|
||||
<panel_attributes>Workers
|
||||
--</panel_attributes>
|
||||
<additional_attributes/>
|
||||
</element>
|
||||
<element>
|
||||
<id>UMLClass</id>
|
||||
<coordinates>
|
||||
<x>40</x>
|
||||
<y>710</y>
|
||||
<w>120</w>
|
||||
<h>40</h>
|
||||
</coordinates>
|
||||
<panel_attributes>/IPeriodicWorker/</panel_attributes>
|
||||
<additional_attributes/>
|
||||
</element>
|
||||
<element>
|
||||
<id>Relation</id>
|
||||
<coordinates>
|
||||
<x>150</x>
|
||||
<y>720</y>
|
||||
<w>120</w>
|
||||
<h>30</h>
|
||||
</coordinates>
|
||||
<panel_attributes>lt=<<-</panel_attributes>
|
||||
<additional_attributes>100.0;10.0;10.0;10.0</additional_attributes>
|
||||
</element>
|
||||
<element>
|
||||
<id>Relation</id>
|
||||
<coordinates>
|
||||
<x>90</x>
|
||||
<y>620</y>
|
||||
<w>30</w>
|
||||
<h>110</h>
|
||||
</coordinates>
|
||||
<panel_attributes>lt=<<-</panel_attributes>
|
||||
<additional_attributes>10.0;90.0;10.0;10.0</additional_attributes>
|
||||
<additional_attributes>10.0;10.0;10.0;90.0;460.0;90.0;460.0;30.0</additional_attributes>
|
||||
</element>
|
||||
<element>
|
||||
<id>UMLClass</id>
|
||||
<id>Relation</id>
|
||||
<coordinates>
|
||||
<x>20</x>
|
||||
<y>590</y>
|
||||
<w>160</w>
|
||||
<h>40</h>
|
||||
<x>800</x>
|
||||
<y>670</y>
|
||||
<w>380</w>
|
||||
<h>280</h>
|
||||
</coordinates>
|
||||
<panel_attributes>CheckForNewChapters
|
||||
Worker</panel_attributes>
|
||||
<additional_attributes/>
|
||||
</element>
|
||||
<element>
|
||||
<id>UMLPackage</id>
|
||||
<coordinates>
|
||||
<x>660</x>
|
||||
<y>290</y>
|
||||
<w>710</w>
|
||||
<h>680</h>
|
||||
</coordinates>
|
||||
<panel_attributes>MangaContext
|
||||
--</panel_attributes>
|
||||
<additional_attributes/>
|
||||
<panel_attributes>lt=<<-</panel_attributes>
|
||||
<additional_attributes>10.0;260.0;260.0;260.0;260.0;10.0;360.0;10.0</additional_attributes>
|
||||
</element>
|
||||
</diagram>
|
||||
|
13
Dockerfile
13
Dockerfile
@@ -1,8 +1,17 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
ARG DOTNET=9.0
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:$DOTNET AS base
|
||||
FROM --platform=$TARGETPLATFORM mcr.microsoft.com/dotnet/aspnet:$DOTNET AS base
|
||||
WORKDIR /publish
|
||||
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true
|
||||
ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium
|
||||
ENV XDG_CONFIG_HOME=/tmp/.chromium
|
||||
ENV XDG_CACHE_HOME=/tmp/.chromium
|
||||
RUN apt-get update \
|
||||
&& apt-get upgrade -y \
|
||||
&& apt-get install -y chromium \
|
||||
&& apt-get autopurge -y \
|
||||
&& apt-get autoclean -y
|
||||
|
||||
FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:$DOTNET AS build-env
|
||||
WORKDIR /src
|
||||
@@ -14,7 +23,7 @@ RUN dotnet restore /src/Tranga.sln
|
||||
COPY . /src/
|
||||
RUN dotnet publish -c Release --property:OutputPath=/publish -maxcpucount:1
|
||||
|
||||
FROM base AS runtime
|
||||
FROM --platform=$TARGETPLATFORM base AS runtime
|
||||
EXPOSE 6531
|
||||
ARG UNAME=tranga
|
||||
ARG UID=1000
|
||||
|
678
LICENSE.txt
678
LICENSE.txt
@@ -1,4 +1,674 @@
|
||||
Tranga
|
||||
Docker-Container to monitor (Manga) Scanlation-Sites for download new chapters.
|
||||
Copyright (C) 2025 C9Glax
|
||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU General Public License is a free, copyleft license for
|
||||
software and other kinds of works.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
the GNU General Public License is intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users. We, the Free Software Foundation, use the
|
||||
GNU General Public License for most of our software; it applies also to
|
||||
any other work released this way by its authors. You can apply it to
|
||||
your programs, too.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to prevent others from denying you
|
||||
these rights or asking you to surrender the rights. Therefore, you have
|
||||
certain responsibilities if you distribute copies of the software, or if
|
||||
you modify it: responsibilities to respect the freedom of others.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must pass on to the recipients the same
|
||||
freedoms that you received. You must make sure that they, too, receive
|
||||
or can get the source code. And you must show them these terms so they
|
||||
know their rights.
|
||||
|
||||
Developers that use the GNU GPL protect your rights with two steps:
|
||||
(1) assert copyright on the software, and (2) offer you this License
|
||||
giving you legal permission to copy, distribute and/or modify it.
|
||||
|
||||
For the developers' and authors' protection, the GPL clearly explains
|
||||
that there is no warranty for this free software. For both users' and
|
||||
authors' sake, the GPL requires that modified versions be marked as
|
||||
changed, so that their problems will not be attributed erroneously to
|
||||
authors of previous versions.
|
||||
|
||||
Some devices are designed to deny users access to install or run
|
||||
modified versions of the software inside them, although the manufacturer
|
||||
can do so. This is fundamentally incompatible with the aim of
|
||||
protecting users' freedom to change the software. The systematic
|
||||
pattern of such abuse occurs in the area of products for individuals to
|
||||
use, which is precisely where it is most unacceptable. Therefore, we
|
||||
have designed this version of the GPL to prohibit the practice for those
|
||||
products. If such problems arise substantially in other domains, we
|
||||
stand ready to extend this provision to those domains in future versions
|
||||
of the GPL, as needed to protect the freedom of users.
|
||||
|
||||
Finally, every program is threatened constantly by software patents.
|
||||
States should not allow patents to restrict development and use of
|
||||
software on general-purpose computers, but in those that do, we wish to
|
||||
avoid the special danger that patents applied to a free program could
|
||||
make it effectively proprietary. To prevent this, the GPL assures that
|
||||
patents cannot be used to render the program non-free.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Use with the GNU Affero General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU Affero General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the special requirements of the GNU Affero General Public License,
|
||||
section 13, concerning interaction through a network will apply to the
|
||||
combination as such.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program does terminal interaction, make it output a short
|
||||
notice like this when it starts in an interactive mode:
|
||||
|
||||
<program> Copyright (C) <year> <name of author>
|
||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, your program's commands
|
||||
might be different; for a GUI interface, you would use an "about box".
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU GPL, see
|
||||
<https://www.gnu.org/licenses/>.
|
||||
|
||||
The GNU General Public License does not permit incorporating your program
|
||||
into proprietary programs. If your program is a subroutine library, you
|
||||
may consider it more useful to permit linking proprietary applications with
|
||||
the library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License. But first, please read
|
||||
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
||||
|
94
README.md
94
README.md
@@ -18,12 +18,8 @@
|
||||
<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/testing?label=testing"></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-testing.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/oldstable?label=oldstable"></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-oldstable.yml%2Fruns%3Fper_page%3D1&query=workflow_runs%5B0%5D.created_at&label=Last%20Run"></td>
|
||||
<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>
|
||||
|
||||
@@ -44,21 +40,22 @@ Notifications can be sent to your devices using [Gotify](https://gotify.net/), [
|
||||
|
||||
## What this program does and does *not* do
|
||||
|
||||
*DOES*: Download Images from a Website.<br />
|
||||
*DOES*: Create Archives with those images.<br />
|
||||
DOES: Download Images from a Website.<br />
|
||||
DOES: Create Archives.<br />
|
||||
|
||||
_**how**?_
|
||||
### how:
|
||||
|
||||
Tranga (this repository) is a REST-API and worker in one. Tranga provides REST-Endpoints to configure workers (Jobs).
|
||||
Requests include searches for Manga, creating and starting Jobs such as downloading available chapters.
|
||||
For available endpoints check `http(s)://<hostedInstance>/swagger`
|
||||
For available endpoints check `<hostedInstance>/swagger`
|
||||
|
||||
**This repository** _**does not**_ include a frontend. A frontend can take many forms, such as a website:
|
||||
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 Website, Tranga will
|
||||
additionally try and fetch Metadata from the same website or enhance it from third-party sources.
|
||||
additionally try and scrape Metadata from the same website ~~or enhance it from third-party sources~~
|
||||
([tbd issue](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).
|
||||
|
||||
@@ -88,7 +85,7 @@ Endpoints are documented in Swagger. Just spin up an instance, and go to `http:/
|
||||
## Built With
|
||||
|
||||
- ASP.NET
|
||||
- Entity Framework Core (EF Core)
|
||||
- Entity Framework Core
|
||||
- [PostgreSQL](https://www.postgresql.org/about/licence/)
|
||||
- [Ngpsql](https://github.com/npgsql/npgsql/blob/main/LICENSE)
|
||||
- [Swagger](https://github.com/domaindrivendev/Swashbuckle.AspNetCore/blob/master/LICENSE)
|
||||
@@ -140,7 +137,7 @@ Downloads (default) are stored in - but this can be configured in `settings.json
|
||||
- Linux `/Manga`
|
||||
- Windows `%currentDirectory%/Downloads`
|
||||
|
||||
### Prerequisits
|
||||
#### Prerequisits
|
||||
|
||||
[.NET-Core 9.0](https://dotnet.microsoft.com/en-us/download/dotnet/9.0)
|
||||
|
||||
@@ -149,67 +146,34 @@ Downloads (default) are stored in - but this can be configured in `settings.json
|
||||
|
||||
If you want to contribute, please feel free to fork and create a Pull-Request!
|
||||
|
||||
### General rules
|
||||
|
||||
- Strong-type your variables. This improves readability.
|
||||
- **DO**
|
||||
```csharp
|
||||
Manga[] zyx = Object.GetAnotherThing(); //I can see that zyx is an Array, without digging through more code
|
||||
```
|
||||
- **DO _NOT_**
|
||||
```csharp
|
||||
var xyz = Object.GetSomething(); //What is xyz? An Array? A string? An object?
|
||||
```
|
||||
|
||||
- Indent your `if` and `for` blocks
|
||||
- **DO**
|
||||
```csharp
|
||||
if(true)
|
||||
return false;
|
||||
```
|
||||
- **DO _NOT_**
|
||||
```csharp
|
||||
if(true) return false;
|
||||
```
|
||||
<details>
|
||||
<summary>Because try reading this</summary>
|
||||
|
||||
```csharp
|
||||
if (s.StartsWith("http://", StringComparison.OrdinalIgnoreCase) || s.StartsWith("https://", StringComparison.OrdinalIgnoreCase)) return s;
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
- When using shorthand, _this_ improves readability for longer lines (at some point just use if-else...):
|
||||
General rules:
|
||||
- Strongly-type your variables. This improves readability.
|
||||
```csharp
|
||||
bool retVal = xyz is true
|
||||
? false
|
||||
: true;
|
||||
```
|
||||
```csharp
|
||||
bool retVal = xyz?
|
||||
?? abc?
|
||||
?? true;
|
||||
var xyz = Object.GetSomething(); //Do not do this. What type is xyz (without looking at Method returns etc.)?
|
||||
Manga[] zyx = Object.GetAnotherThing(); //I can now easily see that zyx is an Array.
|
||||
```
|
||||
Tranga is using a code-first Entity-Framework Core approach. If you modify the db-table structure you need to create a migration.
|
||||
|
||||
### Database and EF Core
|
||||
|
||||
Tranga is using a **code-first** EF-Core approach. If you modify the database(context) structure you need to create a migration.
|
||||
|
||||
### A broad overview of where is what:
|
||||
**A broad overview of where is what:**<br />
|
||||
|
||||

|
||||
|
||||
- `Program.cs` Configuration for ASP.NET, Swagger (also in `NamedSwaggerGenOptions.cs`)
|
||||
- `Tranga.cs` Worker-Logic
|
||||
- `Schema/**` Entity-Framework Schema Definitions
|
||||
- `MangaDownloadClients/**` Networking-Clients for Scraping
|
||||
- `Controllers/**` ASP.NET Controllers (Endpoints)
|
||||
|
||||
##### If you want to add a new Website-Connector:
|
||||
- `Schema/` Entity-Framework
|
||||
- `Schema/Jobs/` + Logic for Jobs
|
||||
- `Schema/**/` + Logic for **
|
||||
- `Schema/Contexts/` 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 Website-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 `Tranga.cs` to the MangaConnector-Array `connectors`.
|
||||
2. Add the new Connector as Object-Instance in `Program.cs` to the MangaConnector-Array `connectors`.
|
||||
3. In `PgsqlContext.cs` add the Discriminator for the Connector (the value is the name of the connector, as defined
|
||||
in the constructor).
|
||||
4. In `Program.cs` add a new Object to the Array.
|
||||
|
||||
### How to test locally
|
||||
|
||||
|
@@ -28,9 +28,8 @@ services:
|
||||
- "5432:5432"
|
||||
environment:
|
||||
- POSTGRES_PASSWORD=postgres
|
||||
- POSTGRES_USER=postgres
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready", "-U", "postgres"]
|
||||
test: ["CMD-SHELL", "pg_isready"]
|
||||
interval: 30s
|
||||
timeout: 60s
|
||||
retries: 5
|
||||
|
@@ -39,9 +39,8 @@ services:
|
||||
- "5432:5432"
|
||||
environment:
|
||||
- POSTGRES_PASSWORD=postgres
|
||||
- POSTGRES_USER=postgres
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready", "-U", "postgres"]
|
||||
test: ["CMD-SHELL", "pg_isready"]
|
||||
interval: 30s
|
||||
timeout: 60s
|
||||
retries: 5
|
||||
|
Reference in New Issue
Block a user