mirror of
https://github.com/C9Glax/tranga.git
synced 2025-09-10 20:08:19 +02:00
Compare commits
37 Commits
bubez81/mw
...
6b4317834d
Author | SHA1 | Date | |
---|---|---|---|
6b4317834d | |||
88fef8417c | |||
eb9fc08b2d | |||
9743bb6e8e | |||
e8d612557f | |||
cf2dbeaf6a | |||
84940c414c | |||
ea627081b8 | |||
a90a6fb200 | |||
c3a0bb03e9 | |||
f8ccd2d69e | |||
ad224190a2 | |||
f05f2cc8e0 | |||
d6f0630a99 | |||
0ac4c23ac9 | |||
d6847d769e | |||
f6f5e21151 | |||
da3b5078af | |||
681d56710a | |||
6f5823596a | |||
8a06ed648c | |||
4dcd6ee035 | |||
e327e93163 | |||
6cd836540a | |||
91c91e4989 | |||
57bb87120a | |||
07880fedb5 | |||
f1d3203ae1 | |||
314700aa8e | |||
b299cc9109 | |||
7e9ba7090a | |||
ea73d03b8f | |||
e9d9bebcd7 | |||
e5937d2654 | |||
7c9e0eddf9 | |||
ae0c6c8240 | |||
2eb0d941b2 |
10
.github/workflows/docker-image-cuttingedge.yml
vendored
10
.github/workflows/docker-image-cuttingedge.yml
vendored
@@ -13,7 +13,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
# https://github.com/docker/setup-qemu-action#usage
|
# https://github.com/docker/setup-qemu-action#usage
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
@@ -22,7 +22,7 @@ jobs:
|
|||||||
# https://github.com/marketplace/actions/docker-setup-buildx
|
# https://github.com/marketplace/actions/docker-setup-buildx
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
id: buildx
|
id: buildx
|
||||||
uses: docker/setup-buildx-action@v3.11.1
|
uses: docker/setup-buildx-action@v3.10.0
|
||||||
|
|
||||||
# https://github.com/docker/login-action#docker-hub
|
# https://github.com/docker/login-action#docker-hub
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
@@ -33,12 +33,12 @@ jobs:
|
|||||||
|
|
||||||
# https://github.com/docker/build-push-action#multi-platform-image
|
# https://github.com/docker/build-push-action#multi-platform-image
|
||||||
- name: Build and push API
|
- name: Build and push API
|
||||||
uses: docker/build-push-action@v6.18.0
|
uses: docker/build-push-action@v6.15.0
|
||||||
with:
|
with:
|
||||||
context: ./
|
context: ./
|
||||||
file: ./Dockerfile
|
file: ./Dockerfile
|
||||||
#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/amd64/v2,linux/amd64/v3,linux/arm64,linux/arm/v7
|
platforms: linux/amd64,linux/arm64
|
||||||
pull: true
|
pull: true
|
||||||
push: true
|
push: true
|
||||||
tags: |
|
tags: |
|
||||||
|
6
.github/workflows/docker-image-master.yml
vendored
6
.github/workflows/docker-image-master.yml
vendored
@@ -13,7 +13,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
# https://github.com/docker/setup-qemu-action#usage
|
# https://github.com/docker/setup-qemu-action#usage
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
@@ -22,7 +22,7 @@ jobs:
|
|||||||
# https://github.com/marketplace/actions/docker-setup-buildx
|
# https://github.com/marketplace/actions/docker-setup-buildx
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
id: buildx
|
id: buildx
|
||||||
uses: docker/setup-buildx-action@v3.11.1
|
uses: docker/setup-buildx-action@v3.10.0
|
||||||
|
|
||||||
# https://github.com/docker/login-action#docker-hub
|
# https://github.com/docker/login-action#docker-hub
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
@@ -33,7 +33,7 @@ jobs:
|
|||||||
|
|
||||||
# https://github.com/docker/build-push-action#multi-platform-image
|
# https://github.com/docker/build-push-action#multi-platform-image
|
||||||
- name: Build and push API
|
- name: Build and push API
|
||||||
uses: docker/build-push-action@v6.18.0
|
uses: docker/build-push-action@v6.15.0
|
||||||
with:
|
with:
|
||||||
context: ./
|
context: ./
|
||||||
file: ./Dockerfile
|
file: ./Dockerfile
|
||||||
|
@@ -2,7 +2,7 @@ name: Docker Image CI
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [ "testing" ]
|
branches: [ "postgres-Server-V2" ]
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
@@ -13,7 +13,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
# https://github.com/docker/setup-qemu-action#usage
|
# https://github.com/docker/setup-qemu-action#usage
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
@@ -22,7 +22,7 @@ jobs:
|
|||||||
# https://github.com/marketplace/actions/docker-setup-buildx
|
# https://github.com/marketplace/actions/docker-setup-buildx
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
id: buildx
|
id: buildx
|
||||||
uses: docker/setup-buildx-action@v3.11.1
|
uses: docker/setup-buildx-action@v3.10.0
|
||||||
|
|
||||||
# https://github.com/docker/login-action#docker-hub
|
# https://github.com/docker/login-action#docker-hub
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
@@ -33,13 +33,13 @@ jobs:
|
|||||||
|
|
||||||
# https://github.com/docker/build-push-action#multi-platform-image
|
# https://github.com/docker/build-push-action#multi-platform-image
|
||||||
- name: Build and push API
|
- name: Build and push API
|
||||||
uses: docker/build-push-action@v6.18.0
|
uses: docker/build-push-action@v6.15.0
|
||||||
with:
|
with:
|
||||||
context: ./
|
context: ./
|
||||||
file: ./Dockerfile
|
file: ./Dockerfile
|
||||||
#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/amd64/v2,linux/amd64/v3,linux/arm64,linux/arm/v7
|
platforms: linux/amd64,linux/arm64
|
||||||
pull: true
|
pull: true
|
||||||
push: true
|
push: true
|
||||||
tags: |
|
tags: |
|
||||||
glax/tranga-api:testing
|
glax/tranga-api:Server-V2
|
@@ -11,32 +11,26 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Asp.Versioning.Mvc.ApiExplorer" Version="8.1.0" />
|
<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="JikanDotNet" Version="2.9.1" />
|
||||||
<PackageReference Include="log4net" Version="3.2.0" />
|
<PackageReference Include="log4net" Version="3.0.4" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="9.0.8" />
|
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="9.0.3" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.8" />
|
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.3" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.8">
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.3">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Proxies" Version="9.0.5" />
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||||
<PackageReference Include="Npgsql" Version="9.0.3" />
|
<PackageReference Include="Npgsql" Version="9.0.3" />
|
||||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4" />
|
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4" />
|
||||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.Design" Version="1.1.0" />
|
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.Design" Version="1.1.0" />
|
||||||
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.11" />
|
<PackageReference Include="PuppeteerSharp" Version="20.1.3" />
|
||||||
<PackageReference Include="Soenneker.Utils.String.NeedlemanWunsch" Version="3.0.978" />
|
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.7" />
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.4" />
|
<PackageReference Include="Soenneker.Utils.String.NeedlemanWunsch" Version="3.0.929" />
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore.Newtonsoft" Version="9.0.4" />
|
<PackageReference Include="Swashbuckle.AspNetCore" Version="8.1.0" />
|
||||||
<PackageReference Include="System.Drawing.Common" Version="9.0.8" />
|
<PackageReference Include="Swashbuckle.AspNetCore.Newtonsoft" Version="8.1.0" />
|
||||||
</ItemGroup>
|
<PackageReference Include="System.Drawing.Common" Version="9.0.3" />
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<Folder Include="Migrations\Manga\" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<None Include="Log4Net.config.xml" CopyToOutputDirectory="Always" CopyToPublishDirectory="Always" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</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 Asp.Versioning;
|
||||||
using Microsoft.AspNetCore.Http.HttpResults;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using static Microsoft.AspNetCore.Http.StatusCodes;
|
using static Microsoft.AspNetCore.Http.StatusCodes;
|
||||||
using FileLibrary = API.Controllers.DTOs.FileLibrary;
|
|
||||||
|
|
||||||
// ReSharper disable InconsistentNaming
|
// ReSharper disable InconsistentNaming
|
||||||
|
|
||||||
namespace API.Controllers;
|
namespace API.Controllers;
|
||||||
@@ -17,21 +12,14 @@ namespace API.Controllers;
|
|||||||
public class FileLibraryController(MangaContext context) : Controller
|
public class FileLibraryController(MangaContext context) : Controller
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns all <see cref="DTOs.FileLibrary"/>
|
/// Returns all <see cref="FileLibrary"/>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <response code="200"></response>
|
/// <response code="200"></response>
|
||||||
/// <response code="500">Error during Database Operation</response>
|
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
[ProducesResponseType<List<FileLibrary>>(Status200OK, "application/json")]
|
[ProducesResponseType<FileLibrary[]>(Status200OK, "application/json")]
|
||||||
[ProducesResponseType(Status500InternalServerError)]
|
public IActionResult GetFileLibraries()
|
||||||
public async Task<Results<Ok<List<FileLibrary>>, InternalServerError>> GetFileLibraries ()
|
|
||||||
{
|
{
|
||||||
if (await context.FileLibraries.ToListAsync(HttpContext.RequestAborted) is not { } result)
|
return Ok(context.FileLibraries.ToArray());
|
||||||
return TypedResults.InternalServerError();
|
|
||||||
|
|
||||||
List<FileLibrary> fileLibraries = result.Select(f => new FileLibrary(f.Key, f.BasePath, f.LibraryName)).ToList();
|
|
||||||
|
|
||||||
return TypedResults.Ok(fileLibraries);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -42,13 +30,13 @@ public class FileLibraryController(MangaContext context) : Controller
|
|||||||
/// <response code="404"><see cref="FileLibrary"/> with <paramref name="FileLibraryId"/> not found.</response>
|
/// <response code="404"><see cref="FileLibrary"/> with <paramref name="FileLibraryId"/> not found.</response>
|
||||||
[HttpGet("{FileLibraryId}")]
|
[HttpGet("{FileLibraryId}")]
|
||||||
[ProducesResponseType<FileLibrary>(Status200OK, "application/json")]
|
[ProducesResponseType<FileLibrary>(Status200OK, "application/json")]
|
||||||
[ProducesResponseType<string>(Status404NotFound, "text/plain")]
|
[ProducesResponseType(Status404NotFound)]
|
||||||
public async Task<Results<Ok<FileLibrary>, NotFound<string>>> GetFileLibrary (string FileLibraryId)
|
public IActionResult GetFileLibrary(string FileLibraryId)
|
||||||
{
|
{
|
||||||
if(await context.FileLibraries.FirstOrDefaultAsync(l => l.Key == FileLibraryId, HttpContext.RequestAborted) is not { } library)
|
if (context.FileLibraries.Find(FileLibraryId) is not { } library)
|
||||||
return TypedResults.NotFound(nameof(FileLibraryId));
|
return NotFound();
|
||||||
|
|
||||||
return TypedResults.Ok(new FileLibrary(library.Key, library.BasePath, library.LibraryName));
|
return Ok(library);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -61,19 +49,19 @@ public class FileLibraryController(MangaContext context) : Controller
|
|||||||
/// <response code="500">Error during Database Operation</response>
|
/// <response code="500">Error during Database Operation</response>
|
||||||
[HttpPatch("{FileLibraryId}/ChangeBasePath")]
|
[HttpPatch("{FileLibraryId}/ChangeBasePath")]
|
||||||
[ProducesResponseType(Status200OK)]
|
[ProducesResponseType(Status200OK)]
|
||||||
[ProducesResponseType<string>(Status404NotFound, "text/plain")]
|
[ProducesResponseType(Status404NotFound)]
|
||||||
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
|
[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)
|
if (context.FileLibraries.Find(FileLibraryId) is not { } library)
|
||||||
return TypedResults.NotFound(nameof(FileLibraryId));
|
return NotFound();
|
||||||
|
|
||||||
//TODO Path check
|
//TODO Path check
|
||||||
library.BasePath = newBasePath;
|
library.BasePath = newBasePath;
|
||||||
|
|
||||||
if(await context.Sync(HttpContext.RequestAborted) is { success: false } result)
|
if(context.Sync() is { success: false } result)
|
||||||
return TypedResults.InternalServerError(result.exceptionMessage);
|
return StatusCode(Status500InternalServerError, result.exceptionMessage);
|
||||||
return TypedResults.Ok();
|
return Ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -86,40 +74,40 @@ public class FileLibraryController(MangaContext context) : Controller
|
|||||||
/// <response code="500">Error during Database Operation</response>
|
/// <response code="500">Error during Database Operation</response>
|
||||||
[HttpPatch("{FileLibraryId}/ChangeName")]
|
[HttpPatch("{FileLibraryId}/ChangeName")]
|
||||||
[ProducesResponseType(Status200OK)]
|
[ProducesResponseType(Status200OK)]
|
||||||
[ProducesResponseType<string>(Status404NotFound, "text/plain")]
|
[ProducesResponseType(Status404NotFound)]
|
||||||
|
[ProducesResponseType(Status400BadRequest)]
|
||||||
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
|
[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)
|
if (context.FileLibraries.Find(FileLibraryId) is not { } library)
|
||||||
return TypedResults.NotFound(nameof(FileLibraryId));
|
return NotFound();
|
||||||
|
|
||||||
//TODO Name check
|
//TODO Name check
|
||||||
library.LibraryName = newName;
|
library.LibraryName = newName;
|
||||||
|
|
||||||
if(await context.Sync(HttpContext.RequestAborted) is { success: false } result)
|
if(context.Sync() is { success: false } result)
|
||||||
return TypedResults.InternalServerError(result.exceptionMessage);
|
return StatusCode(Status500InternalServerError, result.exceptionMessage);
|
||||||
return TypedResults.Ok();
|
return Ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates new <see cref="FileLibrary"/>
|
/// Creates new <see cref="FileLibraryId"/>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="requestData">New <see cref="FileLibrary"/> to add</param>
|
/// <param name="library">New <see cref="FileLibrary"/> to add</param>
|
||||||
/// <response code="200">Key of new Library</response>
|
/// <response code="200"></response>
|
||||||
/// <response code="500">Error during Database Operation</response>
|
/// <response code="500">Error during Database Operation</response>
|
||||||
[HttpPut]
|
[HttpPut]
|
||||||
[ProducesResponseType<string>(Status201Created, "text/plain")]
|
[ProducesResponseType(Status201Created)]
|
||||||
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
|
[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
|
//TODO Parameter check
|
||||||
Schema.MangaContext.FileLibrary library = new Schema.MangaContext.FileLibrary(requestData.BasePath, requestData.LibraryName);
|
|
||||||
context.FileLibraries.Add(library);
|
context.FileLibraries.Add(library);
|
||||||
|
|
||||||
if(await context.Sync(HttpContext.RequestAborted) is { success: false } result)
|
if(context.Sync() is { success: false } result)
|
||||||
return TypedResults.InternalServerError(result.exceptionMessage);
|
return StatusCode(Status500InternalServerError, result.exceptionMessage);
|
||||||
|
return Created();
|
||||||
return TypedResults.Created(string.Empty, library.Key);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -127,21 +115,20 @@ public class FileLibraryController(MangaContext context) : Controller
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="FileLibraryId"><see cref="FileLibrary"/>.Key</param>
|
/// <param name="FileLibraryId"><see cref="FileLibrary"/>.Key</param>
|
||||||
/// <response code="200"></response>
|
/// <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>
|
/// <response code="500">Error during Database Operation</response>
|
||||||
[HttpDelete("{FileLibraryId}")]
|
[HttpDelete("{FileLibraryId}")]
|
||||||
[ProducesResponseType(Status200OK)]
|
[ProducesResponseType(Status200OK)]
|
||||||
[ProducesResponseType<string>(Status404NotFound, "text/plain")]
|
[ProducesResponseType(Status404NotFound)]
|
||||||
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
|
[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)
|
if (context.FileLibraries.Find(FileLibraryId) is not { } library)
|
||||||
return TypedResults.NotFound(nameof(FileLibraryId));
|
return NotFound();
|
||||||
|
|
||||||
context.FileLibraries.Remove(library);
|
context.FileLibraries.Remove(library);
|
||||||
|
|
||||||
if(await context.Sync(HttpContext.RequestAborted) is { success: false } result)
|
if(context.Sync() is { success: false } result)
|
||||||
return TypedResults.InternalServerError(result.exceptionMessage);
|
return StatusCode(Status500InternalServerError, result.exceptionMessage);
|
||||||
return TypedResults.Ok();
|
return Ok();
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -1,13 +1,8 @@
|
|||||||
using API.Controllers.Requests;
|
using API.Schema.LibraryContext;
|
||||||
using API.Schema.LibraryContext;
|
|
||||||
using API.Schema.LibraryContext.LibraryConnectors;
|
using API.Schema.LibraryContext.LibraryConnectors;
|
||||||
using Asp.Versioning;
|
using Asp.Versioning;
|
||||||
using Microsoft.AspNetCore.Http.HttpResults;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using static Microsoft.AspNetCore.Http.StatusCodes;
|
using static Microsoft.AspNetCore.Http.StatusCodes;
|
||||||
using LibraryConnector = API.Controllers.DTOs.LibraryConnector;
|
|
||||||
|
|
||||||
// ReSharper disable InconsistentNaming
|
// ReSharper disable InconsistentNaming
|
||||||
|
|
||||||
namespace API.Controllers;
|
namespace API.Controllers;
|
||||||
@@ -18,20 +13,16 @@ namespace API.Controllers;
|
|||||||
public class LibraryConnectorController(LibraryContext context) : Controller
|
public class LibraryConnectorController(LibraryContext context) : Controller
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets all configured <see cref="DTOs.LibraryConnector"/>
|
/// Gets all configured <see cref="LibraryConnector"/>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <response code="200"></response>
|
/// <response code="200"></response>
|
||||||
/// <response code="500">Error during Database Operation</response>
|
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
[ProducesResponseType<List<LibraryConnector>>(Status200OK, "application/json")]
|
[ProducesResponseType<LibraryConnector[]>(Status200OK, "application/json")]
|
||||||
public async Task<Results<Ok<List<LibraryConnector>>, InternalServerError>> GetAllConnectors ()
|
public IActionResult GetAllConnectors()
|
||||||
{
|
{
|
||||||
if (await context.LibraryConnectors.ToListAsync(HttpContext.RequestAborted) is not { } connectors)
|
LibraryConnector[] connectors = context.LibraryConnectors.ToArray();
|
||||||
return TypedResults.InternalServerError();
|
|
||||||
|
|
||||||
List<LibraryConnector> libraryConnectors = connectors.Select(c => new LibraryConnector(c.Key, c.BaseUrl, c.LibraryType)).ToList();
|
return Ok(connectors);
|
||||||
|
|
||||||
return TypedResults.Ok(libraryConnectors);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -42,39 +33,32 @@ public class LibraryConnectorController(LibraryContext context) : Controller
|
|||||||
/// <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>
|
||||||
[HttpGet("{LibraryConnectorId}")]
|
[HttpGet("{LibraryConnectorId}")]
|
||||||
[ProducesResponseType<LibraryConnector>(Status200OK, "application/json")]
|
[ProducesResponseType<LibraryConnector>(Status200OK, "application/json")]
|
||||||
[ProducesResponseType<string>(Status404NotFound, "text/plain")]
|
[ProducesResponseType(Status404NotFound)]
|
||||||
public async Task<Results<Ok<LibraryConnector>, NotFound<string>>> GetConnector (string LibraryConnectorId)
|
public IActionResult GetConnector(string LibraryConnectorId)
|
||||||
{
|
{
|
||||||
if (await context.LibraryConnectors.FirstOrDefaultAsync(l => l.Key == LibraryConnectorId) is not { } connector)
|
if (context.LibraryConnectors.Find(LibraryConnectorId) is not { } connector)
|
||||||
return TypedResults.NotFound(nameof(LibraryConnectorId));
|
return NotFound();
|
||||||
|
|
||||||
return TypedResults.Ok(new LibraryConnector(connector.Key, connector.BaseUrl, connector.LibraryType));
|
return Ok(connector);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates a new <see cref="LibraryConnector"/>
|
/// Creates a new <see cref="LibraryConnector"/>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="requestData"></param>
|
/// <param name="libraryConnector"></param>
|
||||||
/// <response code="201"></response>
|
/// <response code="201"></response>
|
||||||
/// <response code="500">Error during Database Operation</response>
|
/// <response code="500">Error during Database Operation</response>
|
||||||
[HttpPut]
|
[HttpPut]
|
||||||
[ProducesResponseType<string>(Status201Created, "text/plain")]
|
[ProducesResponseType(Status201Created)]
|
||||||
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
|
[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)
|
if(context.Sync() is { success: false } result)
|
||||||
return TypedResults.InternalServerError(result.exceptionMessage);
|
return StatusCode(Status500InternalServerError, result.exceptionMessage);
|
||||||
return TypedResults.Created(string.Empty, connector.Key);
|
return Created();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -82,21 +66,21 @@ public class LibraryConnectorController(LibraryContext context) : Controller
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="LibraryConnectorId">ToFileLibrary-Connector-ID</param>
|
/// <param name="LibraryConnectorId">ToFileLibrary-Connector-ID</param>
|
||||||
/// <response code="200"></response>
|
/// <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>
|
/// <response code="500">Error during Database Operation</response>
|
||||||
[HttpDelete("{LibraryConnectorId}")]
|
[HttpDelete("{LibraryConnectorId}")]
|
||||||
[ProducesResponseType(Status200OK)]
|
[ProducesResponseType(Status200OK)]
|
||||||
[ProducesResponseType<string>(Status404NotFound, "text/plain")]
|
[ProducesResponseType(Status404NotFound)]
|
||||||
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
|
[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)
|
if (context.LibraryConnectors.Find(LibraryConnectorId) is not { } connector)
|
||||||
return TypedResults.NotFound(nameof(LibraryConnectorId));
|
return NotFound();
|
||||||
|
|
||||||
context.LibraryConnectors.Remove(connector);
|
context.LibraryConnectors.Remove(connector);
|
||||||
|
|
||||||
if(await context.Sync(HttpContext.RequestAborted) is { success: false } result)
|
if(context.Sync() is { success: false } result)
|
||||||
return TypedResults.InternalServerError(result.exceptionMessage);
|
return StatusCode(Status500InternalServerError, result.exceptionMessage);
|
||||||
return TypedResults.Ok();
|
return Ok();
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -1,39 +0,0 @@
|
|||||||
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;
|
|
||||||
|
|
||||||
namespace API.Controllers;
|
|
||||||
|
|
||||||
[ApiVersion(2)]
|
|
||||||
[ApiController]
|
|
||||||
[Route("v{v:apiVersion}/[controller]")]
|
|
||||||
public class MaintenanceController(MangaContext mangaContext) : Controller
|
|
||||||
{
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Removes all <see cref="Manga"/> not marked for Download on any <see cref="MangaConnector"/>
|
|
||||||
/// </summary>
|
|
||||||
/// <response code="200"></response>
|
|
||||||
/// <response code="500">Error during Database Operation</response>
|
|
||||||
[HttpPost("CleanupNoDownloadManga")]
|
|
||||||
[ProducesResponseType(Status200OK)]
|
|
||||||
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
|
|
||||||
public async Task<Results<Ok, InternalServerError<string>>> 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");
|
|
||||||
|
|
||||||
mangaContext.Mangas.RemoveRange(noDownloads);
|
|
||||||
|
|
||||||
if(await mangaContext.Sync(HttpContext.RequestAborted) is { success: false } result)
|
|
||||||
return TypedResults.InternalServerError(result.exceptionMessage);
|
|
||||||
return TypedResults.Ok();
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,7 +1,6 @@
|
|||||||
using API.Controllers.DTOs;
|
using API.Schema.MangaContext;
|
||||||
using API.Schema.MangaContext;
|
using API.Schema.MangaContext.MangaConnectors;
|
||||||
using Asp.Versioning;
|
using Asp.Versioning;
|
||||||
using Microsoft.AspNetCore.Http.HttpResults;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using static Microsoft.AspNetCore.Http.StatusCodes;
|
using static Microsoft.AspNetCore.Http.StatusCodes;
|
||||||
// ReSharper disable InconsistentNaming
|
// ReSharper disable InconsistentNaming
|
||||||
@@ -14,85 +13,78 @@ namespace API.Controllers;
|
|||||||
public class MangaConnectorController(MangaContext context) : Controller
|
public class MangaConnectorController(MangaContext context) : Controller
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Get all <see cref="API.MangaConnectors.MangaConnector"/> (Scanlation-Sites)
|
/// Get all <see cref="MangaConnector"/> (Scanlation-Sites)
|
||||||
/// </summary>
|
/// </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]
|
[HttpGet]
|
||||||
[ProducesResponseType<List<MangaConnector>>(Status200OK, "application/json")]
|
[ProducesResponseType<MangaConnector[]>(Status200OK, "application/json")]
|
||||||
public Ok<List<MangaConnector>> GetConnectors()
|
public IActionResult GetConnectors()
|
||||||
{
|
{
|
||||||
return TypedResults.Ok(Tranga.MangaConnectors
|
return Ok(context.MangaConnectors.Select(c => c.Name).ToArray());
|
||||||
.Select(c => new MangaConnector(c.Name, c.Enabled, c.IconUrl, c.SupportedLanguages))
|
|
||||||
.ToList());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <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>
|
/// </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="200"></response>
|
||||||
/// <response code="404"><see cref="MangaConnector"/> (Scanlation-Sites) with Name not found.</response>
|
/// <response code="404"><see cref="MangaConnector"/> (Scanlation-Sites) with Name not found.</response>
|
||||||
[HttpGet("{MangaConnectorName}")]
|
[HttpGet("{MangaConnectorName}")]
|
||||||
[ProducesResponseType<MangaConnector>(Status200OK, "application/json")]
|
[ProducesResponseType<MangaConnector>(Status200OK, "application/json")]
|
||||||
[ProducesResponseType<string>(Status404NotFound, "text/plain")]
|
[ProducesResponseType(Status404NotFound)]
|
||||||
public Results<Ok<MangaConnector>, NotFound<string>> GetConnector(string MangaConnectorName)
|
public IActionResult GetConnector(string MangaConnectorName)
|
||||||
{
|
{
|
||||||
if(Tranga.MangaConnectors.FirstOrDefault(c => c.Name.Equals(MangaConnectorName, StringComparison.InvariantCultureIgnoreCase)) is not { } connector)
|
if(context.MangaConnectors.Find(MangaConnectorName) 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>
|
/// <summary>
|
||||||
/// Get all enabled <see cref="API.MangaConnectors.MangaConnector"/> (Scanlation-Sites)
|
/// Get all enabled <see cref="MangaConnector"/> (Scanlation-Sites)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <response code="200"></response>
|
/// <response code="200"></response>
|
||||||
[HttpGet("Enabled")]
|
[HttpGet("Enabled")]
|
||||||
[ProducesResponseType<List<MangaConnector>>(Status200OK, "application/json")]
|
[ProducesResponseType<MangaConnector[]>(Status200OK, "application/json")]
|
||||||
public Ok<List<MangaConnector>> GetEnabledConnectors()
|
public IActionResult GetEnabledConnectors()
|
||||||
{
|
{
|
||||||
return TypedResults.Ok(Tranga.MangaConnectors
|
|
||||||
.Where(c => c.Enabled)
|
return Ok(context.MangaConnectors.Where(c => c.Enabled).ToArray());
|
||||||
.Select(c => new MangaConnector(c.Name, c.Enabled, c.IconUrl, c.SupportedLanguages))
|
|
||||||
.ToList());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Get all disabled <see cref="API.MangaConnectors.MangaConnector"/> (Scanlation-Sites)
|
/// Get all disabled <see cref="MangaConnector"/> (Scanlation-Sites)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <response code="200"></response>
|
/// <response code="200"></response>
|
||||||
[HttpGet("Disabled")]
|
[HttpGet("Disabled")]
|
||||||
[ProducesResponseType<List<MangaConnector>>(Status200OK, "application/json")]
|
[ProducesResponseType<MangaConnector[]>(Status200OK, "application/json")]
|
||||||
public Ok<List<MangaConnector>> GetDisabledConnectors()
|
public IActionResult GetDisabledConnectors()
|
||||||
{
|
{
|
||||||
|
|
||||||
return TypedResults.Ok(Tranga.MangaConnectors
|
return Ok(context.MangaConnectors.Where(c => c.Enabled == false).ToArray());
|
||||||
.Where(c => c.Enabled == false)
|
|
||||||
.Select(c => new MangaConnector(c.Name, c.Enabled, c.IconUrl, c.SupportedLanguages))
|
|
||||||
.ToList());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Enabled or disables <see cref="API.MangaConnectors.MangaConnector"/> (Scanlation-Sites) with Name
|
/// Enabled or disables <see cref="MangaConnector"/> (Scanlation-Sites) with Name
|
||||||
/// </summary>
|
/// </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>
|
/// <param name="Enabled">Set true to enable, false to disable</param>
|
||||||
/// <response code="200"></response>
|
/// <response code="202"></response>
|
||||||
/// <response code="404"><see cref="API.MangaConnectors.MangaConnector"/> (Scanlation-Sites) with Name not found.</response>
|
/// <response code="404"><see cref="MangaConnector"/> (Scanlation-Sites) with Name not found.</response>
|
||||||
/// <response code="500">Error during Database Operation</response>
|
/// <response code="500">Error during Database Operation</response>
|
||||||
[HttpPatch("{MangaConnectorName}/SetEnabled/{Enabled}")]
|
[HttpPatch("{MangaConnectorName}/SetEnabled/{Enabled}")]
|
||||||
[ProducesResponseType(Status200OK)]
|
[ProducesResponseType(Status202Accepted)]
|
||||||
[ProducesResponseType<string>(Status404NotFound, "text/plain")]
|
[ProducesResponseType(Status404NotFound)]
|
||||||
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
|
[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)
|
if(context.MangaConnectors.Find(MangaConnectorName) is not { } connector)
|
||||||
return TypedResults.NotFound(nameof(MangaConnectorName));
|
return NotFound();
|
||||||
|
|
||||||
connector.Enabled = Enabled;
|
connector.Enabled = Enabled;
|
||||||
|
|
||||||
if(await context.Sync(HttpContext.RequestAborted) is { success: false } result)
|
if(context.Sync() is { success: false } result)
|
||||||
return TypedResults.InternalServerError(result.exceptionMessage);
|
return StatusCode(Status500InternalServerError, result.exceptionMessage);
|
||||||
return TypedResults.Ok();
|
return Accepted();
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -1,19 +1,14 @@
|
|||||||
using API.Controllers.DTOs;
|
using API.Schema.MangaContext;
|
||||||
using API.Schema.MangaContext;
|
using API.Schema.MangaContext.MangaConnectors;
|
||||||
using API.Workers;
|
using API.Workers;
|
||||||
using Asp.Versioning;
|
using Asp.Versioning;
|
||||||
using Microsoft.AspNetCore.Http.HttpResults;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Microsoft.EntityFrameworkCore.ChangeTracking;
|
|
||||||
using Microsoft.Net.Http.Headers;
|
using Microsoft.Net.Http.Headers;
|
||||||
|
using SixLabors.ImageSharp;
|
||||||
|
using SixLabors.ImageSharp.Formats.Jpeg;
|
||||||
|
using SixLabors.ImageSharp.Processing;
|
||||||
|
using SixLabors.ImageSharp.Processing.Processors.Transforms;
|
||||||
using static Microsoft.AspNetCore.Http.StatusCodes;
|
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
|
// ReSharper disable InconsistentNaming
|
||||||
|
|
||||||
namespace API.Controllers;
|
namespace API.Controllers;
|
||||||
@@ -23,116 +18,45 @@ namespace API.Controllers;
|
|||||||
[Route("v{v:apiVersion}/[controller]")]
|
[Route("v{v:apiVersion}/[controller]")]
|
||||||
public class MangaController(MangaContext context) : Controller
|
public class MangaController(MangaContext context) : Controller
|
||||||
{
|
{
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns all cached <see cref="DTOs.Manga"/>
|
/// Returns all cached <see cref="Manga"/>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <response code="200"><see cref="MinimalManga"/> exert of <see cref="Schema.MangaContext.Manga"/>. Use <see cref="GetManga"/> for more information</response>
|
/// <response code="200"></response>
|
||||||
/// <response code="500">Error during Database Operation</response>
|
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
[ProducesResponseType<List<MinimalManga>>(Status200OK, "application/json")]
|
[ProducesResponseType<Manga[]>(Status200OK, "application/json")]
|
||||||
[ProducesResponseType(Status500InternalServerError)]
|
public IActionResult GetAllManga()
|
||||||
public async Task<Results<Ok<List<MinimalManga>>, InternalServerError>> GetAllManga ()
|
|
||||||
{
|
{
|
||||||
if (await context.Mangas.Include(m => m.MangaConnectorIds).ToArrayAsync(HttpContext.RequestAborted) is not
|
Manga[] ret = context.Mangas.ToArray();
|
||||||
{ } result)
|
return Ok(ret);
|
||||||
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>
|
/// <summary>
|
||||||
/// Returns all cached <see cref="Schema.MangaContext.Manga"/>.Keys
|
/// Returns all cached <see cref="Manga"/> with <paramref name="MangaIds"/>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <response code="200"><see cref="Schema.MangaContext.Manga"/> Keys/IDs</response>
|
/// <param name="MangaIds">Array of <<see cref="Manga"/>.Key</param>
|
||||||
/// <response code="500">Error during Database Operation</response>
|
/// <response code="200"></response>
|
||||||
[HttpGet("Keys")]
|
|
||||||
[ProducesResponseType<string[]>(Status200OK, "application/json")]
|
|
||||||
[ProducesResponseType(Status500InternalServerError)]
|
|
||||||
public async Task<Results<Ok<string[]>, InternalServerError>> GetAllMangaKeys ()
|
|
||||||
{
|
|
||||||
if (await context.Mangas.Select(m => m.Key).ToArrayAsync(HttpContext.RequestAborted) is not { } result)
|
|
||||||
return TypedResults.InternalServerError();
|
|
||||||
|
|
||||||
return TypedResults.Ok(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns all <see cref="Schema.MangaContext.Manga"/> that are being downloaded from at least one <see cref="API.MangaConnectors.MangaConnector"/>
|
|
||||||
/// </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>
|
|
||||||
[HttpPost("WithIDs")]
|
[HttpPost("WithIDs")]
|
||||||
[ProducesResponseType<List<Manga>>(Status200OK, "application/json")]
|
[ProducesResponseType<Manga[]>(Status200OK, "application/json")]
|
||||||
[ProducesResponseType(Status500InternalServerError)]
|
public IActionResult GetManga([FromBody]string[] MangaIds)
|
||||||
public async Task<Results<Ok<List<Manga>>, InternalServerError>> GetMangaWithIds ([FromBody]string[] MangaIds)
|
|
||||||
{
|
{
|
||||||
if (await context.MangaIncludeAll()
|
Manga[] ret = context.Mangas.Where(m => MangaIds.Contains(m.Key)).ToArray();
|
||||||
.Where(m => MangaIds.Contains(m.Key))
|
return Ok(ret);
|
||||||
.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());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Return <see cref="Schema.MangaContext.Manga"/> with <paramref name="MangaId"/>
|
/// Return <see cref="Manga"/> with <paramref name="MangaId"/>
|
||||||
/// </summary>
|
/// </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="200"></response>
|
||||||
/// <response code="404"><see cref="Manga"/> with <paramref name="MangaId"/> not found</response>
|
/// <response code="404"><see cref="Manga"/> with <paramref name="MangaId"/> not found</response>
|
||||||
[HttpGet("{MangaId}")]
|
[HttpGet("{MangaId}")]
|
||||||
[ProducesResponseType<Manga>(Status200OK, "application/json")]
|
[ProducesResponseType<Manga>(Status200OK, "application/json")]
|
||||||
[ProducesResponseType<string>(Status404NotFound, "text/plain")]
|
[ProducesResponseType(Status404NotFound)]
|
||||||
public async Task<Results<Ok<Manga>, NotFound<string>>> GetManga (string MangaId)
|
public IActionResult GetManga(string MangaId)
|
||||||
{
|
{
|
||||||
if (await context.MangaIncludeAll().FirstOrDefaultAsync(m => m.Key == MangaId, HttpContext.RequestAborted) is not { } manga)
|
if (context.Mangas.Find(MangaId) is not { } manga)
|
||||||
return TypedResults.NotFound(nameof(MangaId));
|
return NotFound(nameof(MangaId));
|
||||||
|
return Ok(manga);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -140,22 +64,22 @@ public class MangaController(MangaContext context) : Controller
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="MangaId"><see cref="Manga"/>.Key</param>
|
/// <param name="MangaId"><see cref="Manga"/>.Key</param>
|
||||||
/// <response code="200"></response>
|
/// <response code="200"></response>
|
||||||
/// <response code="404"><see cref="Manga"/> with <paramref name="MangaId"/> not found</response>
|
/// <response code="404"><<see cref="Manga"/> with <paramref name="MangaId"/> not found</response>
|
||||||
/// <response code="500">Error during Database Operation</response>
|
/// <response code="500">Error during Database Operation</response>
|
||||||
[HttpDelete("{MangaId}")]
|
[HttpDelete("{MangaId}")]
|
||||||
[ProducesResponseType(Status200OK)]
|
[ProducesResponseType(Status200OK)]
|
||||||
[ProducesResponseType<string>(Status404NotFound, "text/plain")]
|
[ProducesResponseType(Status404NotFound)]
|
||||||
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
|
[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)
|
if (context.Mangas.Find(MangaId) is not { } manga)
|
||||||
return TypedResults.NotFound(nameof(MangaId));
|
return NotFound(nameof(MangaId));
|
||||||
|
|
||||||
context.Mangas.Remove(manga);
|
context.Mangas.Remove(manga);
|
||||||
|
|
||||||
if(await context.Sync(HttpContext.RequestAborted) is { success: false } result)
|
if(context.Sync() is { success: false } result)
|
||||||
return TypedResults.InternalServerError(result.exceptionMessage);
|
return StatusCode(Status500InternalServerError, result.exceptionMessage);
|
||||||
return TypedResults.Ok();
|
return Ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -167,71 +91,73 @@ public class MangaController(MangaContext context) : Controller
|
|||||||
/// <response code="200"></response>
|
/// <response code="200"></response>
|
||||||
/// <response code="404"><see cref="Manga"/> with <paramref name="MangaIdFrom"/> or <paramref name="MangaIdInto"/> not found</response>
|
/// <response code="404"><see cref="Manga"/> with <paramref name="MangaIdFrom"/> or <paramref name="MangaIdInto"/> not found</response>
|
||||||
[HttpPatch("{MangaIdFrom}/MergeInto/{MangaIdInto}")]
|
[HttpPatch("{MangaIdFrom}/MergeInto/{MangaIdInto}")]
|
||||||
[ProducesResponseType(Status200OK)]
|
[ProducesResponseType<byte[]>(Status200OK,"image/jpeg")]
|
||||||
[ProducesResponseType<string>(Status404NotFound, "text/plain")]
|
[ProducesResponseType(Status404NotFound)]
|
||||||
public async Task<Results<Ok, NotFound<string>>> MergeIntoManga (string MangaIdFrom, string MangaIdInto)
|
public IActionResult MergeIntoManga(string MangaIdFrom, string MangaIdInto)
|
||||||
{
|
{
|
||||||
if (await context.MangaIncludeAll().FirstOrDefaultAsync(m => m.Key == MangaIdFrom, HttpContext.RequestAborted) is not { } from)
|
if (context.Mangas.Find(MangaIdFrom) is not { } from)
|
||||||
return TypedResults.NotFound(nameof(MangaIdFrom));
|
return NotFound(nameof(MangaIdFrom));
|
||||||
if (await context.MangaIncludeAll().FirstOrDefaultAsync(m => m.Key == MangaIdInto, HttpContext.RequestAborted) is not { } into)
|
if (context.Mangas.Find(MangaIdInto) is not { } into)
|
||||||
return TypedResults.NotFound(nameof(MangaIdInto));
|
return NotFound(nameof(MangaIdInto));
|
||||||
|
|
||||||
BaseWorker[] newJobs = into.MergeFrom(from, context);
|
BaseWorker[] newJobs = into.MergeFrom(from, context);
|
||||||
Tranga.AddWorkers(newJobs);
|
Tranga.AddWorkers(newJobs);
|
||||||
|
|
||||||
return TypedResults.Ok();
|
return Ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns Cover of <see cref="Manga"/> with <paramref name="MangaId"/>
|
/// Returns Cover of <see cref="Manga"/> with <paramref name="MangaId"/>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="MangaId"><see cref="Manga"/>.Key</param>
|
/// <param name="MangaId"><see cref="Manga"/>.Key</param>
|
||||||
/// <param name="CoverSize">Size of the cover returned
|
/// <param name="width">If <paramref name="width"/> is provided, <paramref name="height"/> needs to also be provided</param>
|
||||||
/// <br /> - <see cref="CoverSize.Small"/> <see cref="Constants.ImageSmSize"/>
|
/// <param name="height">If <paramref name="height"/> is provided, <paramref name="width"/> needs to also be provided</param>
|
||||||
/// <br /> - <see cref="CoverSize.Medium"/> <see cref="Constants.ImageMdSize"/>
|
|
||||||
/// <br /> - <see cref="CoverSize.Large"/> <see cref="Constants.ImageLgSize"/>
|
|
||||||
/// </param>
|
|
||||||
/// <response code="200">JPEG Image</response>
|
/// <response code="200">JPEG Image</response>
|
||||||
/// <response code="204">Cover not loaded</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="404"><see cref="Manga"/> with <paramref name="MangaId"/> not found</response>
|
||||||
/// <response code="503">Retry later, downloading cover</response>
|
/// <response code="503">Retry later, downloading cover</response>
|
||||||
[HttpGet("{MangaId}/Cover/{CoverSize?}")]
|
[HttpGet("{MangaId}/Cover")]
|
||||||
[ProducesResponseType<FileContentResult>(Status200OK,"image/jpeg")]
|
[ProducesResponseType<byte[]>(Status200OK,"image/jpeg")]
|
||||||
[ProducesResponseType(Status204NoContent)]
|
[ProducesResponseType(Status204NoContent)]
|
||||||
[ProducesResponseType(Status400BadRequest)]
|
[ProducesResponseType(Status400BadRequest)]
|
||||||
[ProducesResponseType<string>(Status404NotFound, "text/plain")]
|
[ProducesResponseType(Status404NotFound)]
|
||||||
[ProducesResponseType(Status503ServiceUnavailable)]
|
[ProducesResponseType<int>(Status503ServiceUnavailable, "text/plain")]
|
||||||
public async Task<Results<FileContentHttpResult, NoContent, BadRequest, NotFound<string>, StatusCodeHttpResult>> GetCover (string MangaId, CoverSize? CoverSize = null)
|
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)
|
if (context.Mangas.Find(MangaId) is not { } manga)
|
||||||
return TypedResults.NotFound(nameof(MangaId));
|
return NotFound(nameof(MangaId));
|
||||||
|
|
||||||
string cache = CoverSize switch
|
if (!System.IO.File.Exists(manga.CoverFileNameInCache))
|
||||||
{
|
|
||||||
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 (Tranga.GetRunningWorkers().Any(worker => worker is DownloadCoverFromMangaconnectorWorker w && context.MangaConnectorToManga.Find(w.MangaConnectorIdId)?.ObjId == MangaId))
|
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}");
|
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;
|
Image image = Image.Load(manga.CoverFileNameInCache);
|
||||||
EntityTagHeaderValue entityTagHeaderValue = EntityTagHeaderValue.Parse($"\"{lastModified.Ticks}\"");
|
|
||||||
if(HttpContext.Request.Headers.ETag.Equals(entityTagHeaderValue.Tag.Value))
|
if (width is { } w && height is { } h)
|
||||||
return TypedResults.StatusCode(Status304NotModified);
|
{
|
||||||
|
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";
|
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>
|
/// <summary>
|
||||||
/// Returns all <see cref="Chapter"/> of <see cref="Manga"/> with <paramref name="MangaId"/>
|
/// Returns all <see cref="Chapter"/> of <see cref="Manga"/> with <paramref name="MangaId"/>
|
||||||
@@ -240,20 +166,15 @@ public class MangaController(MangaContext context) : Controller
|
|||||||
/// <response code="200"></response>
|
/// <response code="200"></response>
|
||||||
/// <response code="404"><see cref="Manga"/> with <paramref name="MangaId"/> not found</response>
|
/// <response code="404"><see cref="Manga"/> with <paramref name="MangaId"/> not found</response>
|
||||||
[HttpGet("{MangaId}/Chapters")]
|
[HttpGet("{MangaId}/Chapters")]
|
||||||
[ProducesResponseType<List<Chapter>>(Status200OK, "application/json")]
|
[ProducesResponseType<Chapter[]>(Status200OK, "application/json")]
|
||||||
[ProducesResponseType(Status404NotFound)]
|
[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)
|
if (context.Mangas.Find(MangaId) is not { } manga)
|
||||||
return TypedResults.NotFound(nameof(MangaId));
|
return NotFound(nameof(MangaId));
|
||||||
|
|
||||||
List<Chapter> chapters = manga.Chapters.Select(c =>
|
Chapter[] chapters = manga.Chapters.ToArray();
|
||||||
{
|
return Ok(chapters);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -266,22 +187,17 @@ public class MangaController(MangaContext context) : Controller
|
|||||||
[HttpGet("{MangaId}/Chapters/Downloaded")]
|
[HttpGet("{MangaId}/Chapters/Downloaded")]
|
||||||
[ProducesResponseType<Chapter[]>(Status200OK, "application/json")]
|
[ProducesResponseType<Chapter[]>(Status200OK, "application/json")]
|
||||||
[ProducesResponseType(Status204NoContent)]
|
[ProducesResponseType(Status204NoContent)]
|
||||||
[ProducesResponseType<string>(Status404NotFound, "text/plain")]
|
[ProducesResponseType(Status404NotFound)]
|
||||||
public async Task<Results<Ok<List<Chapter>>, NoContent, NotFound<string>>> GetChaptersDownloaded(string MangaId)
|
public IActionResult GetChaptersDownloaded(string MangaId)
|
||||||
{
|
{
|
||||||
if (await context.Mangas.Include(m => m.Chapters).FirstOrDefaultAsync(m => m.Key == MangaId, HttpContext.RequestAborted) is not { } manga)
|
if (context.Mangas.Find(MangaId) is not { } manga)
|
||||||
return TypedResults.NotFound(nameof(MangaId));
|
return NotFound(nameof(MangaId));
|
||||||
|
|
||||||
List<Chapter> chapters = manga.Chapters.Where(c => c.Downloaded).Select(c =>
|
List<Chapter> chapters = manga.Chapters.Where(c => c.Downloaded).ToList();
|
||||||
{
|
|
||||||
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();
|
|
||||||
if (chapters.Count == 0)
|
if (chapters.Count == 0)
|
||||||
return TypedResults.NoContent();
|
return NoContent();
|
||||||
|
|
||||||
return TypedResults.Ok(chapters);
|
return Ok(chapters);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -292,28 +208,23 @@ public class MangaController(MangaContext context) : Controller
|
|||||||
/// <response code="204">No available chapters</response>
|
/// <response code="204">No available chapters</response>
|
||||||
/// <response code="404"><see cref="Manga"/> with <paramref name="MangaId"/> not found.</response>
|
/// <response code="404"><see cref="Manga"/> with <paramref name="MangaId"/> not found.</response>
|
||||||
[HttpGet("{MangaId}/Chapters/NotDownloaded")]
|
[HttpGet("{MangaId}/Chapters/NotDownloaded")]
|
||||||
[ProducesResponseType<List<Chapter>>(Status200OK, "application/json")]
|
[ProducesResponseType<Chapter[]>(Status200OK, "application/json")]
|
||||||
[ProducesResponseType(Status204NoContent)]
|
[ProducesResponseType(Status204NoContent)]
|
||||||
[ProducesResponseType<string>(Status404NotFound, "text/plain")]
|
[ProducesResponseType(Status404NotFound)]
|
||||||
public async Task<Results<Ok<List<Chapter>>, NoContent, NotFound<string>>> GetChaptersNotDownloaded(string MangaId)
|
public IActionResult GetChaptersNotDownloaded(string MangaId)
|
||||||
{
|
{
|
||||||
if (await context.Mangas.Include(m => m.Chapters).FirstOrDefaultAsync(m => m.Key == MangaId, HttpContext.RequestAborted) is not { } manga)
|
if (context.Mangas.Find(MangaId) is not { } manga)
|
||||||
return TypedResults.NotFound(nameof(MangaId));
|
return NotFound(nameof(MangaId));
|
||||||
|
|
||||||
List<Chapter> chapters = manga.Chapters.Where(c => c.Downloaded == false).Select(c =>
|
List<Chapter> chapters = manga.Chapters.Where(c => c.Downloaded == false).ToList();
|
||||||
{
|
|
||||||
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();
|
|
||||||
if (chapters.Count == 0)
|
if (chapters.Count == 0)
|
||||||
return TypedResults.NoContent();
|
return NoContent();
|
||||||
|
|
||||||
return TypedResults.Ok(chapters);
|
return Ok(chapters);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <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>
|
/// </summary>
|
||||||
/// <param name="MangaId"><see cref="Manga"/>.Key</param>
|
/// <param name="MangaId"><see cref="Manga"/>.Key</param>
|
||||||
/// <response code="200"></response>
|
/// <response code="200"></response>
|
||||||
@@ -322,41 +233,32 @@ public class MangaController(MangaContext context) : Controller
|
|||||||
/// <response code="412">Could not retrieve the maximum chapter-number</response>
|
/// <response code="412">Could not retrieve the maximum chapter-number</response>
|
||||||
/// <response code="503">Retry after timeout, updating value</response>
|
/// <response code="503">Retry after timeout, updating value</response>
|
||||||
[HttpGet("{MangaId}/Chapter/LatestAvailable")]
|
[HttpGet("{MangaId}/Chapter/LatestAvailable")]
|
||||||
[ProducesResponseType<int>(Status200OK, "application/json")]
|
[ProducesResponseType<Chapter>(Status200OK, "application/json")]
|
||||||
[ProducesResponseType(Status204NoContent)]
|
[ProducesResponseType(Status204NoContent)]
|
||||||
[ProducesResponseType<string>(Status404NotFound, "text/plain")]
|
[ProducesResponseType<string>(Status404NotFound, "text/plain")]
|
||||||
[ProducesResponseType(Status500InternalServerError)]
|
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
|
||||||
[ProducesResponseType(Status503ServiceUnavailable)]
|
[ProducesResponseType<int>(Status503ServiceUnavailable, "text/plain")]
|
||||||
public async Task<Results<Ok<Chapter>, NoContent, InternalServerError, NotFound<string>, StatusCodeHttpResult>> GetLatestChapter(string MangaId)
|
public IActionResult GetLatestChapter(string MangaId)
|
||||||
{
|
{
|
||||||
if (await context.Mangas.Include(m => m.Chapters).FirstOrDefaultAsync(m => m.Key == MangaId, HttpContext.RequestAborted) is not { } manga)
|
if (context.Mangas.Find(MangaId) is not { } manga)
|
||||||
return TypedResults.NotFound(nameof(MangaId));
|
return NotFound(nameof(MangaId));
|
||||||
|
|
||||||
List<API.Schema.MangaContext.Chapter> chapters = manga.Chapters.ToList();
|
List<Chapter> chapters = manga.Chapters.ToList();
|
||||||
if (chapters.Count == 0)
|
if (chapters.Count == 0)
|
||||||
{
|
{
|
||||||
if (Tranga.GetRunningWorkers().Any(worker =>
|
if (Tranga.GetRunningWorkers().Any(worker => worker is RetrieveMangaChaptersFromMangaconnectorWorker w && context.MangaConnectorToManga.Find(w.MangaConnectorIdId)?.ObjId == MangaId && w.State < WorkerExecutionState.Completed))
|
||||||
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}");
|
Response.Headers.Append("Retry-After", $"{Tranga.Settings.WorkCycleTimeoutMs * 2 / 1000:D}");
|
||||||
return TypedResults.StatusCode(Status503ServiceUnavailable);
|
return StatusCode(Status503ServiceUnavailable, Tranga.Settings.WorkCycleTimeoutMs * 2/ 1000);
|
||||||
}
|
}else
|
||||||
else
|
return Ok(0);
|
||||||
return TypedResults.NoContent();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
API.Schema.MangaContext.Chapter? max = chapters.Max();
|
Chapter? max = chapters.Max();
|
||||||
if (max is null)
|
if (max is null)
|
||||||
return TypedResults.InternalServerError();
|
return StatusCode(Status500InternalServerError, "Max chapter could not be found");
|
||||||
|
|
||||||
foreach (CollectionEntry collectionEntry in context.Entry(max).Collections)
|
return Ok(max);
|
||||||
await collectionEntry.LoadAsync(HttpContext.RequestAborted);
|
|
||||||
|
|
||||||
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));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -371,34 +273,30 @@ public class MangaController(MangaContext context) : Controller
|
|||||||
[HttpGet("{MangaId}/Chapter/LatestDownloaded")]
|
[HttpGet("{MangaId}/Chapter/LatestDownloaded")]
|
||||||
[ProducesResponseType<Chapter>(Status200OK, "application/json")]
|
[ProducesResponseType<Chapter>(Status200OK, "application/json")]
|
||||||
[ProducesResponseType(Status204NoContent)]
|
[ProducesResponseType(Status204NoContent)]
|
||||||
[ProducesResponseType<string>(Status404NotFound, "text/plain")]
|
[ProducesResponseType(Status404NotFound)]
|
||||||
[ProducesResponseType(Status412PreconditionFailed)]
|
[ProducesResponseType<string>(Status412PreconditionFailed, "text/plain")]
|
||||||
[ProducesResponseType(Status503ServiceUnavailable)]
|
[ProducesResponseType<int>(Status503ServiceUnavailable, "text/plain")]
|
||||||
public async Task<Results<Ok<Chapter>, NoContent, NotFound<string>, StatusCodeHttpResult>> GetLatestChapterDownloaded(string MangaId)
|
public IActionResult GetLatestChapterDownloaded(string MangaId)
|
||||||
{
|
{
|
||||||
if (await context.Mangas.FirstOrDefaultAsync(m => m.Key == MangaId, HttpContext.RequestAborted) is not { } manga)
|
if (context.Mangas.Find(MangaId) is not { } manga)
|
||||||
return TypedResults.NotFound(nameof(MangaId));
|
return NotFound(nameof(MangaId));
|
||||||
|
|
||||||
await context.Entry(manga).Collection(m => m.Chapters).LoadAsync(HttpContext.RequestAborted);
|
List<Chapter> chapters = manga.Chapters.ToList();
|
||||||
|
|
||||||
List<API.Schema.MangaContext.Chapter> chapters = manga.Chapters.ToList();
|
|
||||||
if (chapters.Count == 0)
|
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}");
|
Response.Headers.Append("Retry-After", $"{Tranga.Settings.WorkCycleTimeoutMs * 2 / 1000:D}");
|
||||||
return TypedResults.StatusCode(Status503ServiceUnavailable);
|
return StatusCode(Status503ServiceUnavailable, Tranga.Settings.WorkCycleTimeoutMs * 2/ 1000);
|
||||||
}else
|
}else
|
||||||
return TypedResults.NoContent();
|
return NoContent();
|
||||||
}
|
}
|
||||||
|
|
||||||
API.Schema.MangaContext.Chapter? max = chapters.Max();
|
Chapter? max = chapters.Max();
|
||||||
if (max is null)
|
if (max is null)
|
||||||
return TypedResults.StatusCode(Status412PreconditionFailed);
|
return StatusCode(Status412PreconditionFailed, "Max chapter could not be found");
|
||||||
|
|
||||||
IEnumerable<MangaConnectorId> ids = max.MangaConnectorIds.Select(id =>
|
return Ok(max);
|
||||||
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));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -410,19 +308,19 @@ public class MangaController(MangaContext context) : Controller
|
|||||||
/// <response code="404"><see cref="Manga"/> with <paramref name="MangaId"/> not found.</response>
|
/// <response code="404"><see cref="Manga"/> with <paramref name="MangaId"/> not found.</response>
|
||||||
/// <response code="500">Error during Database Operation</response>
|
/// <response code="500">Error during Database Operation</response>
|
||||||
[HttpPatch("{MangaId}/IgnoreChaptersBefore")]
|
[HttpPatch("{MangaId}/IgnoreChaptersBefore")]
|
||||||
[ProducesResponseType(Status200OK)]
|
[ProducesResponseType(Status202Accepted)]
|
||||||
[ProducesResponseType<string>(Status404NotFound, "text/plain")]
|
[ProducesResponseType(Status404NotFound)]
|
||||||
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
|
[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)
|
if (context.Mangas.Find(MangaId) is not { } manga)
|
||||||
return TypedResults.NotFound(nameof(MangaId));
|
return NotFound();
|
||||||
|
|
||||||
manga.IgnoreChaptersBefore = chapterThreshold;
|
manga.IgnoreChaptersBefore = chapterThreshold;
|
||||||
if(await context.Sync(HttpContext.RequestAborted) is { success: false } result)
|
if(context.Sync() is { success: false } result)
|
||||||
return TypedResults.InternalServerError(result.exceptionMessage);
|
return StatusCode(Status500InternalServerError, result.exceptionMessage);
|
||||||
|
|
||||||
return TypedResults.Ok();
|
return Accepted();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -433,35 +331,32 @@ public class MangaController(MangaContext context) : Controller
|
|||||||
/// <response code="202">Folder is going to be moved</response>
|
/// <response code="202">Folder is going to be moved</response>
|
||||||
/// <response code="404"><paramref name="MangaId"/> or <paramref name="LibraryId"/> not found</response>
|
/// <response code="404"><paramref name="MangaId"/> or <paramref name="LibraryId"/> not found</response>
|
||||||
[HttpPost("{MangaId}/ChangeLibrary/{LibraryId}")]
|
[HttpPost("{MangaId}/ChangeLibrary/{LibraryId}")]
|
||||||
[ProducesResponseType(Status200OK)]
|
[ProducesResponseType(Status202Accepted)]
|
||||||
[ProducesResponseType<string>(Status404NotFound, "text/plain")]
|
[ProducesResponseType(Status404NotFound)]
|
||||||
public async Task<Results<Ok, NotFound<string>>> ChangeLibrary(string MangaId, string LibraryId)
|
public IActionResult MoveFolder(string MangaId, string LibraryId)
|
||||||
{
|
{
|
||||||
if (await context.Mangas.FirstOrDefaultAsync(m => m.Key == MangaId, HttpContext.RequestAborted) is not { } manga)
|
if (context.Mangas.Find(MangaId) is not { } manga)
|
||||||
return TypedResults.NotFound(nameof(MangaId));
|
return NotFound(nameof(MangaId));
|
||||||
if (await context.FileLibraries.FirstOrDefaultAsync(l => l.Key == LibraryId, HttpContext.RequestAborted) is not { } library)
|
if(context.FileLibraries.Find(LibraryId) is not { } library)
|
||||||
return TypedResults.NotFound(nameof(LibraryId));
|
return NotFound(nameof(LibraryId));
|
||||||
|
|
||||||
if(manga.LibraryId == library.Key)
|
|
||||||
return TypedResults.Ok();
|
|
||||||
|
|
||||||
MoveMangaLibraryWorker moveLibrary = new(manga, library);
|
MoveMangaLibraryWorker moveLibrary = new(manga, library);
|
||||||
|
|
||||||
Tranga.AddWorkers([moveLibrary]);
|
Tranga.AddWorkers([moveLibrary]);
|
||||||
|
|
||||||
return TypedResults.Ok();
|
return Accepted();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <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>
|
/// </summary>
|
||||||
/// <param name="MangaId"><see cref="Manga"/> with <paramref name="MangaId"/></param>
|
/// <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>
|
/// <param name="IsRequested">true to mark as requested, false to mark as not-requested</param>
|
||||||
/// <response code="200"></response>
|
/// <response code="200"></response>
|
||||||
/// <response code="404"><paramref name="MangaId"/> or <paramref name="MangaConnectorName"/> not found</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="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="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="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>
|
/// <response code="500">Error during Database Operation</response>
|
||||||
[HttpPost("{MangaId}/SetAsDownloadFrom/{MangaConnectorName}/{IsRequested}")]
|
[HttpPost("{MangaId}/SetAsDownloadFrom/{MangaConnectorName}/{IsRequested}")]
|
||||||
[ProducesResponseType(Status200OK)]
|
[ProducesResponseType(Status200OK)]
|
||||||
@@ -469,111 +364,27 @@ public class MangaController(MangaContext context) : Controller
|
|||||||
[ProducesResponseType<string>(Status412PreconditionFailed, "text/plain")]
|
[ProducesResponseType<string>(Status412PreconditionFailed, "text/plain")]
|
||||||
[ProducesResponseType<string>(Status428PreconditionRequired, "text/plain")]
|
[ProducesResponseType<string>(Status428PreconditionRequired, "text/plain")]
|
||||||
[ProducesResponseType<string>(Status500InternalServerError, "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 { } _)
|
if (context.Mangas.Find(MangaId) is null)
|
||||||
return TypedResults.NotFound(nameof(MangaId));
|
return NotFound(nameof(MangaId));
|
||||||
if(!Tranga.TryGetMangaConnector(MangaConnectorName, out API.MangaConnectors.MangaConnector? _))
|
if(context.MangaConnectors.Find(MangaConnectorName) is null)
|
||||||
return TypedResults.NotFound(nameof(MangaConnectorName));
|
return NotFound(nameof(MangaConnectorName));
|
||||||
|
|
||||||
if (context.MangaConnectorToManga
|
if (context.MangaConnectorToManga.FirstOrDefault(id => id.MangaConnectorName == MangaConnectorName && id.ObjId == MangaId) is not { } mcId)
|
||||||
.FirstOrDefault(id => id.MangaConnectorName == MangaConnectorName && id.ObjId == MangaId)
|
|
||||||
is not { } mcId)
|
|
||||||
{
|
|
||||||
if(IsRequested)
|
if(IsRequested)
|
||||||
return TypedResults.StatusCode(Status428PreconditionRequired);
|
return StatusCode(Status428PreconditionRequired, "Don't know how to download this Manga from MangaConnector");
|
||||||
else
|
else
|
||||||
return TypedResults.StatusCode(Status412PreconditionFailed);
|
return StatusCode(Status412PreconditionFailed, "Not linked anyways.");
|
||||||
}
|
|
||||||
|
|
||||||
mcId.UseForDownload = IsRequested;
|
mcId.UseForDownload = IsRequested;
|
||||||
if(await context.Sync(HttpContext.RequestAborted) is { success: false } result)
|
if(context.Sync() is { success: false } result)
|
||||||
return TypedResults.InternalServerError(result.exceptionMessage);
|
return StatusCode(Status500InternalServerError, result.exceptionMessage);
|
||||||
|
|
||||||
DownloadCoverFromMangaconnectorWorker downloadCover = new(mcId);
|
DownloadCoverFromMangaconnectorWorker downloadCover = new(mcId);
|
||||||
RetrieveMangaChaptersFromMangaconnectorWorker retrieveChapters = new(mcId, Tranga.Settings.DownloadLanguage);
|
RetrieveMangaChaptersFromMangaconnectorWorker retrieveChapters = new(mcId, Tranga.Settings.DownloadLanguage);
|
||||||
Tranga.AddWorkers([downloadCover, retrieveChapters]);
|
Tranga.AddWorkers([downloadCover, retrieveChapters]);
|
||||||
|
|
||||||
return TypedResults.Ok();
|
return Ok();
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initiate a search for <see cref="API.Schema.MangaContext.Manga"/> on a different <see cref="API.MangaConnectors.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>
|
|
||||||
[HttpPost("{MangaId}/SearchOn/{MangaConnectorName}")]
|
|
||||||
[ProducesResponseType<List<MinimalManga>>(Status200OK, "application/json")]
|
|
||||||
[ProducesResponseType<string>(Status404NotFound, "text/plain")]
|
|
||||||
[ProducesResponseType(Status406NotAcceptable)]
|
|
||||||
public async Task<Results<Ok<List<MinimalManga>>, NotFound<string>, StatusCodeHttpResult>> SearchOnDifferentConnector (string MangaId, string MangaConnectorName)
|
|
||||||
{
|
|
||||||
if (await context.Mangas.FirstOrDefaultAsync(m => m.Key == MangaId, HttpContext.RequestAborted) is not { } manga)
|
|
||||||
return TypedResults.NotFound(nameof(MangaId));
|
|
||||||
|
|
||||||
return new SearchController(context).SearchManga(MangaConnectorName, manga.Name);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns all <see cref="Manga"/> which where Authored by <see cref="Author"/> with <paramref name="AuthorId"/>
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="AuthorId"><see cref="Author"/>.Key</param>
|
|
||||||
/// <response code="200"></response>
|
|
||||||
/// <response code="404"><see cref="Author"/> with <paramref name="AuthorId"/></response>
|
|
||||||
/// /// <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)
|
|
||||||
{
|
|
||||||
if (await context.Authors.FirstOrDefaultAsync(a => a.Key == AuthorId, HttpContext.RequestAborted) is not { } _)
|
|
||||||
return TypedResults.NotFound(nameof(AuthorId));
|
|
||||||
|
|
||||||
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());
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns all <see cref="Manga"/> with <see cref="Tag"/>
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="Tag"><see cref="Tag"/>.Tag</param>
|
|
||||||
/// <response code="200"></response>
|
|
||||||
/// <response code="404"><see cref="Tag"/> not found</response>
|
|
||||||
/// <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)
|
|
||||||
{
|
|
||||||
if (await context.Tags.FirstOrDefaultAsync(t => t.Tag == Tag, HttpContext.RequestAborted) is not { } tag)
|
|
||||||
return TypedResults.NotFound(nameof(Tag));
|
|
||||||
|
|
||||||
|
|
||||||
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());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -1,10 +1,8 @@
|
|||||||
using API.Schema.MangaContext;
|
using API.Schema.MangaContext;
|
||||||
using API.Schema.MangaContext.MetadataFetchers;
|
using API.Schema.MangaContext.MetadataFetchers;
|
||||||
using Asp.Versioning;
|
using Asp.Versioning;
|
||||||
using Microsoft.AspNetCore.Http.HttpResults;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using static Microsoft.AspNetCore.Http.StatusCodes;
|
using static Microsoft.AspNetCore.Http.StatusCodes;
|
||||||
// ReSharper disable InconsistentNaming
|
// ReSharper disable InconsistentNaming
|
||||||
|
|
||||||
@@ -20,26 +18,21 @@ public class MetadataFetcherController(MangaContext context) : Controller
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <response code="200">Names of <see cref="MetadataFetcher"/> (Metadata-Sites)</response>
|
/// <response code="200">Names of <see cref="MetadataFetcher"/> (Metadata-Sites)</response>
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
[ProducesResponseType<List<string>>(Status200OK, "application/json")]
|
[ProducesResponseType<string[]>(Status200OK, "application/json")]
|
||||||
public Ok<List<string>> GetConnectors ()
|
public IActionResult GetConnectors()
|
||||||
{
|
{
|
||||||
return TypedResults.Ok(Tranga.MetadataFetchers.Select(m => m.Name).ToList());
|
return Ok(Tranga.MetadataFetchers.Select(m => m.Name).ToArray());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns all <see cref="MetadataEntry"/>
|
/// Returns all <see cref="MetadataEntry"/>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <response code="200"></response>
|
/// <response code="200"></response>
|
||||||
/// <response code="500">Error during Database Operation</response>
|
|
||||||
[HttpGet("Links")]
|
[HttpGet("Links")]
|
||||||
[ProducesResponseType<List<MetadataEntry>>(Status200OK, "application/json")]
|
[ProducesResponseType<MetadataEntry[]>(Status200OK, "application/json")]
|
||||||
[ProducesResponseType(Status500InternalServerError)]
|
public IActionResult GetLinkedEntries()
|
||||||
public async Task<Results<Ok<List<MetadataEntry>>, InternalServerError>> GetLinkedEntries ()
|
|
||||||
{
|
{
|
||||||
if (await context.MetadataEntries.ToListAsync() is not { } result)
|
return Ok(context.MetadataEntries.ToArray());
|
||||||
return TypedResults.InternalServerError();
|
|
||||||
|
|
||||||
return TypedResults.Ok(result);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -54,16 +47,16 @@ public class MetadataFetcherController(MangaContext context) : Controller
|
|||||||
[HttpPost("{MetadataFetcherName}/SearchManga/{MangaId}")]
|
[HttpPost("{MetadataFetcherName}/SearchManga/{MangaId}")]
|
||||||
[ProducesResponseType<MetadataSearchResult[]>(Status200OK, "application/json")]
|
[ProducesResponseType<MetadataSearchResult[]>(Status200OK, "application/json")]
|
||||||
[ProducesResponseType(Status400BadRequest)]
|
[ProducesResponseType(Status400BadRequest)]
|
||||||
[ProducesResponseType<string>(Status404NotFound, "text/plain")]
|
[ProducesResponseType(Status404NotFound)]
|
||||||
public async Task<Results<Ok<List<MetadataSearchResult>>, BadRequest, NotFound<string>>> SearchMangaMetadata(string MangaId, string MetadataFetcherName, [FromBody (EmptyBodyBehavior = EmptyBodyBehavior.Allow)]string? searchTerm = null)
|
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)
|
if(context.Mangas.Find(MangaId) is not { } manga)
|
||||||
return TypedResults.NotFound(nameof(MangaId));
|
return NotFound();
|
||||||
if(Tranga.MetadataFetchers.FirstOrDefault(f => f.Name == MetadataFetcherName) is not { } fetcher)
|
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);
|
MetadataSearchResult[] searchResults = searchTerm is null ? fetcher.SearchMetadataEntry(manga) : fetcher.SearchMetadataEntry(searchTerm);
|
||||||
return TypedResults.Ok(searchResults.ToList());
|
return Ok(searchResults);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -79,21 +72,21 @@ public class MetadataFetcherController(MangaContext context) : Controller
|
|||||||
[HttpPost("{MetadataFetcherName}/Link/{MangaId}")]
|
[HttpPost("{MetadataFetcherName}/Link/{MangaId}")]
|
||||||
[ProducesResponseType<MetadataEntry>(Status200OK, "application/json")]
|
[ProducesResponseType<MetadataEntry>(Status200OK, "application/json")]
|
||||||
[ProducesResponseType(Status400BadRequest)]
|
[ProducesResponseType(Status400BadRequest)]
|
||||||
[ProducesResponseType<string>(Status404NotFound, "text/plain")]
|
[ProducesResponseType(Status404NotFound)]
|
||||||
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
|
[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)
|
if(context.Mangas.Find(MangaId) is not { } manga)
|
||||||
return TypedResults.NotFound(nameof(MangaId));
|
return NotFound();
|
||||||
if(Tranga.MetadataFetchers.FirstOrDefault(f => f.Name == MetadataFetcherName) is not { } fetcher)
|
if(Tranga.MetadataFetchers.FirstOrDefault(f => f.Name == MetadataFetcherName) is not { } fetcher)
|
||||||
return TypedResults.BadRequest();
|
return BadRequest();
|
||||||
|
|
||||||
MetadataEntry entry = fetcher.CreateMetadataEntry(manga, Identifier);
|
MetadataEntry entry = fetcher.CreateMetadataEntry(manga, Identifier);
|
||||||
context.MetadataEntries.Add(entry);
|
context.MetadataEntries.Add(entry);
|
||||||
|
|
||||||
if(await context.Sync(HttpContext.RequestAborted) is { success: false } result)
|
if(context.Sync() is { } errorMessage)
|
||||||
return TypedResults.InternalServerError(result.exceptionMessage);
|
return StatusCode(Status500InternalServerError, errorMessage);
|
||||||
return TypedResults.Ok(entry);
|
return Ok(entry);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -107,23 +100,22 @@ public class MetadataFetcherController(MangaContext context) : Controller
|
|||||||
[HttpPost("{MetadataFetcherName}/Unlink/{MangaId}")]
|
[HttpPost("{MetadataFetcherName}/Unlink/{MangaId}")]
|
||||||
[ProducesResponseType(Status200OK)]
|
[ProducesResponseType(Status200OK)]
|
||||||
[ProducesResponseType(Status400BadRequest)]
|
[ProducesResponseType(Status400BadRequest)]
|
||||||
[ProducesResponseType<string>(Status404NotFound, "text/plain")]
|
[ProducesResponseType(Status404NotFound)]
|
||||||
[ProducesResponseType(Status412PreconditionFailed)]
|
[ProducesResponseType<string>(Status412PreconditionFailed, "text/plain")]
|
||||||
[ProducesResponseType<string>(Status500InternalServerError, "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 { } _)
|
if(context.Mangas.Find(MangaId) is null)
|
||||||
return TypedResults.NotFound(nameof(MangaId));
|
return NotFound();
|
||||||
if(Tranga.MetadataFetchers.FirstOrDefault(f => f.Name == MetadataFetcherName) is null)
|
if(Tranga.MetadataFetchers.FirstOrDefault(f => f.Name == MetadataFetcherName) is null)
|
||||||
return TypedResults.BadRequest();
|
return BadRequest();
|
||||||
if (context.MetadataEntries.FirstOrDefault(e =>
|
if(context.MetadataEntries.FirstOrDefault(e => e.MangaId == MangaId && e.MetadataFetcherName == MetadataFetcherName) is not { } entry)
|
||||||
e.MangaId == MangaId && e.MetadataFetcherName == MetadataFetcherName) is not { } entry)
|
return StatusCode(Status412PreconditionFailed, "No entry found");
|
||||||
return TypedResults.StatusCode(Status412PreconditionFailed);
|
|
||||||
|
|
||||||
context.Remove(entry);
|
context.Remove(entry);
|
||||||
|
|
||||||
if(await context.Sync(HttpContext.RequestAborted) is { success: false } result)
|
if(context.Sync() is { success: false } result)
|
||||||
return TypedResults.InternalServerError(result.exceptionMessage);
|
return StatusCode(Status500InternalServerError, result.exceptionMessage);
|
||||||
return TypedResults.Ok();
|
return Ok();
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -1,11 +1,9 @@
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
using API.Controllers.DTOs;
|
using API.APIEndpointRecords;
|
||||||
using API.Controllers.Requests;
|
|
||||||
using API.Schema.NotificationsContext;
|
using API.Schema.NotificationsContext;
|
||||||
|
using API.Schema.NotificationsContext.NotificationConnectors;
|
||||||
using Asp.Versioning;
|
using Asp.Versioning;
|
||||||
using Microsoft.AspNetCore.Http.HttpResults;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using static Microsoft.AspNetCore.Http.StatusCodes;
|
using static Microsoft.AspNetCore.Http.StatusCodes;
|
||||||
// ReSharper disable InconsistentNaming
|
// ReSharper disable InconsistentNaming
|
||||||
|
|
||||||
@@ -21,18 +19,12 @@ public class NotificationConnectorController(NotificationsContext context) : Con
|
|||||||
/// Gets all configured <see cref="NotificationConnector"/>
|
/// Gets all configured <see cref="NotificationConnector"/>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <response code="200"></response>
|
/// <response code="200"></response>
|
||||||
/// <response code="500">Error during Database Operation</response>
|
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
[ProducesResponseType<List<NotificationConnector>>(Status200OK, "application/json")]
|
[ProducesResponseType<NotificationConnector[]>(Status200OK, "application/json")]
|
||||||
[ProducesResponseType(Status500InternalServerError)]
|
public IActionResult GetAllConnectors()
|
||||||
public async Task<Results<Ok<List<NotificationConnector>>, InternalServerError>> 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 Ok(context.NotificationConnectors.ToArray());
|
||||||
|
|
||||||
return TypedResults.Ok(notificationConnectors);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -43,101 +35,103 @@ public class NotificationConnectorController(NotificationsContext context) : Con
|
|||||||
/// <response code="404"><see cref="NotificationConnector"/> with <paramref name="Name"/> not found</response>
|
/// <response code="404"><see cref="NotificationConnector"/> with <paramref name="Name"/> not found</response>
|
||||||
[HttpGet("{Name}")]
|
[HttpGet("{Name}")]
|
||||||
[ProducesResponseType<NotificationConnector>(Status200OK, "application/json")]
|
[ProducesResponseType<NotificationConnector>(Status200OK, "application/json")]
|
||||||
[ProducesResponseType<string>(Status404NotFound, "text/plain")]
|
[ProducesResponseType(Status404NotFound)]
|
||||||
public async Task<Results<Ok<NotificationConnector>, NotFound<string>>> GetConnector (string Name)
|
public IActionResult GetConnector(string Name)
|
||||||
{
|
{
|
||||||
if (await context.NotificationConnectors.FirstOrDefaultAsync(c => c.Name == Name, HttpContext.RequestAborted) is not { } connector)
|
if(context.NotificationConnectors.Find(Name) is not { } connector)
|
||||||
return TypedResults.NotFound(nameof(Name));
|
return NotFound();
|
||||||
|
|
||||||
NotificationConnector notificationConnector = new NotificationConnector(connector.Name, connector.Url, connector.HttpMethod, connector.Body, connector.Headers);
|
return Ok(connector);
|
||||||
|
|
||||||
return TypedResults.Ok(notificationConnector);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates a new <see cref="NotificationConnector"/>
|
/// Creates a new <see cref="NotificationConnector"/>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>Formatting placeholders: "%title" and "%text" can be placed in url, header-values and body and will be replaced when notifications are sent</remarks>
|
/// <remarks>Formatting placeholders: "%title" and "%text" can be placed in url, header-values and body and will be replaced when notifications are sent</remarks>
|
||||||
/// <response code="200">ID of the new <see cref="NotificationConnector"/></response>
|
/// <response code="201"></response>
|
||||||
/// <response code="500">Error during Database Operation</response>
|
/// <response code="500">Error during Database Operation</response>
|
||||||
[HttpPut]
|
[HttpPut]
|
||||||
[ProducesResponseType<string>(Status201Created, "text/plain")]
|
[ProducesResponseType(Status201Created)]
|
||||||
|
[ProducesResponseType(Status409Conflict)]
|
||||||
[ProducesResponseType<string>(Status500InternalServerError, "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(newConnector);
|
context.NotificationConnectors.Add(notificationConnector);
|
||||||
context.Notifications.Add(new ("Added new Notification Connector!", newConnector.Name, NotificationUrgency.High));
|
|
||||||
|
|
||||||
if(await context.Sync(HttpContext.RequestAborted) is { success: false } result)
|
if(context.Sync() is { success: false } result)
|
||||||
return TypedResults.InternalServerError(result.exceptionMessage);
|
return StatusCode(Status500InternalServerError, result.exceptionMessage);
|
||||||
return TypedResults.Created(string.Empty, newConnector.Name);
|
return Created();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates a new Gotify-<see cref="NotificationConnector"/>
|
/// Creates a new Gotify-<see cref="NotificationConnector"/>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>Priority needs to be between 0 and 10</remarks>
|
/// <remarks>Priority needs to be between 0 and 10</remarks>
|
||||||
/// <response code="200">ID of the new <see cref="NotificationConnector"/></response>
|
/// <response code="201"></response>
|
||||||
/// <response code="500">Error during Database Operation</response>
|
/// <response code="500">Error during Database Operation</response>
|
||||||
[HttpPut("Gotify")]
|
[HttpPut("Gotify")]
|
||||||
[ProducesResponseType<string>(Status201Created, "text/plain")]
|
[ProducesResponseType<string>(Status201Created, "application/json")]
|
||||||
[ProducesResponseType<string>(Status500InternalServerError, "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
|
//TODO Validate Data
|
||||||
CreateNotificationConnectorRecord gotifyConnector = new (createGotifyConnectorData.Name,
|
|
||||||
createGotifyConnectorData.Url,
|
NotificationConnector gotifyConnector = new (gotifyData.Name,
|
||||||
|
gotifyData.Endpoint,
|
||||||
|
new Dictionary<string, string>() { { "X-Gotify-IDOnConnector", gotifyData.AppToken } },
|
||||||
"POST",
|
"POST",
|
||||||
$"{{\"message\": \"%text\", \"title\": \"%title\", \"Priority\": {createGotifyConnectorData.Priority}}}",
|
$"{{\"message\": \"%text\", \"title\": \"%title\", \"Priority\": {gotifyData.Priority}}}");
|
||||||
new () { { "X-Gotify-Key", createGotifyConnectorData.AppToken } });
|
return CreateConnector(gotifyConnector);
|
||||||
return await CreateConnector(gotifyConnector);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates a new Ntfy-<see cref="NotificationConnector"/>
|
/// Creates a new Ntfy-<see cref="NotificationConnector"/>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>Priority needs to be between 1 and 5</remarks>
|
/// <remarks>Priority needs to be between 1 and 5</remarks>
|
||||||
/// <response code="200">ID of the new <see cref="NotificationConnector"/></response>
|
/// <response code="201"></response>
|
||||||
/// <response code="500">Error during Database Operation</response>
|
/// <response code="500">Error during Database Operation</response>
|
||||||
[HttpPut("Ntfy")]
|
[HttpPut("Ntfy")]
|
||||||
[ProducesResponseType<string>(Status201Created, "text/plain")]
|
[ProducesResponseType<string>(Status201Created, "application/json")]
|
||||||
[ProducesResponseType<string>(Status500InternalServerError, "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
|
//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("=","");
|
string auth = Convert.ToBase64String(Encoding.UTF8.GetBytes(authHeader)).Replace("=","");
|
||||||
|
|
||||||
CreateNotificationConnectorRecord ntfyConnector = new (createNtfyConnectorRecord.Name,
|
NotificationConnector ntfyConnector = new (ntfyRecord.Name,
|
||||||
$"{createNtfyConnectorRecord.Url}?auth={auth}",
|
$"{ntfyRecord.Endpoint}/{ntfyRecord.Topic}?auth={auth}",
|
||||||
|
new Dictionary<string, string>()
|
||||||
|
{
|
||||||
|
{"Title", "%title"},
|
||||||
|
{"Priority", ntfyRecord.Priority.ToString()},
|
||||||
|
},
|
||||||
"POST",
|
"POST",
|
||||||
$"{{\"message\": \"%text\", \"title\": \"%title\", \"Priority\": {createNtfyConnectorRecord.Priority} \"Topic\": \"{createNtfyConnectorRecord.Topic}\"}}",
|
"%text");
|
||||||
new () {{"Authorization", auth}});
|
return CreateConnector(ntfyConnector);
|
||||||
return await CreateConnector(ntfyConnector);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates a new Pushover-<see cref="NotificationConnector"/>
|
/// Creates a new Pushover-<see cref="NotificationConnector"/>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>https://pushover.net/api</remarks>
|
/// <remarks>https://pushover.net/api</remarks>
|
||||||
/// <response code="200">ID of the new <see cref="NotificationConnector"/></response>
|
/// <response code="201">ID of new connector</response>
|
||||||
/// <response code="500">Error during Database Operation</response>
|
/// <response code="500">Error during Database Operation</response>
|
||||||
[HttpPut("Pushover")]
|
[HttpPut("Pushover")]
|
||||||
[ProducesResponseType<string>(Status201Created, "text/plain")]
|
[ProducesResponseType<string>(Status201Created, "application/json")]
|
||||||
[ProducesResponseType<string>(Status500InternalServerError, "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
|
//TODO Validate Data
|
||||||
CreateNotificationConnectorRecord pushoverConnector = new (createPushoverConnectorRecord.Name,
|
|
||||||
|
NotificationConnector pushoverConnector = new (pushoverRecord.Name,
|
||||||
$"https://api.pushover.net/1/messages.json",
|
$"https://api.pushover.net/1/messages.json",
|
||||||
|
new Dictionary<string, string>(),
|
||||||
"POST",
|
"POST",
|
||||||
$"{{\"token\": \"{createPushoverConnectorRecord.AppToken}\", \"user\": \"{createPushoverConnectorRecord.Username}\", \"message:\":\"%text\", \"%title\" }}",
|
$"{{\"token\": \"{pushoverRecord.AppToken}\", \"user\": \"{pushoverRecord.User}\", \"message:\":\"%text\", \"%title\" }}");
|
||||||
new ());
|
return CreateConnector(pushoverConnector);
|
||||||
return await CreateConnector(pushoverConnector);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -149,17 +143,17 @@ public class NotificationConnectorController(NotificationsContext context) : Con
|
|||||||
/// <response code="500">Error during Database Operation</response>
|
/// <response code="500">Error during Database Operation</response>
|
||||||
[HttpDelete("{Name}")]
|
[HttpDelete("{Name}")]
|
||||||
[ProducesResponseType(Status200OK)]
|
[ProducesResponseType(Status200OK)]
|
||||||
[ProducesResponseType<string>(Status404NotFound, "text/plain")]
|
[ProducesResponseType(Status404NotFound)]
|
||||||
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
|
[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)
|
if(context.NotificationConnectors.Find(Name) is not { } connector)
|
||||||
return TypedResults.NotFound(nameof(Name));
|
return NotFound();
|
||||||
|
|
||||||
context.NotificationConnectors.Remove(connector);
|
context.NotificationConnectors.Remove(connector);
|
||||||
|
|
||||||
if(await context.Sync(HttpContext.RequestAborted) is { success: false } result)
|
if(context.Sync() is { success: false } result)
|
||||||
return TypedResults.InternalServerError(result.exceptionMessage);
|
return StatusCode(Status500InternalServerError, result.exceptionMessage);
|
||||||
return TypedResults.Ok();
|
return Created();
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -1,14 +1,7 @@
|
|||||||
using API.Controllers.DTOs;
|
using API.Schema.MangaContext;
|
||||||
using API.Schema.MangaContext;
|
|
||||||
using Asp.Versioning;
|
using Asp.Versioning;
|
||||||
using Microsoft.AspNetCore.Http.HttpResults;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Soenneker.Utils.String.NeedlemanWunsch;
|
|
||||||
using static Microsoft.AspNetCore.Http.StatusCodes;
|
using static Microsoft.AspNetCore.Http.StatusCodes;
|
||||||
using Author = API.Controllers.DTOs.Author;
|
|
||||||
using Chapter = API.Controllers.DTOs.Chapter;
|
|
||||||
|
|
||||||
// ReSharper disable InconsistentNaming
|
// ReSharper disable InconsistentNaming
|
||||||
|
|
||||||
namespace API.Controllers;
|
namespace API.Controllers;
|
||||||
@@ -26,13 +19,45 @@ public class QueryController(MangaContext context) : Controller
|
|||||||
/// <response code="404"><see cref="Author"/> with <paramref name="AuthorId"/> not found</response>
|
/// <response code="404"><see cref="Author"/> with <paramref name="AuthorId"/> not found</response>
|
||||||
[HttpGet("Author/{AuthorId}")]
|
[HttpGet("Author/{AuthorId}")]
|
||||||
[ProducesResponseType<Author>(Status200OK, "application/json")]
|
[ProducesResponseType<Author>(Status200OK, "application/json")]
|
||||||
[ProducesResponseType<string>(Status404NotFound, "text/plain")]
|
[ProducesResponseType(Status404NotFound)]
|
||||||
public async Task<Results<Ok<Author>, NotFound<string>>> GetAuthor (string AuthorId)
|
public IActionResult GetAuthor(string AuthorId)
|
||||||
{
|
{
|
||||||
if (await context.Authors.FirstOrDefaultAsync(a => a.Key == AuthorId, HttpContext.RequestAborted) is not { } author)
|
if (context.Authors.Find(AuthorId) is not { } author)
|
||||||
return TypedResults.NotFound(nameof(AuthorId));
|
return NotFound();
|
||||||
|
|
||||||
return TypedResults.Ok(new Author(author.Key, author.AuthorName));
|
return Ok(author);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns all <see cref="Manga"/> which where Authored by <see cref="Author"/> with <paramref name="AuthorId"/>
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="AuthorId"><see cref="Author"/>.Key</param>
|
||||||
|
/// <response code="200"></response>
|
||||||
|
/// <response code="404"><see cref="Author"/> with <paramref name="AuthorId"/></response>
|
||||||
|
[HttpGet("Mangas/WithAuthorId/{AuthorId}")]
|
||||||
|
[ProducesResponseType<Manga[]>(Status200OK, "application/json")]
|
||||||
|
public IActionResult GetMangaWithAuthorIds(string AuthorId)
|
||||||
|
{
|
||||||
|
if (context.Authors.Find(AuthorId) is not { } author)
|
||||||
|
return NotFound();
|
||||||
|
|
||||||
|
return Ok(context.Mangas.Where(m => m.Authors.Contains(author)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns all <see cref="Manga"/> with <see cref="Tag"/>
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="Tag"><see cref="Tag"/>.Tag</param>
|
||||||
|
/// <response code="200"></response>
|
||||||
|
/// <response code="404"><see cref="Tag"/> not found</response>
|
||||||
|
[HttpGet("Mangas/WithTag/{Tag}")]
|
||||||
|
[ProducesResponseType<Manga[]>(Status200OK, "application/json")]
|
||||||
|
public IActionResult GetMangasWithTag(string Tag)
|
||||||
|
{
|
||||||
|
if (context.Tags.Find(Tag) is not { } tag)
|
||||||
|
return NotFound();
|
||||||
|
|
||||||
|
return Ok(context.Mangas.Where(m => m.MangaTags.Contains(tag)));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -43,82 +68,11 @@ public class QueryController(MangaContext context) : Controller
|
|||||||
/// <response code="404"><see cref="Chapter"/> with <paramref name="ChapterId"/> not found</response>
|
/// <response code="404"><see cref="Chapter"/> with <paramref name="ChapterId"/> not found</response>
|
||||||
[HttpGet("Chapter/{ChapterId}")]
|
[HttpGet("Chapter/{ChapterId}")]
|
||||||
[ProducesResponseType<Chapter>(Status200OK, "application/json")]
|
[ProducesResponseType<Chapter>(Status200OK, "application/json")]
|
||||||
[ProducesResponseType<string>(Status404NotFound, "text/plain")]
|
public IActionResult GetChapter(string ChapterId)
|
||||||
public async Task<Results<Ok<Chapter>, NotFound<string>>> GetChapter (string ChapterId)
|
|
||||||
{
|
{
|
||||||
if (await context.Chapters.FirstOrDefaultAsync(c => c.Key == ChapterId, HttpContext.RequestAborted) is not { } chapter)
|
if (context.Chapters.Find(ChapterId) is not { } chapter)
|
||||||
return TypedResults.NotFound(nameof(ChapterId));
|
return NotFound();
|
||||||
|
|
||||||
IEnumerable<MangaConnectorId> ids = chapter.MangaConnectorIds.Select(id =>
|
return Ok(chapter);
|
||||||
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));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns the <see cref="MangaConnectorId{Manga}"/> with <see cref="MangaConnectorId{Manga}"/>.Key
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="MangaConnectorIdId">Key of <see cref="MangaConnectorId{Manga}"/></param>
|
|
||||||
/// <response code="200"></response>
|
|
||||||
/// <response code="404"><see cref="MangaConnectorId{Manga}"/> with <paramref name="MangaConnectorIdId"/> not found</response>
|
|
||||||
[HttpGet("Manga/MangaConnectorId/{MangaConnectorIdId}")]
|
|
||||||
[ProducesResponseType<MangaConnectorId>(Status200OK, "application/json")]
|
|
||||||
[ProducesResponseType<string>(Status404NotFound, "text/plain")]
|
|
||||||
public async Task<Results<Ok<MangaConnectorId>, NotFound<string>>> GetMangaMangaConnectorId (string MangaConnectorIdId)
|
|
||||||
{
|
|
||||||
if (await context.MangaConnectorToManga.FirstOrDefaultAsync(c => c.Key == MangaConnectorIdId, HttpContext.RequestAborted) is not { } mcIdManga)
|
|
||||||
return TypedResults.NotFound(nameof(MangaConnectorIdId));
|
|
||||||
|
|
||||||
MangaConnectorId result = new (mcIdManga.Key, mcIdManga.MangaConnectorName, mcIdManga.ObjId, mcIdManga.WebsiteUrl, mcIdManga.UseForDownload);
|
|
||||||
|
|
||||||
return TypedResults.Ok(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns <see cref="Schema.MangaContext.Manga"/> with names similar to <see cref="Schema.MangaContext.Manga"/> (identified by <paramref name="MangaId"/>)
|
|
||||||
/// </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)
|
|
||||||
{
|
|
||||||
if (await context.Mangas.FirstOrDefaultAsync(m => m.Key == MangaId, HttpContext.RequestAborted) is not { } manga)
|
|
||||||
return TypedResults.NotFound(nameof(MangaId));
|
|
||||||
|
|
||||||
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
|
|
||||||
.Where(kv => NeedlemanWunschStringUtil.CalculateSimilarityPercentage(name, kv.Value) > 0.8)
|
|
||||||
.Select(kv => kv.Key)
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
return TypedResults.Ok(similarIds);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns the <see cref="MangaConnectorId{Chapter}"/> with <see cref="MangaConnectorId{Chapter}"/>.Key
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="MangaConnectorIdId">Key of <see cref="MangaConnectorId{Manga}"/></param>
|
|
||||||
/// <response code="200"></response>
|
|
||||||
/// <response code="404"><see cref="MangaConnectorId{Chapter}"/> with <paramref name="MangaConnectorIdId"/> not found</response>
|
|
||||||
[HttpGet("Chapter/MangaConnectorId/{MangaConnectorIdId}")]
|
|
||||||
[ProducesResponseType<MangaConnectorId>(Status200OK, "application/json")]
|
|
||||||
[ProducesResponseType<string>(Status404NotFound, "text/plain")]
|
|
||||||
public async Task<Results<Ok<MangaConnectorId>, NotFound<string>>> GetChapterMangaConnectorId (string MangaConnectorIdId)
|
|
||||||
{
|
|
||||||
if (await context.MangaConnectorToManga.FirstOrDefaultAsync(c => c.Key == MangaConnectorIdId, HttpContext.RequestAborted) is not { } mcIdChapter)
|
|
||||||
return TypedResults.NotFound(nameof(MangaConnectorIdId));
|
|
||||||
|
|
||||||
MangaConnectorId result = new(mcIdChapter.Key, mcIdChapter.MangaConnectorName, mcIdChapter.ObjId, mcIdChapter.WebsiteUrl, mcIdChapter.UseForDownload);
|
|
||||||
|
|
||||||
return TypedResults.Ok(result);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -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.Schema.MangaContext;
|
using API.Schema.MangaContext;
|
||||||
|
using API.Schema.MangaContext.MangaConnectors;
|
||||||
using Asp.Versioning;
|
using Asp.Versioning;
|
||||||
using Microsoft.AspNetCore.Http.HttpResults;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using static Microsoft.AspNetCore.Http.StatusCodes;
|
using static Microsoft.AspNetCore.Http.StatusCodes;
|
||||||
using Manga = API.Schema.MangaContext.Manga;
|
|
||||||
|
|
||||||
// ReSharper disable InconsistentNaming
|
// ReSharper disable InconsistentNaming
|
||||||
|
|
||||||
namespace API.Controllers;
|
namespace API.Controllers;
|
||||||
@@ -16,64 +13,58 @@ namespace API.Controllers;
|
|||||||
public class SearchController(MangaContext context) : Controller
|
public class SearchController(MangaContext context) : Controller
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <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>
|
/// </summary>
|
||||||
/// <param name="MangaConnectorName"><see cref="MangaConnector"/>.Name</param>
|
/// <param name="MangaConnectorName"><see cref="MangaConnector"/>.Name</param>
|
||||||
/// <param name="Query">searchTerm</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="404"><see cref="MangaConnector"/> with Name not found</response>
|
||||||
/// <response code="412"><see cref="MangaConnector"/> with Name is disabled</response>
|
/// <response code="412"><see cref="MangaConnector"/> with Name is disabled</response>
|
||||||
[HttpGet("{MangaConnectorName}/{Query}")]
|
[HttpGet("{MangaConnectorName}/{Query}")]
|
||||||
[ProducesResponseType<List<MinimalManga>>(Status200OK, "application/json")]
|
[ProducesResponseType<Manga[]>(Status200OK, "application/json")]
|
||||||
[ProducesResponseType<string>(Status404NotFound, "text/plain")]
|
[ProducesResponseType(Status404NotFound)]
|
||||||
[ProducesResponseType(Status406NotAcceptable)]
|
[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)
|
if(context.MangaConnectors.Find(MangaConnectorName) is not { } connector)
|
||||||
return TypedResults.NotFound(nameof(MangaConnectorName));
|
return NotFound();
|
||||||
if (connector.Enabled is false)
|
if (connector.Enabled is false)
|
||||||
return TypedResults.StatusCode(Status412PreconditionFailed);
|
return StatusCode(Status412PreconditionFailed);
|
||||||
|
|
||||||
(Manga manga, MangaConnectorId<Manga> id)[] mangas = connector.SearchManga(Query);
|
(Manga, MangaConnectorId<Manga>)[] mangas = connector.SearchManga(Query);
|
||||||
|
List<Manga> retMangas = new();
|
||||||
IEnumerable<(Manga manga, MangaConnectorId<Manga> id)> addedManga = mangas.Where(kv => context.AddMangaToContext(kv, HttpContext.RequestAborted).GetAwaiter().GetResult());
|
foreach ((Manga manga, MangaConnectorId<Manga> mcId) manga in mangas)
|
||||||
|
|
||||||
IEnumerable<MinimalManga> result = addedManga.Select(manga => manga.manga).Select(m =>
|
|
||||||
{
|
{
|
||||||
IEnumerable<MangaConnectorId> ids = m.MangaConnectorIds.Select(id =>
|
if(Tranga.AddMangaToContext(manga, context, out Manga? add))
|
||||||
new MangaConnectorId(id.Key, id.MangaConnectorName, id.ObjId, id.WebsiteUrl, id.UseForDownload));
|
retMangas.Add(add);
|
||||||
return new MinimalManga(m.Key, m.Name, m.Description, m.ReleaseStatus, ids);
|
}
|
||||||
});
|
|
||||||
|
|
||||||
return TypedResults.Ok(result.ToList());
|
return Ok(retMangas.ToArray());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <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>
|
/// </summary>
|
||||||
/// <param name="url"></param>
|
/// <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="404"><see cref="Manga"/> not found</response>
|
||||||
/// <response code="500">Error during Database Operation</response>
|
/// <response code="500">Error during Database Operation</response>
|
||||||
[HttpPost("Url")]
|
[HttpPost("Url")]
|
||||||
[ProducesResponseType<MinimalManga>(Status200OK, "application/json")]
|
[ProducesResponseType<Manga>(Status200OK, "application/json")]
|
||||||
[ProducesResponseType<string>(Status404NotFound, "text/plain")]
|
[ProducesResponseType(Status404NotFound)]
|
||||||
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
|
[ProducesResponseType(Status500InternalServerError)]
|
||||||
public async Task<Results<Ok<MinimalManga>, NotFound<string>, InternalServerError<string>>> GetMangaFromUrl ([FromBody]string url)
|
public IActionResult GetMangaFromUrl([FromBody]string url)
|
||||||
{
|
{
|
||||||
if(Tranga.MangaConnectors.FirstOrDefault(c => c.Name.Equals("Global", StringComparison.InvariantCultureIgnoreCase)) is not { } connector)
|
if (context.MangaConnectors.Find("Global") 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)
|
if(connector.GetMangaFromUrl(url) is not { } manga)
|
||||||
return TypedResults.NotFound("Could not retrieve Manga");
|
return NotFound();
|
||||||
|
|
||||||
if(await context.AddMangaToContext(manga, HttpContext.RequestAborted) == false)
|
if(Tranga.AddMangaToContext(manga, context, out Manga? add) == false)
|
||||||
return TypedResults.InternalServerError("Could not add Manga to context");
|
return StatusCode(Status500InternalServerError);
|
||||||
|
|
||||||
IEnumerable<MangaConnectorId> ids = m.MangaConnectorIds.Select(id =>
|
return Ok(add);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -1,6 +1,5 @@
|
|||||||
using API.MangaDownloadClients;
|
using API.MangaDownloadClients;
|
||||||
using Asp.Versioning;
|
using Asp.Versioning;
|
||||||
using Microsoft.AspNetCore.Http.HttpResults;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using static Microsoft.AspNetCore.Http.StatusCodes;
|
using static Microsoft.AspNetCore.Http.StatusCodes;
|
||||||
// ReSharper disable InconsistentNaming
|
// ReSharper disable InconsistentNaming
|
||||||
@@ -18,9 +17,9 @@ public class SettingsController() : Controller
|
|||||||
/// <response code="200"></response>
|
/// <response code="200"></response>
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
[ProducesResponseType<TrangaSettings>(Status200OK, "application/json")]
|
[ProducesResponseType<TrangaSettings>(Status200OK, "application/json")]
|
||||||
public Ok<TrangaSettings> GetSettings()
|
public IActionResult GetSettings()
|
||||||
{
|
{
|
||||||
return TypedResults.Ok(Tranga.Settings);
|
return Ok(Tranga.Settings);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -29,9 +28,9 @@ public class SettingsController() : Controller
|
|||||||
/// <response code="200"></response>
|
/// <response code="200"></response>
|
||||||
[HttpGet("UserAgent")]
|
[HttpGet("UserAgent")]
|
||||||
[ProducesResponseType<string>(Status200OK, "text/plain")]
|
[ProducesResponseType<string>(Status200OK, "text/plain")]
|
||||||
public Ok<string> GetUserAgent()
|
public IActionResult GetUserAgent()
|
||||||
{
|
{
|
||||||
return TypedResults.Ok(Tranga.Settings.UserAgent);
|
return Ok(Tranga.Settings.UserAgent);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -40,11 +39,11 @@ public class SettingsController() : Controller
|
|||||||
/// <response code="200"></response>
|
/// <response code="200"></response>
|
||||||
[HttpPatch("UserAgent")]
|
[HttpPatch("UserAgent")]
|
||||||
[ProducesResponseType(Status200OK)]
|
[ProducesResponseType(Status200OK)]
|
||||||
public Ok SetUserAgent([FromBody]string userAgent)
|
public IActionResult SetUserAgent([FromBody]string userAgent)
|
||||||
{
|
{
|
||||||
//TODO Validate
|
//TODO Validate
|
||||||
Tranga.Settings.SetUserAgent(userAgent);
|
Tranga.Settings.SetUserAgent(userAgent);
|
||||||
return TypedResults.Ok();
|
return Ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -53,10 +52,10 @@ public class SettingsController() : Controller
|
|||||||
/// <response code="200"></response>
|
/// <response code="200"></response>
|
||||||
[HttpDelete("UserAgent")]
|
[HttpDelete("UserAgent")]
|
||||||
[ProducesResponseType(Status200OK)]
|
[ProducesResponseType(Status200OK)]
|
||||||
public Ok ResetUserAgent()
|
public IActionResult ResetUserAgent()
|
||||||
{
|
{
|
||||||
Tranga.Settings.SetUserAgent(TrangaSettings.DefaultUserAgent);
|
Tranga.Settings.SetUserAgent(TrangaSettings.DefaultUserAgent);
|
||||||
return TypedResults.Ok();
|
return Ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -65,9 +64,9 @@ public class SettingsController() : Controller
|
|||||||
/// <response code="200"></response>
|
/// <response code="200"></response>
|
||||||
[HttpGet("RequestLimits")]
|
[HttpGet("RequestLimits")]
|
||||||
[ProducesResponseType<Dictionary<RequestType,int>>(Status200OK, "application/json")]
|
[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>
|
/// <summary>
|
||||||
@@ -76,9 +75,9 @@ public class SettingsController() : Controller
|
|||||||
/// <remarks><h1>NOT IMPLEMENTED</h1></remarks>
|
/// <remarks><h1>NOT IMPLEMENTED</h1></remarks>
|
||||||
[HttpPatch("RequestLimits")]
|
[HttpPatch("RequestLimits")]
|
||||||
[ProducesResponseType(Status501NotImplemented)]
|
[ProducesResponseType(Status501NotImplemented)]
|
||||||
public StatusCodeHttpResult SetRequestLimits()
|
public IActionResult SetRequestLimits()
|
||||||
{
|
{
|
||||||
return TypedResults.StatusCode(Status501NotImplemented);
|
return StatusCode(501);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -91,12 +90,12 @@ public class SettingsController() : Controller
|
|||||||
[HttpPatch("RequestLimits/{RequestType}")]
|
[HttpPatch("RequestLimits/{RequestType}")]
|
||||||
[ProducesResponseType(Status200OK)]
|
[ProducesResponseType(Status200OK)]
|
||||||
[ProducesResponseType(Status400BadRequest)]
|
[ProducesResponseType(Status400BadRequest)]
|
||||||
public Results<Ok, BadRequest> SetRequestLimit(RequestType RequestType, [FromBody]int requestLimit)
|
public IActionResult SetRequestLimit(RequestType RequestType, [FromBody]int requestLimit)
|
||||||
{
|
{
|
||||||
if (requestLimit <= 0)
|
if (requestLimit <= 0)
|
||||||
return TypedResults.BadRequest();
|
return BadRequest();
|
||||||
Tranga.Settings.SetRequestLimit(RequestType, requestLimit);
|
Tranga.Settings.SetRequestLimit(RequestType, requestLimit);
|
||||||
return TypedResults.Ok();
|
return Ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -105,10 +104,10 @@ public class SettingsController() : Controller
|
|||||||
/// <response code="200"></response>
|
/// <response code="200"></response>
|
||||||
[HttpDelete("RequestLimits/{RequestType}")]
|
[HttpDelete("RequestLimits/{RequestType}")]
|
||||||
[ProducesResponseType<string>(Status200OK)]
|
[ProducesResponseType<string>(Status200OK)]
|
||||||
public Ok ResetRequestLimits(RequestType RequestType)
|
public IActionResult ResetRequestLimits(RequestType RequestType)
|
||||||
{
|
{
|
||||||
Tranga.Settings.SetRequestLimit(RequestType, TrangaSettings.DefaultRequestLimits[RequestType]);
|
Tranga.Settings.SetRequestLimit(RequestType, TrangaSettings.DefaultRequestLimits[RequestType]);
|
||||||
return TypedResults.Ok();
|
return Ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -117,10 +116,10 @@ public class SettingsController() : Controller
|
|||||||
/// <response code="200"></response>
|
/// <response code="200"></response>
|
||||||
[HttpDelete("RequestLimits")]
|
[HttpDelete("RequestLimits")]
|
||||||
[ProducesResponseType<string>(Status200OK)]
|
[ProducesResponseType<string>(Status200OK)]
|
||||||
public Ok ResetRequestLimits()
|
public IActionResult ResetRequestLimits()
|
||||||
{
|
{
|
||||||
Tranga.Settings.ResetRequestLimits();
|
Tranga.Settings.ResetRequestLimits();
|
||||||
return TypedResults.Ok();
|
return Ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -129,9 +128,9 @@ public class SettingsController() : Controller
|
|||||||
/// <response code="200">JPEG ImageCompression-level as Integer</response>
|
/// <response code="200">JPEG ImageCompression-level as Integer</response>
|
||||||
[HttpGet("ImageCompressionLevel")]
|
[HttpGet("ImageCompressionLevel")]
|
||||||
[ProducesResponseType<int>(Status200OK, "text/plain")]
|
[ProducesResponseType<int>(Status200OK, "text/plain")]
|
||||||
public Ok<int> GetImageCompression()
|
public IActionResult GetImageCompression()
|
||||||
{
|
{
|
||||||
return TypedResults.Ok(Tranga.Settings.ImageCompression);
|
return Ok(Tranga.Settings.ImageCompression);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -143,12 +142,12 @@ public class SettingsController() : Controller
|
|||||||
[HttpPatch("ImageCompressionLevel/{level}")]
|
[HttpPatch("ImageCompressionLevel/{level}")]
|
||||||
[ProducesResponseType(Status200OK)]
|
[ProducesResponseType(Status200OK)]
|
||||||
[ProducesResponseType(Status400BadRequest)]
|
[ProducesResponseType(Status400BadRequest)]
|
||||||
public Results<Ok, BadRequest> SetImageCompression(int level)
|
public IActionResult SetImageCompression(int level)
|
||||||
{
|
{
|
||||||
if (level < 1 || level > 100)
|
if (level < 1 || level > 100)
|
||||||
return TypedResults.BadRequest();
|
return BadRequest();
|
||||||
Tranga.Settings.UpdateImageCompression(level);
|
Tranga.Settings.UpdateImageCompression(level);
|
||||||
return TypedResults.Ok();
|
return Ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -157,9 +156,9 @@ public class SettingsController() : Controller
|
|||||||
/// <response code="200">True if enabled</response>
|
/// <response code="200">True if enabled</response>
|
||||||
[HttpGet("BWImages")]
|
[HttpGet("BWImages")]
|
||||||
[ProducesResponseType<bool>(Status200OK, "text/plain")]
|
[ProducesResponseType<bool>(Status200OK, "text/plain")]
|
||||||
public Ok<bool> GetBwImagesToggle()
|
public IActionResult GetBwImagesToggle()
|
||||||
{
|
{
|
||||||
return TypedResults.Ok(Tranga.Settings.BlackWhiteImages);
|
return Ok(Tranga.Settings.BlackWhiteImages);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -169,10 +168,10 @@ public class SettingsController() : Controller
|
|||||||
/// <response code="200"></response>
|
/// <response code="200"></response>
|
||||||
[HttpPatch("BWImages/{enabled}")]
|
[HttpPatch("BWImages/{enabled}")]
|
||||||
[ProducesResponseType(Status200OK)]
|
[ProducesResponseType(Status200OK)]
|
||||||
public Ok SetBwImagesToggle(bool enabled)
|
public IActionResult SetBwImagesToggle(bool enabled)
|
||||||
{
|
{
|
||||||
Tranga.Settings.SetBlackWhiteImageEnabled(enabled);
|
Tranga.Settings.SetBlackWhiteImageEnabled(enabled);
|
||||||
return TypedResults.Ok();
|
return Ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -195,9 +194,9 @@ public class SettingsController() : Controller
|
|||||||
/// <response code="200"></response>
|
/// <response code="200"></response>
|
||||||
[HttpGet("ChapterNamingScheme")]
|
[HttpGet("ChapterNamingScheme")]
|
||||||
[ProducesResponseType<string>(Status200OK, "text/plain")]
|
[ProducesResponseType<string>(Status200OK, "text/plain")]
|
||||||
public Ok<string> GetCustomNamingScheme()
|
public IActionResult GetCustomNamingScheme()
|
||||||
{
|
{
|
||||||
return TypedResults.Ok(Tranga.Settings.ChapterNamingScheme);
|
return Ok(Tranga.Settings.ChapterNamingScheme);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -218,12 +217,12 @@ public class SettingsController() : Controller
|
|||||||
/// <response code="200"></response>
|
/// <response code="200"></response>
|
||||||
[HttpPatch("ChapterNamingScheme")]
|
[HttpPatch("ChapterNamingScheme")]
|
||||||
[ProducesResponseType(Status200OK)]
|
[ProducesResponseType(Status200OK)]
|
||||||
public Ok SetCustomNamingScheme([FromBody]string namingScheme)
|
public IActionResult SetCustomNamingScheme([FromBody]string namingScheme)
|
||||||
{
|
{
|
||||||
//TODO Move old Chapters
|
//TODO Move old Chapters
|
||||||
Tranga.Settings.SetChapterNamingScheme(namingScheme);
|
Tranga.Settings.SetChapterNamingScheme(namingScheme);
|
||||||
|
|
||||||
return TypedResults.Ok();
|
return Ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -233,10 +232,10 @@ public class SettingsController() : Controller
|
|||||||
/// <response code="200"></response>
|
/// <response code="200"></response>
|
||||||
[HttpPost("FlareSolverr/Url")]
|
[HttpPost("FlareSolverr/Url")]
|
||||||
[ProducesResponseType(Status200OK)]
|
[ProducesResponseType(Status200OK)]
|
||||||
public Ok SetFlareSolverrUrl([FromBody]string flareSolverrUrl)
|
public IActionResult SetFlareSolverrUrl([FromBody]string flareSolverrUrl)
|
||||||
{
|
{
|
||||||
Tranga.Settings.SetFlareSolverrUrl(flareSolverrUrl);
|
Tranga.Settings.SetFlareSolverrUrl(flareSolverrUrl);
|
||||||
return TypedResults.Ok();
|
return Ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -245,10 +244,10 @@ public class SettingsController() : Controller
|
|||||||
/// <response code="200"></response>
|
/// <response code="200"></response>
|
||||||
[HttpDelete("FlareSolverr/Url")]
|
[HttpDelete("FlareSolverr/Url")]
|
||||||
[ProducesResponseType(Status200OK)]
|
[ProducesResponseType(Status200OK)]
|
||||||
public Ok ClearFlareSolverrUrl()
|
public IActionResult ClearFlareSolverrUrl()
|
||||||
{
|
{
|
||||||
Tranga.Settings.SetFlareSolverrUrl(string.Empty);
|
Tranga.Settings.SetFlareSolverrUrl(string.Empty);
|
||||||
return TypedResults.Ok();
|
return Ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -259,12 +258,12 @@ public class SettingsController() : Controller
|
|||||||
[HttpPost("FlareSolverr/Test")]
|
[HttpPost("FlareSolverr/Test")]
|
||||||
[ProducesResponseType(Status200OK)]
|
[ProducesResponseType(Status200OK)]
|
||||||
[ProducesResponseType(Status500InternalServerError)]
|
[ProducesResponseType(Status500InternalServerError)]
|
||||||
public Results<Ok, InternalServerError> TestFlareSolverrReachable()
|
public IActionResult TestFlareSolverrReachable()
|
||||||
{
|
{
|
||||||
const string knownProtectedUrl = "https://prowlarr.servarr.com/v1/ping";
|
const string knownProtectedUrl = "https://prowlarr.servarr.com/v1/ping";
|
||||||
FlareSolverrDownloadClient client = new();
|
FlareSolverrDownloadClient client = new();
|
||||||
RequestResult result = client.MakeRequestInternal(knownProtectedUrl);
|
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>
|
/// <summary>
|
||||||
@@ -273,9 +272,9 @@ public class SettingsController() : Controller
|
|||||||
/// <response code="200"></response>
|
/// <response code="200"></response>
|
||||||
[HttpGet("DownloadLanguage")]
|
[HttpGet("DownloadLanguage")]
|
||||||
[ProducesResponseType<string>(Status200OK, "text/plain")]
|
[ProducesResponseType<string>(Status200OK, "text/plain")]
|
||||||
public Ok<string> GetDownloadLanguage()
|
public IActionResult GetDownloadLanguage()
|
||||||
{
|
{
|
||||||
return TypedResults.Ok(Tranga.Settings.DownloadLanguage);
|
return Ok(Tranga.Settings.DownloadLanguage);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -284,10 +283,10 @@ public class SettingsController() : Controller
|
|||||||
/// <response code="200"></response>
|
/// <response code="200"></response>
|
||||||
[HttpPatch("DownloadLanguage/{Language}")]
|
[HttpPatch("DownloadLanguage/{Language}")]
|
||||||
[ProducesResponseType(Status200OK)]
|
[ProducesResponseType(Status200OK)]
|
||||||
public Ok SetDownloadLanguage(string Language)
|
public IActionResult SetDownloadLanguage(string Language)
|
||||||
{
|
{
|
||||||
//TODO Validation
|
//TODO Validation
|
||||||
Tranga.Settings.SetDownloadLanguage(Language);
|
Tranga.Settings.SetDownloadLanguage(Language);
|
||||||
return TypedResults.Ok();
|
return Ok();
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -1,7 +1,7 @@
|
|||||||
using API.Controllers.DTOs;
|
using API.APIEndpointRecords;
|
||||||
using API.Workers;
|
using API.Workers;
|
||||||
using Asp.Versioning;
|
using Asp.Versioning;
|
||||||
using Microsoft.AspNetCore.Http.HttpResults;
|
using log4net;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using static Microsoft.AspNetCore.Http.StatusCodes;
|
using static Microsoft.AspNetCore.Http.StatusCodes;
|
||||||
// ReSharper disable InconsistentNaming
|
// ReSharper disable InconsistentNaming
|
||||||
@@ -11,30 +11,29 @@ namespace API.Controllers;
|
|||||||
[ApiVersion(2)]
|
[ApiVersion(2)]
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Route("v{version:apiVersion}/[controller]")]
|
[Route("v{version:apiVersion}/[controller]")]
|
||||||
public class WorkerController : Controller
|
public class WorkerController() : Controller
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns all <see cref="BaseWorker"/>
|
/// Returns all <see cref="BaseWorker"/>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <response code="200"><see cref="Worker"/></response>
|
/// <response code="200"></response>
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
[ProducesResponseType<List<Worker>>(Status200OK, "application/json")]
|
[ProducesResponseType<BaseWorker[]>(Status200OK, "application/json")]
|
||||||
public Ok<List<Worker>> GetWorkers()
|
public IActionResult GetAllWorkers()
|
||||||
{
|
{
|
||||||
IEnumerable<Worker> result = Tranga.GetRunningWorkers().Select(w =>
|
return Ok(Tranga.AllWorkers.ToArray());
|
||||||
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>
|
/// <summary>
|
||||||
/// Returns all <see cref="BaseWorker"/>.Keys
|
/// Returns <see cref="BaseWorker"/> with requested <paramref name="WorkerIds"/>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="WorkerIds">Array of <see cref="BaseWorker"/>.Key</param>
|
||||||
/// <response code="200"></response>
|
/// <response code="200"></response>
|
||||||
[HttpGet("Keys")]
|
[HttpPost("WithIDs")]
|
||||||
[ProducesResponseType<string[]>(Status200OK, "application/json")]
|
[ProducesResponseType<BaseWorker[]>(Status200OK, "application/json")]
|
||||||
public Ok<List<string>> GetWorkerIds()
|
public IActionResult GetJobs([FromBody]string[] WorkerIds)
|
||||||
{
|
{
|
||||||
return TypedResults.Ok(Tranga.GetRunningWorkers().Select(w => w.Key).ToList());
|
return Ok(Tranga.AllWorkers.Where(worker => WorkerIds.Contains(worker.Key)).ToArray());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -43,12 +42,10 @@ public class WorkerController : Controller
|
|||||||
/// <param name="State">Requested <see cref="WorkerExecutionState"/></param>
|
/// <param name="State">Requested <see cref="WorkerExecutionState"/></param>
|
||||||
/// <response code="200"></response>
|
/// <response code="200"></response>
|
||||||
[HttpGet("State/{State}")]
|
[HttpGet("State/{State}")]
|
||||||
[ProducesResponseType<List<Worker>>(Status200OK, "application/json")]
|
[ProducesResponseType<BaseWorker[]>(Status200OK, "application/json")]
|
||||||
public Ok<List<Worker>> GetWorkersInState(WorkerExecutionState State)
|
public IActionResult GetJobsInState(WorkerExecutionState State)
|
||||||
{
|
{
|
||||||
IEnumerable<Worker> result = Tranga.GetRunningWorkers().Where(worker => worker.State == State).Select(w =>
|
return Ok(Tranga.AllWorkers.Where(worker => worker.State == State).ToArray());
|
||||||
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>
|
/// <summary>
|
||||||
@@ -58,16 +55,13 @@ public class WorkerController : Controller
|
|||||||
/// <response code="200"></response>
|
/// <response code="200"></response>
|
||||||
/// <response code="404"><see cref="BaseWorker"/> with <paramref name="WorkerId"/> could not be found</response>
|
/// <response code="404"><see cref="BaseWorker"/> with <paramref name="WorkerId"/> could not be found</response>
|
||||||
[HttpGet("{WorkerId}")]
|
[HttpGet("{WorkerId}")]
|
||||||
[ProducesResponseType<Worker>(Status200OK, "application/json")]
|
[ProducesResponseType<BaseWorker>(Status200OK, "application/json")]
|
||||||
[ProducesResponseType<string>(Status404NotFound, "text/plain")]
|
[ProducesResponseType(Status404NotFound)]
|
||||||
public Results<Ok<Worker>, NotFound<string>> GetWorker(string WorkerId)
|
public IActionResult GetJob(string WorkerId)
|
||||||
{
|
{
|
||||||
if(Tranga.GetRunningWorkers().FirstOrDefault(w => w.Key == WorkerId) is not { } w)
|
if(Tranga.AllWorkers.FirstOrDefault(w => w.Key == WorkerId) is not { } worker)
|
||||||
return TypedResults.NotFound(nameof(WorkerId));
|
return NotFound(nameof(WorkerId));
|
||||||
|
return Ok(worker);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -78,13 +72,40 @@ public class WorkerController : Controller
|
|||||||
/// <response code="404"><see cref="BaseWorker"/> with <paramref name="WorkerId"/> could not be found</response>
|
/// <response code="404"><see cref="BaseWorker"/> with <paramref name="WorkerId"/> could not be found</response>
|
||||||
[HttpDelete("{WorkerId}")]
|
[HttpDelete("{WorkerId}")]
|
||||||
[ProducesResponseType(Status200OK)]
|
[ProducesResponseType(Status200OK)]
|
||||||
[ProducesResponseType<string>(Status404NotFound, "text/plain")]
|
[ProducesResponseType(Status404NotFound)]
|
||||||
public Results<Ok, NotFound<string>> DeleteWorker(string WorkerId)
|
public IActionResult DeleteJob(string WorkerId)
|
||||||
{
|
{
|
||||||
if(Tranga.GetRunningWorkers().FirstOrDefault(w => w.Key == WorkerId) is not { } worker)
|
if(Tranga.AllWorkers.FirstOrDefault(w => w.Key == WorkerId) is not { } worker)
|
||||||
return TypedResults.NotFound(nameof(WorkerId));
|
return NotFound(nameof(WorkerId));
|
||||||
Tranga.StopWorker(worker);
|
Tranga.RemoveWorker(worker);
|
||||||
return TypedResults.Ok();
|
return Ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Modify <see cref="BaseWorker"/> with <paramref name="WorkerId"/>
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="WorkerId"><see cref="BaseWorker"/>.Key</param>
|
||||||
|
/// <param name="modifyWorkerRecord">Fields to modify, set to null to keep previous value</param>
|
||||||
|
/// <response code="202"></response>
|
||||||
|
/// <response code="400"></response>
|
||||||
|
/// <response code="404"><see cref="BaseWorker"/> with <paramref name="WorkerId"/> could not be found</response>
|
||||||
|
/// <response code="409"><see cref="BaseWorker"/> is not <see cref="IPeriodic"/>, can not modify <paramref name="modifyWorkerRecord.IntervalMs"/></response>
|
||||||
|
[HttpPatch("{WorkerId}")]
|
||||||
|
[ProducesResponseType<BaseWorker>(Status202Accepted, "application/json")]
|
||||||
|
[ProducesResponseType(Status400BadRequest)]
|
||||||
|
[ProducesResponseType(Status404NotFound)]
|
||||||
|
[ProducesResponseType<string>(Status409Conflict, "text/plain")]
|
||||||
|
public IActionResult ModifyJob(string WorkerId, [FromBody]ModifyWorkerRecord modifyWorkerRecord)
|
||||||
|
{
|
||||||
|
if(Tranga.AllWorkers.FirstOrDefault(w => w.Key == WorkerId) is not { } worker)
|
||||||
|
return NotFound(nameof(WorkerId));
|
||||||
|
|
||||||
|
if(modifyWorkerRecord.IntervalMs is not null && worker is not IPeriodic)
|
||||||
|
return Conflict("Can not modify Interval of non-Periodic worker");
|
||||||
|
else if(modifyWorkerRecord.IntervalMs is not null && worker is IPeriodic periodic)
|
||||||
|
periodic.Interval = TimeSpan.FromMilliseconds((long)modifyWorkerRecord.IntervalMs);
|
||||||
|
|
||||||
|
return Accepted(worker);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -96,18 +117,18 @@ public class WorkerController : Controller
|
|||||||
/// <response code="412"><see cref="BaseWorker"/> was already running</response>
|
/// <response code="412"><see cref="BaseWorker"/> was already running</response>
|
||||||
[HttpPost("{WorkerId}/Start")]
|
[HttpPost("{WorkerId}/Start")]
|
||||||
[ProducesResponseType(Status202Accepted)]
|
[ProducesResponseType(Status202Accepted)]
|
||||||
[ProducesResponseType<string>(Status404NotFound, "text/plain")]
|
[ProducesResponseType(Status404NotFound)]
|
||||||
[ProducesResponseType(Status412PreconditionFailed)]
|
[ProducesResponseType<string>(Status412PreconditionFailed, "text/plain")]
|
||||||
public Results<Ok, NotFound<string>, StatusCodeHttpResult> StartWorker(string WorkerId)
|
public IActionResult StartJob(string WorkerId)
|
||||||
{
|
{
|
||||||
if(Tranga.GetRunningWorkers().FirstOrDefault(w => w.Key == WorkerId) is not { } worker)
|
if(Tranga.AllWorkers.FirstOrDefault(w => w.Key == WorkerId) is not { } worker)
|
||||||
return TypedResults.NotFound(nameof(WorkerId));
|
return NotFound(nameof(WorkerId));
|
||||||
|
|
||||||
if (worker.State >= WorkerExecutionState.Waiting)
|
if (worker.State >= WorkerExecutionState.Waiting)
|
||||||
return TypedResults.StatusCode(Status412PreconditionFailed);
|
return StatusCode(Status412PreconditionFailed, "Already running");
|
||||||
|
|
||||||
Tranga.StartWorker(worker);
|
Tranga.MarkWorkerForStart(worker);
|
||||||
return TypedResults.Ok();
|
return Ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -116,20 +137,18 @@ public class WorkerController : Controller
|
|||||||
/// <param name="WorkerId"><see cref="BaseWorker"/>.Key</param>
|
/// <param name="WorkerId"><see cref="BaseWorker"/>.Key</param>
|
||||||
/// <response code="200"></response>
|
/// <response code="200"></response>
|
||||||
/// <response code="404"><see cref="BaseWorker"/> with <paramref name="WorkerId"/> could not be found</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")]
|
[HttpPost("{WorkerId}/Stop")]
|
||||||
[ProducesResponseType(Status202Accepted)]
|
[ProducesResponseType(Status501NotImplemented)]
|
||||||
[ProducesResponseType<string>(Status404NotFound, "text/plain")]
|
public IActionResult StopJob(string WorkerId)
|
||||||
[ProducesResponseType(Status412PreconditionFailed)]
|
|
||||||
public Results<Ok, NotFound<string>, StatusCodeHttpResult> StopWorker(string WorkerId)
|
|
||||||
{
|
{
|
||||||
if(Tranga.GetRunningWorkers().FirstOrDefault(w => w.Key == WorkerId) is not { } worker)
|
if(Tranga.AllWorkers.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)
|
if(worker.State is < WorkerExecutionState.Running or >= WorkerExecutionState.Completed)
|
||||||
return TypedResults.StatusCode(Status412PreconditionFailed);
|
return StatusCode(Status208AlreadyReported, "Not running");
|
||||||
|
|
||||||
Tranga.StopWorker(worker);
|
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>
|
|
@@ -1,91 +0,0 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
|
||||||
using System.ComponentModel.DataAnnotations.Schema;
|
|
||||||
using System.Text.RegularExpressions;
|
|
||||||
using API.MangaDownloadClients;
|
|
||||||
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!;
|
|
||||||
|
|
||||||
[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;
|
|
||||||
public bool Enabled { get; internal set; } = true;
|
|
||||||
|
|
||||||
public abstract (Manga, MangaConnectorId<Manga>)[] SearchManga(string mangaSearchName);
|
|
||||||
|
|
||||||
public abstract (Manga, MangaConnectorId<Manga>)? GetMangaFromUrl(string url);
|
|
||||||
|
|
||||||
public abstract (Manga, MangaConnectorId<Manga>)? GetMangaFromId(string mangaIdOnSite);
|
|
||||||
|
|
||||||
public abstract (Chapter, MangaConnectorId<Chapter>)[] GetChapters(MangaConnectorId<Manga> mangaId,
|
|
||||||
string? language = null);
|
|
||||||
|
|
||||||
internal abstract string[] GetChapterImageUrls(MangaConnectorId<Chapter> chapterId);
|
|
||||||
|
|
||||||
public bool UrlMatchesConnector(string url) => BaseUris.Any(baseUri => Regex.IsMatch(url, "https?://" + baseUri + "/.*"));
|
|
||||||
|
|
||||||
internal string? SaveCoverImageToCache(MangaConnectorId<Manga> mangaId, int retries = 3)
|
|
||||||
{
|
|
||||||
if(retries < 0)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
Regex urlRex = new (@"https?:\/\/((?:[a-zA-Z0-9-]+\.)+[a-zA-Z0-9]+)\/(?:.+\/)*(.+\.([a-zA-Z]+))");
|
|
||||||
//https?:\/\/[a-zA-Z0-9-]+\.([a-zA-Z0-9-]+\.[a-zA-Z0-9]+)\/(?:.+\/)*(.+\.([a-zA-Z]+)) for only second level domains
|
|
||||||
Match match = urlRex.Match(mangaId.Obj.CoverUrl);
|
|
||||||
string filename = $"{match.Groups[1].Value}-{mangaId.ObjId}.{mangaId.MangaConnectorName}.{match.Groups[3].Value}";
|
|
||||||
string saveImagePath = Path.Join(TrangaSettings.coverImageCacheOriginal, filename);
|
|
||||||
|
|
||||||
if (File.Exists(saveImagePath))
|
|
||||||
return filename;
|
|
||||||
|
|
||||||
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 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;
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,507 +0,0 @@
|
|||||||
using System.Text.RegularExpressions;
|
|
||||||
using API.MangaDownloadClients;
|
|
||||||
using API.Schema.MangaContext;
|
|
||||||
using HtmlAgilityPack;
|
|
||||||
// ReSharper disable StringLiteralTypo
|
|
||||||
|
|
||||||
namespace API.MangaConnectors;
|
|
||||||
|
|
||||||
public sealed class Mangaworld : MangaConnector
|
|
||||||
{
|
|
||||||
public Mangaworld() : base(
|
|
||||||
"Mangaworld",
|
|
||||||
["it"],
|
|
||||||
[
|
|
||||||
"mangaworld.cx","www.mangaworld.cx",
|
|
||||||
"mangaworld.bz","www.mangaworld.bz",
|
|
||||||
"mangaworld.fun","www.mangaworld.fun",
|
|
||||||
"mangaworld.ac","www.mangaworld.ac"
|
|
||||||
],
|
|
||||||
"https://www.mangaworld.cx/public/assets/seo/favicon-96x96.png?v=3"
|
|
||||||
)
|
|
||||||
{
|
|
||||||
downloadClient = new HttpDownloadClient();
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================ SEARCH ============================
|
|
||||||
public override (Manga, MangaConnectorId<Manga>)[] SearchManga(string mangaSearchName)
|
|
||||||
{
|
|
||||||
Uri baseUri = new ("https://www.mangaworld.cx/");
|
|
||||||
Uri searchUrl = new (baseUri, "archive?keyword=" + Uri.EscapeDataString(mangaSearchName));
|
|
||||||
|
|
||||||
RequestResult res = downloadClient.MakeRequest(searchUrl.ToString(), RequestType.Default);
|
|
||||||
if ((int)res.statusCode < 200 || (int)res.statusCode >= 300)
|
|
||||||
return [];
|
|
||||||
|
|
||||||
using StreamReader sr = new (res.result);
|
|
||||||
string html = sr.ReadToEnd();
|
|
||||||
|
|
||||||
HtmlDocument doc = new ();
|
|
||||||
doc.LoadHtml(html);
|
|
||||||
|
|
||||||
HtmlNodeCollection? anchors = doc.DocumentNode.SelectNodes("//a[@href and contains(@href,'/manga/')]");
|
|
||||||
// ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract Apparently it does return null. Ask AgilityPack why the return type isnt marked as such...
|
|
||||||
if (anchors is null || anchors.Count < 1)
|
|
||||||
return [];
|
|
||||||
|
|
||||||
List<(Manga, MangaConnectorId<Manga>)> list = [];
|
|
||||||
|
|
||||||
foreach (HtmlNode a in anchors)
|
|
||||||
{
|
|
||||||
string href = a.GetAttributeValue("href", "");
|
|
||||||
if (string.IsNullOrEmpty(href))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
string canonical = new Uri(baseUri, href).ToString();
|
|
||||||
|
|
||||||
(Manga, MangaConnectorId<Manga>)? manga = GetMangaFromUrl(canonical);
|
|
||||||
if(manga is null)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
list.Add(((Manga, MangaConnectorId<Manga>))manga);
|
|
||||||
}
|
|
||||||
|
|
||||||
return list.ToArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
// ======================== URL → Manga ===========================
|
|
||||||
public override (Manga, MangaConnectorId<Manga>)? GetMangaFromUrl(string url)
|
|
||||||
{
|
|
||||||
Match m = SeriesUrl.Match(url);
|
|
||||||
if (!m.Success)
|
|
||||||
return null;
|
|
||||||
return GetMangaFromId($"{m.Groups["id"].Value}/{m.Groups["slug"].Value}");
|
|
||||||
}
|
|
||||||
|
|
||||||
// ======================== ID → Manga ============================
|
|
||||||
public override (Manga, MangaConnectorId<Manga>)? GetMangaFromId(string mangaIdOnSite)
|
|
||||||
{
|
|
||||||
string[] parts = mangaIdOnSite.Split('/', 2);
|
|
||||||
if (parts.Length != 2)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
string id = parts[0];
|
|
||||||
string slug = parts[1];
|
|
||||||
|
|
||||||
string url = $"https://www.mangaworld.cx/manga/{id}/{slug}/";
|
|
||||||
RequestResult res = downloadClient.MakeRequest(url, RequestType.MangaInfo);
|
|
||||||
if ((int)res.statusCode < 200 || (int)res.statusCode >= 300)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
using StreamReader sr = new (res.result);
|
|
||||||
string html = sr.ReadToEnd();
|
|
||||||
|
|
||||||
HtmlDocument doc = new ();
|
|
||||||
doc.LoadHtml(html);
|
|
||||||
|
|
||||||
string title =
|
|
||||||
doc.DocumentNode.SelectSingleNode("//meta[@property='og:title']")?.GetAttributeValue("content", null)
|
|
||||||
?? doc.DocumentNode.SelectSingleNode("//h1")?.InnerText?.Trim()
|
|
||||||
?? slug.Replace('-', ' ');
|
|
||||||
|
|
||||||
title = CleanTitleSuffix(title);
|
|
||||||
|
|
||||||
string cover =
|
|
||||||
ExtractOgImage(html, new Uri(url))
|
|
||||||
?? doc.DocumentNode.SelectSingleNode("//div[contains(@class,'cover') or contains(@class,'poster')]//img[@src or @data-src]")?.GetAttributeValue("data-src", null)
|
|
||||||
?? doc.DocumentNode.SelectSingleNode("//div[contains(@class,'cover') or contains(@class,'poster')]//img[@src or @data-src]")?.GetAttributeValue("src", null)
|
|
||||||
?? string.Empty;
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(cover))
|
|
||||||
cover = MakeAbsoluteUrl(new Uri(url), cover);
|
|
||||||
|
|
||||||
string description =
|
|
||||||
doc.DocumentNode.SelectSingleNode("//meta[@name='description']")?.GetAttributeValue("content", null)
|
|
||||||
?? HtmlEntity.DeEntitize(
|
|
||||||
doc.DocumentNode.SelectSingleNode("//div[contains(@class,'description') or contains(@class,'trama')]")
|
|
||||||
?.InnerText ?? string.Empty
|
|
||||||
).Trim();
|
|
||||||
|
|
||||||
// === STATO (scheda dettaglio) ===
|
|
||||||
MangaReleaseStatus status = MangaReleaseStatus.Unreleased;
|
|
||||||
string? detailRawStatus = ExtractItalianStatus(doc);
|
|
||||||
if (!string.IsNullOrWhiteSpace(detailRawStatus))
|
|
||||||
status = MapItalianStatus(detailRawStatus);
|
|
||||||
|
|
||||||
Manga m = new (
|
|
||||||
HtmlEntity.DeEntitize(title).Trim(),
|
|
||||||
description,
|
|
||||||
cover,
|
|
||||||
status,
|
|
||||||
[],
|
|
||||||
[],
|
|
||||||
[],
|
|
||||||
[],
|
|
||||||
originalLanguage: "it");
|
|
||||||
MangaConnectorId<Manga> mcId = new (m,
|
|
||||||
this,
|
|
||||||
$"{id}/{slug}",
|
|
||||||
$"https://www.mangaworld.cx/manga/{id}/{slug}/");
|
|
||||||
m.MangaConnectorIds.Add(mcId);
|
|
||||||
return (m, mcId);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ========================== CAPITOLI ============================
|
|
||||||
public override (Chapter, MangaConnectorId<Chapter>)[] GetChapters(MangaConnectorId<Manga> mangaId, string? language = null)
|
|
||||||
{
|
|
||||||
string[] parts = mangaId.IdOnConnectorSite.Split('/', 2);
|
|
||||||
if (parts.Length != 2)
|
|
||||||
return [];
|
|
||||||
|
|
||||||
string id = parts[0];
|
|
||||||
string slug = parts[1];
|
|
||||||
string seriesUrl = $"https://www.mangaworld.cx/manga/{id}/{slug}/";
|
|
||||||
|
|
||||||
string html = FetchHtmlWithFallback(seriesUrl, out Uri baseUri);
|
|
||||||
if (string.IsNullOrEmpty(html))
|
|
||||||
return [];
|
|
||||||
|
|
||||||
HtmlDocument doc = new ();
|
|
||||||
doc.LoadHtml(html);
|
|
||||||
|
|
||||||
List<(Chapter, MangaConnectorId<Chapter>)> chapters = ParseChaptersFromHtml(mangaId.Obj ,doc, baseUri);
|
|
||||||
|
|
||||||
// Ordinamento finale: Volume → Capitolo (numerico)
|
|
||||||
return chapters
|
|
||||||
.OrderBy(c => c.Item1, new Chapter.ChapterComparer())
|
|
||||||
.ToArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===================== IMMAGINI CAPITOLO =======================
|
|
||||||
private static readonly Regex ImagesArray = new(@"images\s*=\s*\[(?<arr>.*?)\]", RegexOptions.Singleline | RegexOptions.IgnoreCase);
|
|
||||||
private static readonly Regex UrlInQuotes = new("\"(https?[^\"\\]]+)\"");
|
|
||||||
internal override string[] GetChapterImageUrls(MangaConnectorId<Chapter> chapterId)
|
|
||||||
{
|
|
||||||
string url = EnsureListStyle(chapterId.WebsiteUrl ?? $"https://www.mangaworld.cx/manga/{chapterId.IdOnConnectorSite}");
|
|
||||||
|
|
||||||
RequestResult res = downloadClient.MakeRequest(url, RequestType.MangaInfo);
|
|
||||||
if ((int)res.statusCode < 200 || (int)res.statusCode >= 300)
|
|
||||||
return [];
|
|
||||||
|
|
||||||
using StreamReader sr = new (res.result);
|
|
||||||
string html = sr.ReadToEnd();
|
|
||||||
|
|
||||||
Uri baseUri = new (url);
|
|
||||||
|
|
||||||
HtmlDocument doc = new ();
|
|
||||||
doc.LoadHtml(html);
|
|
||||||
|
|
||||||
HtmlNodeCollection imageNodes = doc.DocumentNode.SelectNodes("//img[@data-src or @src or @srcset]") ?? new HtmlNodeCollection(null);
|
|
||||||
|
|
||||||
IEnumerable<string> fromDom = imageNodes
|
|
||||||
.SelectMany(i =>
|
|
||||||
{
|
|
||||||
var list = new List<string>();
|
|
||||||
string ds = i.GetAttributeValue("data-src", "");
|
|
||||||
string s = i.GetAttributeValue("src", "");
|
|
||||||
string ss = i.GetAttributeValue("srcset", "");
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(ds))
|
|
||||||
list.Add(ds);
|
|
||||||
if (!string.IsNullOrEmpty(s))
|
|
||||||
list.Add(s);
|
|
||||||
if (!string.IsNullOrEmpty(ss))
|
|
||||||
{
|
|
||||||
foreach (string part in ss.Split(','))
|
|
||||||
{
|
|
||||||
string p = part.Trim().Split(' ')[0];
|
|
||||||
if (!string.IsNullOrWhiteSpace(p))
|
|
||||||
list.Add(p);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return list;
|
|
||||||
})
|
|
||||||
.Select(x => MakeAbsoluteUrl(baseUri, x))
|
|
||||||
.Where(u =>
|
|
||||||
{
|
|
||||||
string z = u.ToLowerInvariant();
|
|
||||||
return z.StartsWith("http") && (z.Contains(".jpg") || z.Contains(".jpeg") || z.Contains(".png") || z.Contains(".webp"));
|
|
||||||
});
|
|
||||||
|
|
||||||
Match m = ImagesArray.Match(html);
|
|
||||||
IEnumerable<string> fromJs = [];
|
|
||||||
if (m.Success)
|
|
||||||
{
|
|
||||||
MatchCollection urls = UrlInQuotes.Matches(m.Groups["arr"].Value);
|
|
||||||
fromJs = urls.Select(mm => MakeAbsoluteUrl(baseUri, mm.Groups[1].Value));
|
|
||||||
}
|
|
||||||
|
|
||||||
List<string> final = new ();
|
|
||||||
HashSet<string> seen = new (StringComparer.OrdinalIgnoreCase);
|
|
||||||
foreach (string u in fromDom.Concat(fromJs))
|
|
||||||
if (seen.Add(u))
|
|
||||||
final.Add(u);
|
|
||||||
|
|
||||||
return final.ToArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================ PARSER CAPITOLI ===================
|
|
||||||
private static readonly Regex RexVolume = new(@"[Vv]olume\s+([0-9]+)", RegexOptions.Compiled);
|
|
||||||
private static readonly Regex RexChapter = new(@"(?:\b[Cc]apitolo|\b[Cc]h(?:apter)?)\s*([0-9]+(?:\.[0-9]+)?)", RegexOptions.Compiled);
|
|
||||||
private static readonly Regex RexChapterId = new(@"manga\/([0-9]+\/[a-z0-9\-]+\/read\/[a-z0-9]+)\/", RegexOptions.Compiled);
|
|
||||||
|
|
||||||
private List<(Chapter, MangaConnectorId<Chapter>)> ParseChaptersFromHtml(Manga manga, HtmlDocument document, Uri baseUri)
|
|
||||||
{
|
|
||||||
List<(Chapter, MangaConnectorId<Chapter>)> ret = new ();
|
|
||||||
|
|
||||||
// wrapper principale
|
|
||||||
HtmlNode? chaptersWrapper = document.DocumentNode.SelectSingleNode("//div[contains(@class,'chapters-wrapper')]");
|
|
||||||
// layout A: volumi raggruppati
|
|
||||||
HtmlNodeCollection? volumeElements = document.DocumentNode.SelectNodes("//div[contains(@class,'volume-element')]");
|
|
||||||
|
|
||||||
// ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
|
|
||||||
if (volumeElements is not null && volumeElements.Count > 0)
|
|
||||||
{
|
|
||||||
foreach (HtmlNode volNode in volumeElements)
|
|
||||||
{
|
|
||||||
// titolo volume, es. "<p>Volume 24</p>"
|
|
||||||
string volText = volNode.SelectSingleNode(".//div[contains(@class,'volume')]/p")?.InnerText ?? string.Empty;
|
|
||||||
|
|
||||||
int? volumeNumber = null;
|
|
||||||
Match vm = RexVolume.Match(volText);
|
|
||||||
if (vm.Success && int.TryParse(vm.Groups[1].Value, out int volParsed))
|
|
||||||
volumeNumber = volParsed;
|
|
||||||
|
|
||||||
// capitoli dentro il blocco volume
|
|
||||||
HtmlNodeCollection chapterNodes = volNode
|
|
||||||
.SelectSingleNode(".//div[contains(@class,'volume-chapters')]")
|
|
||||||
?.SelectNodes(".//div") ?? new HtmlNodeCollection(null);
|
|
||||||
|
|
||||||
foreach (HtmlNode chNode in chapterNodes)
|
|
||||||
{
|
|
||||||
HtmlNode? anchor = chNode.SelectSingleNode(".//a[@href]");
|
|
||||||
// ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
|
|
||||||
if (anchor is null)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
string spanText = anchor.SelectSingleNode(".//span")?.InnerText ?? anchor.InnerText ?? string.Empty;
|
|
||||||
|
|
||||||
Match cm = RexChapter.Match(spanText);
|
|
||||||
if (!cm.Success)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
string chapterNumber = NormalizeNumber(cm.Groups[1].Value);
|
|
||||||
string href = anchor.GetAttributeValue("href", "");
|
|
||||||
if (string.IsNullOrWhiteSpace(href))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
string rel = MakeAbsoluteUrl(baseUri, href);
|
|
||||||
string ensured = EnsureListStyle(EnsureReaderUrlHasPage(rel));
|
|
||||||
|
|
||||||
Match idMatch = RexChapterId.Match(ensured);
|
|
||||||
if(!idMatch.Success)
|
|
||||||
continue;
|
|
||||||
string id = idMatch.Groups[1].Value;
|
|
||||||
|
|
||||||
Chapter chapter = new (manga, chapterNumber, volumeNumber);
|
|
||||||
MangaConnectorId<Chapter> chId = new(chapter, this, id, ensured);
|
|
||||||
chapter.MangaConnectorIds.Add(chId);
|
|
||||||
|
|
||||||
// title:null per evitare duplicazioni nel filename
|
|
||||||
ret.Add((chapter, chId));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// layout B: lista piatta (niente blocchi volume) → v1: Volume 0
|
|
||||||
HtmlNodeCollection chapterNodes = chaptersWrapper?.SelectNodes(".//div[contains(@class,'chapter')]")
|
|
||||||
?? document.DocumentNode.SelectNodes("//div[contains(@class,'chapter')]")
|
|
||||||
?? new HtmlNodeCollection(null);
|
|
||||||
|
|
||||||
foreach (HtmlNode chNode in chapterNodes)
|
|
||||||
{
|
|
||||||
HtmlNode? anchor = chNode.SelectSingleNode(".//a[@href]") ?? chNode.SelectSingleNode(".//a");
|
|
||||||
if (anchor is null)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
string spanText = anchor.SelectSingleNode(".//span")?.InnerText ?? anchor.InnerText ?? string.Empty;
|
|
||||||
|
|
||||||
Match cm = RexChapter.Match(spanText);
|
|
||||||
if (!cm.Success)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
string chapterNumber = NormalizeNumber(cm.Groups[1].Value);
|
|
||||||
string href = anchor.GetAttributeValue("href", "");
|
|
||||||
if (string.IsNullOrWhiteSpace(href))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
string rel = MakeAbsoluteUrl(baseUri, href);
|
|
||||||
string ensured = EnsureListStyle(EnsureReaderUrlHasPage(rel));
|
|
||||||
|
|
||||||
Match idMatch = RexChapterId.Match(ensured);
|
|
||||||
if(!idMatch.Success)
|
|
||||||
continue;
|
|
||||||
string id = idMatch.Groups[1].Value;
|
|
||||||
|
|
||||||
// v1 behaviour: senza volumi → Volume 0
|
|
||||||
Chapter chapter = new (manga, chapterNumber, null);
|
|
||||||
MangaConnectorId<Chapter> chId = new(chapter, this, id, ensured);
|
|
||||||
|
|
||||||
ret.Add((chapter, chId));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================ HELPERS ===========================
|
|
||||||
private static readonly Regex SeriesUrl = new(@"https?://[^/]+/manga/(?<id>\d+)/(?<slug>[^/]+)/?", RegexOptions.IgnoreCase);
|
|
||||||
|
|
||||||
private string FetchHtmlWithFallback(string seriesUrl, out Uri baseUri)
|
|
||||||
{
|
|
||||||
baseUri = new (seriesUrl);
|
|
||||||
|
|
||||||
// 1) tenta client "Default"
|
|
||||||
RequestResult res = downloadClient.MakeRequest(seriesUrl, RequestType.Default);
|
|
||||||
if ((int)res.statusCode >= 200 && (int)res.statusCode < 300)
|
|
||||||
{
|
|
||||||
using StreamReader sr = new (res.result);
|
|
||||||
string html = sr.ReadToEnd();
|
|
||||||
if (!LooksLikeChallenge(html))
|
|
||||||
return html;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2) fallback: client “MangaInfo” (proxy/Flare se configurato)
|
|
||||||
RequestResult res2 = downloadClient.MakeRequest(seriesUrl, RequestType.MangaInfo);
|
|
||||||
if ((int)res2.statusCode >= 200 && (int)res2.statusCode < 300)
|
|
||||||
{
|
|
||||||
using StreamReader sr2 = new StreamReader(res2.result);
|
|
||||||
return sr2.ReadToEnd();
|
|
||||||
}
|
|
||||||
|
|
||||||
return string.Empty;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool LooksLikeChallenge(string html)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(html)) return true;
|
|
||||||
string h = html.ToLowerInvariant();
|
|
||||||
return h.Contains("cf-challenge") ||
|
|
||||||
h.Contains("cf-browser-verification") ||
|
|
||||||
h.Contains("just a moment") ||
|
|
||||||
h.Contains("verify you are human") ||
|
|
||||||
h.Contains("captcha");
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string EnsureReaderUrlHasPage(string url)
|
|
||||||
{
|
|
||||||
Match m = Regex.Match(url, @"(/read/[0-9a-fA-F]{16,64})(/(\d+))?", RegexOptions.IgnoreCase);
|
|
||||||
if (m.Success && string.IsNullOrEmpty(m.Groups[2].Value))
|
|
||||||
{
|
|
||||||
int qIdx = url.IndexOf('?', StringComparison.Ordinal);
|
|
||||||
if (qIdx >= 0)
|
|
||||||
url = url.Insert(qIdx, "/1");
|
|
||||||
else
|
|
||||||
url = url.TrimEnd('/') + "/1";
|
|
||||||
}
|
|
||||||
return url;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string EnsureListStyle(string url)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(url))
|
|
||||||
return url;
|
|
||||||
if (url.Contains("style=list", StringComparison.OrdinalIgnoreCase))
|
|
||||||
return url;
|
|
||||||
return url.Contains('?') ? (url + "&style=list") : (url + "?style=list");
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string NormalizeNumber(string s)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(s))
|
|
||||||
return "0";
|
|
||||||
s = s.Trim();
|
|
||||||
Match m = Regex.Match(s, @"^\s*0*(\d+)(?:\.(\d+))?\s*$");
|
|
||||||
if (!m.Success)
|
|
||||||
return s;
|
|
||||||
string intPart = m.Groups[1].Value.TrimStart('0');
|
|
||||||
if (intPart.Length == 0)
|
|
||||||
intPart = "0";
|
|
||||||
string frac = m.Groups[2].Success
|
|
||||||
? "." + m.Groups[2].Value
|
|
||||||
: "";
|
|
||||||
return intPart + frac;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string MakeAbsoluteUrl(Uri baseUri, string s)
|
|
||||||
{
|
|
||||||
s = s.Trim();
|
|
||||||
if (s.StartsWith("//"))
|
|
||||||
return "https:" + s;
|
|
||||||
if (s.StartsWith("http://", StringComparison.OrdinalIgnoreCase) ||
|
|
||||||
s.StartsWith("https://", StringComparison.OrdinalIgnoreCase))
|
|
||||||
return s;
|
|
||||||
if (s.StartsWith("/"))
|
|
||||||
return new Uri(baseUri, s).ToString();
|
|
||||||
return new Uri(baseUri, s).ToString();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string? ExtractOgImage(string html, Uri baseUri)
|
|
||||||
{
|
|
||||||
HtmlDocument doc = new ();
|
|
||||||
doc.LoadHtml(html);
|
|
||||||
string? og = doc.DocumentNode.SelectSingleNode("//meta[@property='og:image']")?.GetAttributeValue("content", null);
|
|
||||||
return string.IsNullOrWhiteSpace(og) ? null : MakeAbsoluteUrl(baseUri, og!);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===================== TITLE CLEANUP (suffisso MW) ==============
|
|
||||||
private static readonly Regex MwSuffix = new(@"\s*(Scan\s\w+\s-\sMangaWorld)$", RegexOptions.IgnoreCase);
|
|
||||||
|
|
||||||
private static string CleanTitleSuffix(string? t)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(t))
|
|
||||||
return t ?? string.Empty;
|
|
||||||
return MwSuffix.Replace(t, "").Trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===================== STATO (estrazione + mapping) =============
|
|
||||||
private static string? ExtractItalianStatus(HtmlDocument doc)
|
|
||||||
{
|
|
||||||
// 1) Percorso più comune: "Stato: <valore>"
|
|
||||||
HtmlNode? node = doc.DocumentNode.SelectSingleNode("//span[normalize-space(text())='Stato:']/following-sibling::*[1]")
|
|
||||||
?? doc.DocumentNode.SelectSingleNode("//span[contains(translate(., 'STATO', 'stato'), 'stato')]/following-sibling::*[1]");
|
|
||||||
string? val = node?.InnerText?.Trim();
|
|
||||||
if (!string.IsNullOrWhiteSpace(val)) return HtmlEntity.DeEntitize(val);
|
|
||||||
|
|
||||||
// 2) Blocchi info vari (tollerante a cambi DOM)
|
|
||||||
HtmlNodeCollection? blocks = doc.DocumentNode.SelectNodes("//*[contains(@class,'info') or contains(@class,'details') or contains(@class,'meta') or contains(@class,'attributes') or contains(@class,'list-group')]");
|
|
||||||
// ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
|
|
||||||
if (blocks is not null)
|
|
||||||
{
|
|
||||||
foreach (HtmlNode block in blocks)
|
|
||||||
{
|
|
||||||
HtmlNodeCollection labels = block.SelectNodes(".//dt|.//li|.//div|.//span|.//strong") ?? new HtmlNodeCollection(null);
|
|
||||||
foreach (HtmlNode label in labels)
|
|
||||||
{
|
|
||||||
string? t = label.InnerText?.Trim()?.ToLowerInvariant();
|
|
||||||
if (string.IsNullOrEmpty(t))
|
|
||||||
continue;
|
|
||||||
if (t != "stato" && t != "stato:" && !t.Contains("stato"))
|
|
||||||
continue;
|
|
||||||
string? vv = label.SelectSingleNode("./following-sibling::*[1]")?.InnerText?.Trim()
|
|
||||||
?? label.ParentNode?.SelectSingleNode(".//a|.//span|.//strong")?.InnerText?.Trim();
|
|
||||||
if (!string.IsNullOrWhiteSpace(vv))
|
|
||||||
return HtmlEntity.DeEntitize(vv);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3) Fallback testuale grezzo
|
|
||||||
string body = doc.DocumentNode.InnerText;
|
|
||||||
Match m = Regex.Match(body, @"Stato\s*:\s*([A-Za-zÀ-ÿ\s\-]+)", RegexOptions.IgnoreCase);
|
|
||||||
return m.Success
|
|
||||||
? m.Groups[1].Value.Trim()
|
|
||||||
: null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static MangaReleaseStatus MapItalianStatus(string s) => s.Trim().ToLowerInvariant() switch
|
|
||||||
{
|
|
||||||
"in corso" or "ongoing" or "attivo" => MangaReleaseStatus.Continuing,
|
|
||||||
"completo" or "concluso" or "finito" or "terminato" or "completed" => MangaReleaseStatus.Completed,
|
|
||||||
"in pausa" or "pausa" or "hiatus" or "sospeso" => MangaReleaseStatus.OnHiatus,
|
|
||||||
"droppato" or "cancellato" or "abbandonato" or "cancelled" or "interrotto" => MangaReleaseStatus.Cancelled,
|
|
||||||
_ => MangaReleaseStatus.Unreleased
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -1,12 +1,11 @@
|
|||||||
using System.Collections.Concurrent;
|
using System.Net;
|
||||||
using System.Net;
|
|
||||||
using log4net;
|
using log4net;
|
||||||
|
|
||||||
namespace API.MangaDownloadClients;
|
namespace API.MangaDownloadClients;
|
||||||
|
|
||||||
public abstract class DownloadClient
|
public abstract class DownloadClient
|
||||||
{
|
{
|
||||||
private static readonly ConcurrentDictionary<RequestType, DateTime> LastExecutedRateLimit = new();
|
private static readonly Dictionary<RequestType, DateTime> LastExecutedRateLimit = new();
|
||||||
protected ILog Log { get; init; }
|
protected ILog Log { get; init; }
|
||||||
|
|
||||||
protected DownloadClient()
|
protected DownloadClient()
|
||||||
@@ -14,22 +13,18 @@ public abstract class DownloadClient
|
|||||||
this.Log = LogManager.GetLogger(GetType());
|
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)
|
public RequestResult MakeRequest(string url, RequestType requestType, string? referrer = null, string? clickButton = null)
|
||||||
{
|
{
|
||||||
Log.Debug($"Requesting {requestType} {url}");
|
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))
|
if (!Tranga.Settings.RequestLimits.ContainsKey(requestType))
|
||||||
requestType = RequestType.Default;
|
{
|
||||||
|
return new RequestResult(HttpStatusCode.NotAcceptable, null, Stream.Null);
|
||||||
|
}
|
||||||
|
|
||||||
int rateLimit = Tranga.Settings.RequestLimits[requestType];
|
int rateLimit = Tranga.Settings.UserAgent == TrangaSettings.DefaultUserAgent
|
||||||
// TODO this probably needs a better check whether the useragent matches...
|
? TrangaSettings.DefaultRequestLimits[requestType]
|
||||||
// If the UserAgent is the default one, do not exceed the default request-limits.
|
: Tranga.Settings.RequestLimits[requestType];
|
||||||
if (Tranga.Settings.UserAgent == TrangaSettings.DefaultUserAgent && rateLimit > TrangaSettings.DefaultRequestLimits[requestType])
|
|
||||||
rateLimit = TrangaSettings.DefaultRequestLimits[requestType];
|
|
||||||
|
|
||||||
// Apply the delay
|
|
||||||
TimeSpan timeBetweenRequests = TimeSpan.FromMinutes(1).Divide(rateLimit);
|
TimeSpan timeBetweenRequests = TimeSpan.FromMinutes(1).Divide(rateLimit);
|
||||||
DateTime now = DateTime.Now;
|
DateTime now = DateTime.Now;
|
||||||
LastExecutedRateLimit.TryAdd(requestType, now.Subtract(timeBetweenRequests));
|
LastExecutedRateLimit.TryAdd(requestType, now.Subtract(timeBetweenRequests));
|
||||||
@@ -38,12 +33,11 @@ public abstract class DownloadClient
|
|||||||
Log.Debug($"Request limit {requestType} {rateLimit}/Minute timeBetweenRequests: {timeBetweenRequests:ss'.'fffff} Timeout: {rateLimitTimeout:ss'.'fffff}");
|
Log.Debug($"Request limit {requestType} {rateLimit}/Minute timeBetweenRequests: {timeBetweenRequests:ss'.'fffff} Timeout: {rateLimitTimeout:ss'.'fffff}");
|
||||||
|
|
||||||
if (rateLimitTimeout > TimeSpan.Zero)
|
if (rateLimitTimeout > TimeSpan.Zero)
|
||||||
|
{
|
||||||
Thread.Sleep(rateLimitTimeout);
|
Thread.Sleep(rateLimitTimeout);
|
||||||
|
}
|
||||||
|
|
||||||
// Make the request
|
|
||||||
RequestResult result = MakeRequestInternal(url, referrer, clickButton);
|
RequestResult result = MakeRequestInternal(url, referrer, clickButton);
|
||||||
|
|
||||||
// Update the time the last request was made
|
|
||||||
LastExecutedRateLimit[requestType] = DateTime.UtcNow;
|
LastExecutedRateLimit[requestType] = DateTime.UtcNow;
|
||||||
Log.Debug($"Result {url}: {result}");
|
Log.Debug($"Result {url}: {result}");
|
||||||
return result;
|
return result;
|
||||||
|
@@ -10,6 +10,8 @@ namespace API.MangaDownloadClients;
|
|||||||
|
|
||||||
public class FlareSolverrDownloadClient : DownloadClient
|
public class FlareSolverrDownloadClient : DownloadClient
|
||||||
{
|
{
|
||||||
|
|
||||||
|
|
||||||
internal override RequestResult MakeRequestInternal(string url, string? referrer = null, string? clickButton = null)
|
internal override RequestResult MakeRequestInternal(string url, string? referrer = null, string? clickButton = null)
|
||||||
{
|
{
|
||||||
if (clickButton is not null)
|
if (clickButton is not null)
|
||||||
|
@@ -5,7 +5,6 @@ namespace API.MangaDownloadClients;
|
|||||||
|
|
||||||
internal class HttpDownloadClient : DownloadClient
|
internal class HttpDownloadClient : DownloadClient
|
||||||
{
|
{
|
||||||
private static readonly FlareSolverrDownloadClient FlareSolverrDownloadClient = new();
|
|
||||||
internal override RequestResult MakeRequestInternal(string url, string? referrer = null, string? clickButton = null)
|
internal override RequestResult MakeRequestInternal(string url, string? referrer = null, string? clickButton = null)
|
||||||
{
|
{
|
||||||
if (clickButton is not null)
|
if (clickButton is not null)
|
||||||
@@ -37,7 +36,7 @@ internal class HttpDownloadClient : DownloadClient
|
|||||||
(s.Product?.Name ?? "").Contains("cloudflare", StringComparison.InvariantCultureIgnoreCase)))
|
(s.Product?.Name ?? "").Contains("cloudflare", StringComparison.InvariantCultureIgnoreCase)))
|
||||||
{
|
{
|
||||||
Log.Debug("Retrying with FlareSolverr!");
|
Log.Debug("Retrying with FlareSolverr!");
|
||||||
return FlareSolverrDownloadClient.MakeRequestInternal(url, referrer, clickButton);
|
return new FlareSolverrDownloadClient().MakeRequestInternal(url, referrer, clickButton);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
@@ -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
|
#pragma warning disable 612, 618
|
||||||
modelBuilder
|
modelBuilder
|
||||||
.HasAnnotation("ProductVersion", "9.0.8")
|
.HasAnnotation("ProductVersion", "9.0.5")
|
||||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||||
|
|
||||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||||
@@ -24,8 +24,7 @@ namespace API.Migrations.Library
|
|||||||
modelBuilder.Entity("API.Schema.LibraryContext.LibraryConnectors.LibraryConnector", b =>
|
modelBuilder.Entity("API.Schema.LibraryContext.LibraryConnectors.LibraryConnector", b =>
|
||||||
{
|
{
|
||||||
b.Property<string>("Key")
|
b.Property<string>("Key")
|
||||||
.HasMaxLength(64)
|
.HasColumnType("text");
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.Property<string>("Auth")
|
b.Property<string>("Auth")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
|
@@ -11,7 +11,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
|||||||
namespace API.Migrations.Manga
|
namespace API.Migrations.Manga
|
||||||
{
|
{
|
||||||
[DbContext(typeof(MangaContext))]
|
[DbContext(typeof(MangaContext))]
|
||||||
[Migration("20250722203315_Initial")]
|
[Migration("20250703192023_Initial")]
|
||||||
partial class Initial
|
partial class Initial
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
@@ -24,39 +24,6 @@ namespace API.Migrations.Manga
|
|||||||
|
|
||||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||||
|
|
||||||
modelBuilder.Entity("API.MangaConnectors.MangaConnector", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Name")
|
|
||||||
.HasMaxLength(32)
|
|
||||||
.HasColumnType("character varying(32)");
|
|
||||||
|
|
||||||
b.PrimitiveCollection<string[]>("BaseUris")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(256)
|
|
||||||
.HasColumnType("text[]");
|
|
||||||
|
|
||||||
b.Property<bool>("Enabled")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<string>("IconUrl")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(2048)
|
|
||||||
.HasColumnType("character varying(2048)");
|
|
||||||
|
|
||||||
b.PrimitiveCollection<string[]>("SupportedLanguages")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(8)
|
|
||||||
.HasColumnType("text[]");
|
|
||||||
|
|
||||||
b.HasKey("Name");
|
|
||||||
|
|
||||||
b.ToTable("MangaConnector");
|
|
||||||
|
|
||||||
b.HasDiscriminator<string>("Name").HasValue("MangaConnector");
|
|
||||||
|
|
||||||
b.UseTphMappingStrategy();
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.MangaContext.Author", b =>
|
modelBuilder.Entity("API.Schema.MangaContext.Author", b =>
|
||||||
{
|
{
|
||||||
b.Property<string>("Key")
|
b.Property<string>("Key")
|
||||||
@@ -210,6 +177,8 @@ namespace API.Migrations.Manga
|
|||||||
|
|
||||||
b.HasKey("Key");
|
b.HasKey("Key");
|
||||||
|
|
||||||
|
b.HasIndex("MangaConnectorName");
|
||||||
|
|
||||||
b.HasIndex("ObjId");
|
b.HasIndex("ObjId");
|
||||||
|
|
||||||
b.ToTable("MangaConnectorToChapter");
|
b.ToTable("MangaConnectorToChapter");
|
||||||
@@ -244,11 +213,46 @@ namespace API.Migrations.Manga
|
|||||||
|
|
||||||
b.HasKey("Key");
|
b.HasKey("Key");
|
||||||
|
|
||||||
|
b.HasIndex("MangaConnectorName");
|
||||||
|
|
||||||
b.HasIndex("ObjId");
|
b.HasIndex("ObjId");
|
||||||
|
|
||||||
b.ToTable("MangaConnectorToManga");
|
b.ToTable("MangaConnectorToManga");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Schema.MangaContext.MangaConnectors.MangaConnector", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasMaxLength(32)
|
||||||
|
.HasColumnType("character varying(32)");
|
||||||
|
|
||||||
|
b.PrimitiveCollection<string[]>("BaseUris")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("text[]");
|
||||||
|
|
||||||
|
b.Property<bool>("Enabled")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<string>("IconUrl")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(2048)
|
||||||
|
.HasColumnType("character varying(2048)");
|
||||||
|
|
||||||
|
b.PrimitiveCollection<string[]>("SupportedLanguages")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(8)
|
||||||
|
.HasColumnType("text[]");
|
||||||
|
|
||||||
|
b.HasKey("Name");
|
||||||
|
|
||||||
|
b.ToTable("MangaConnectors");
|
||||||
|
|
||||||
|
b.HasDiscriminator<string>("Name").HasValue("MangaConnector");
|
||||||
|
|
||||||
|
b.UseTphMappingStrategy();
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.MangaContext.MangaTag", b =>
|
modelBuilder.Entity("API.Schema.MangaContext.MangaTag", b =>
|
||||||
{
|
{
|
||||||
b.Property<string>("Tag")
|
b.Property<string>("Tag")
|
||||||
@@ -328,23 +332,23 @@ namespace API.Migrations.Manga
|
|||||||
b.ToTable("MangaTagToManga");
|
b.ToTable("MangaTagToManga");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("API.MangaConnectors.ComickIo", b =>
|
modelBuilder.Entity("API.Schema.MangaContext.MangaConnectors.ComickIo", b =>
|
||||||
{
|
{
|
||||||
b.HasBaseType("API.MangaConnectors.MangaConnector");
|
b.HasBaseType("API.Schema.MangaContext.MangaConnectors.MangaConnector");
|
||||||
|
|
||||||
b.HasDiscriminator().HasValue("ComickIo");
|
b.HasDiscriminator().HasValue("ComickIo");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("API.MangaConnectors.Global", b =>
|
modelBuilder.Entity("API.Schema.MangaContext.MangaConnectors.Global", b =>
|
||||||
{
|
{
|
||||||
b.HasBaseType("API.MangaConnectors.MangaConnector");
|
b.HasBaseType("API.Schema.MangaContext.MangaConnectors.MangaConnector");
|
||||||
|
|
||||||
b.HasDiscriminator().HasValue("Global");
|
b.HasDiscriminator().HasValue("Global");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("API.MangaConnectors.MangaDex", b =>
|
modelBuilder.Entity("API.Schema.MangaContext.MangaConnectors.MangaDex", b =>
|
||||||
{
|
{
|
||||||
b.HasBaseType("API.MangaConnectors.MangaConnector");
|
b.HasBaseType("API.Schema.MangaContext.MangaConnectors.MangaConnector");
|
||||||
|
|
||||||
b.HasDiscriminator().HasValue("MangaDex");
|
b.HasDiscriminator().HasValue("MangaDex");
|
||||||
});
|
});
|
||||||
@@ -441,23 +445,39 @@ namespace API.Migrations.Manga
|
|||||||
|
|
||||||
modelBuilder.Entity("API.Schema.MangaContext.MangaConnectorId<API.Schema.MangaContext.Chapter>", b =>
|
modelBuilder.Entity("API.Schema.MangaContext.MangaConnectorId<API.Schema.MangaContext.Chapter>", b =>
|
||||||
{
|
{
|
||||||
|
b.HasOne("API.Schema.MangaContext.MangaConnectors.MangaConnector", "MangaConnector")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("MangaConnectorName")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
b.HasOne("API.Schema.MangaContext.Chapter", "Obj")
|
b.HasOne("API.Schema.MangaContext.Chapter", "Obj")
|
||||||
.WithMany("MangaConnectorIds")
|
.WithMany("MangaConnectorIds")
|
||||||
.HasForeignKey("ObjId")
|
.HasForeignKey("ObjId")
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
.OnDelete(DeleteBehavior.NoAction)
|
||||||
.IsRequired();
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("MangaConnector");
|
||||||
|
|
||||||
b.Navigation("Obj");
|
b.Navigation("Obj");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.MangaContext.MangaConnectorId<API.Schema.MangaContext.Manga>", b =>
|
modelBuilder.Entity("API.Schema.MangaContext.MangaConnectorId<API.Schema.MangaContext.Manga>", b =>
|
||||||
{
|
{
|
||||||
|
b.HasOne("API.Schema.MangaContext.MangaConnectors.MangaConnector", "MangaConnector")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("MangaConnectorName")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
b.HasOne("API.Schema.MangaContext.Manga", "Obj")
|
b.HasOne("API.Schema.MangaContext.Manga", "Obj")
|
||||||
.WithMany("MangaConnectorIds")
|
.WithMany("MangaConnectorIds")
|
||||||
.HasForeignKey("ObjId")
|
.HasForeignKey("ObjId")
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
.IsRequired();
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("MangaConnector");
|
||||||
|
|
||||||
b.Navigation("Obj");
|
b.Navigation("Obj");
|
||||||
});
|
});
|
||||||
|
|
@@ -36,7 +36,7 @@ namespace API.Migrations.Manga
|
|||||||
});
|
});
|
||||||
|
|
||||||
migrationBuilder.CreateTable(
|
migrationBuilder.CreateTable(
|
||||||
name: "MangaConnector",
|
name: "MangaConnectors",
|
||||||
columns: table => new
|
columns: table => new
|
||||||
{
|
{
|
||||||
Name = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: false),
|
Name = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: false),
|
||||||
@@ -47,7 +47,7 @@ namespace API.Migrations.Manga
|
|||||||
},
|
},
|
||||||
constraints: table =>
|
constraints: table =>
|
||||||
{
|
{
|
||||||
table.PrimaryKey("PK_MangaConnector", x => x.Name);
|
table.PrimaryKey("PK_MangaConnectors", x => x.Name);
|
||||||
});
|
});
|
||||||
|
|
||||||
migrationBuilder.CreateTable(
|
migrationBuilder.CreateTable(
|
||||||
@@ -201,6 +201,12 @@ namespace API.Migrations.Manga
|
|||||||
constraints: table =>
|
constraints: table =>
|
||||||
{
|
{
|
||||||
table.PrimaryKey("PK_MangaConnectorToManga", x => x.Key);
|
table.PrimaryKey("PK_MangaConnectorToManga", x => x.Key);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_MangaConnectorToManga_MangaConnectors_MangaConnectorName",
|
||||||
|
column: x => x.MangaConnectorName,
|
||||||
|
principalTable: "MangaConnectors",
|
||||||
|
principalColumn: "Name",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
table.ForeignKey(
|
table.ForeignKey(
|
||||||
name: "FK_MangaConnectorToManga_Mangas_ObjId",
|
name: "FK_MangaConnectorToManga_Mangas_ObjId",
|
||||||
column: x => x.ObjId,
|
column: x => x.ObjId,
|
||||||
@@ -276,7 +282,12 @@ namespace API.Migrations.Manga
|
|||||||
name: "FK_MangaConnectorToChapter_Chapters_ObjId",
|
name: "FK_MangaConnectorToChapter_Chapters_ObjId",
|
||||||
column: x => x.ObjId,
|
column: x => x.ObjId,
|
||||||
principalTable: "Chapters",
|
principalTable: "Chapters",
|
||||||
principalColumn: "Key",
|
principalColumn: "Key");
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_MangaConnectorToChapter_MangaConnectors_MangaConnectorName",
|
||||||
|
column: x => x.MangaConnectorName,
|
||||||
|
principalTable: "MangaConnectors",
|
||||||
|
principalColumn: "Name",
|
||||||
onDelete: ReferentialAction.Cascade);
|
onDelete: ReferentialAction.Cascade);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -300,11 +311,21 @@ namespace API.Migrations.Manga
|
|||||||
table: "Link",
|
table: "Link",
|
||||||
column: "MangaKey");
|
column: "MangaKey");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_MangaConnectorToChapter_MangaConnectorName",
|
||||||
|
table: "MangaConnectorToChapter",
|
||||||
|
column: "MangaConnectorName");
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
migrationBuilder.CreateIndex(
|
||||||
name: "IX_MangaConnectorToChapter_ObjId",
|
name: "IX_MangaConnectorToChapter_ObjId",
|
||||||
table: "MangaConnectorToChapter",
|
table: "MangaConnectorToChapter",
|
||||||
column: "ObjId");
|
column: "ObjId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_MangaConnectorToManga_MangaConnectorName",
|
||||||
|
table: "MangaConnectorToManga",
|
||||||
|
column: "MangaConnectorName");
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
migrationBuilder.CreateIndex(
|
||||||
name: "IX_MangaConnectorToManga_ObjId",
|
name: "IX_MangaConnectorToManga_ObjId",
|
||||||
table: "MangaConnectorToManga",
|
table: "MangaConnectorToManga",
|
||||||
@@ -338,9 +359,6 @@ namespace API.Migrations.Manga
|
|||||||
migrationBuilder.DropTable(
|
migrationBuilder.DropTable(
|
||||||
name: "Link");
|
name: "Link");
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "MangaConnector");
|
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
migrationBuilder.DropTable(
|
||||||
name: "MangaConnectorToChapter");
|
name: "MangaConnectorToChapter");
|
||||||
|
|
||||||
@@ -359,6 +377,9 @@ namespace API.Migrations.Manga
|
|||||||
migrationBuilder.DropTable(
|
migrationBuilder.DropTable(
|
||||||
name: "Chapters");
|
name: "Chapters");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "MangaConnectors");
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
migrationBuilder.DropTable(
|
||||||
name: "Tags");
|
name: "Tags");
|
||||||
|
|
@@ -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,49 +16,15 @@ namespace API.Migrations.Manga
|
|||||||
{
|
{
|
||||||
#pragma warning disable 612, 618
|
#pragma warning disable 612, 618
|
||||||
modelBuilder
|
modelBuilder
|
||||||
.HasAnnotation("ProductVersion", "9.0.8")
|
.HasAnnotation("ProductVersion", "9.0.5")
|
||||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||||
|
|
||||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||||
|
|
||||||
modelBuilder.Entity("API.MangaConnectors.MangaConnector", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Name")
|
|
||||||
.HasMaxLength(32)
|
|
||||||
.HasColumnType("character varying(32)");
|
|
||||||
|
|
||||||
b.PrimitiveCollection<string[]>("BaseUris")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(256)
|
|
||||||
.HasColumnType("text[]");
|
|
||||||
|
|
||||||
b.Property<bool>("Enabled")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<string>("IconUrl")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(2048)
|
|
||||||
.HasColumnType("character varying(2048)");
|
|
||||||
|
|
||||||
b.PrimitiveCollection<string[]>("SupportedLanguages")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(8)
|
|
||||||
.HasColumnType("text[]");
|
|
||||||
|
|
||||||
b.HasKey("Name");
|
|
||||||
|
|
||||||
b.ToTable("MangaConnector");
|
|
||||||
|
|
||||||
b.HasDiscriminator<string>("Name").HasValue("MangaConnector");
|
|
||||||
|
|
||||||
b.UseTphMappingStrategy();
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.MangaContext.Author", b =>
|
modelBuilder.Entity("API.Schema.MangaContext.Author", b =>
|
||||||
{
|
{
|
||||||
b.Property<string>("Key")
|
b.Property<string>("Key")
|
||||||
.HasMaxLength(64)
|
.HasColumnType("text");
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.Property<string>("AuthorName")
|
b.Property<string>("AuthorName")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
@@ -73,8 +39,7 @@ namespace API.Migrations.Manga
|
|||||||
modelBuilder.Entity("API.Schema.MangaContext.Chapter", b =>
|
modelBuilder.Entity("API.Schema.MangaContext.Chapter", b =>
|
||||||
{
|
{
|
||||||
b.Property<string>("Key")
|
b.Property<string>("Key")
|
||||||
.HasMaxLength(64)
|
.HasColumnType("text");
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.Property<string>("ChapterNumber")
|
b.Property<string>("ChapterNumber")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
@@ -111,8 +76,7 @@ namespace API.Migrations.Manga
|
|||||||
modelBuilder.Entity("API.Schema.MangaContext.FileLibrary", b =>
|
modelBuilder.Entity("API.Schema.MangaContext.FileLibrary", b =>
|
||||||
{
|
{
|
||||||
b.Property<string>("Key")
|
b.Property<string>("Key")
|
||||||
.HasMaxLength(64)
|
.HasColumnType("text");
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.Property<string>("BasePath")
|
b.Property<string>("BasePath")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
@@ -132,8 +96,7 @@ namespace API.Migrations.Manga
|
|||||||
modelBuilder.Entity("API.Schema.MangaContext.Manga", b =>
|
modelBuilder.Entity("API.Schema.MangaContext.Manga", b =>
|
||||||
{
|
{
|
||||||
b.Property<string>("Key")
|
b.Property<string>("Key")
|
||||||
.HasMaxLength(64)
|
.HasColumnType("text");
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.Property<string>("CoverFileNameInCache")
|
b.Property<string>("CoverFileNameInCache")
|
||||||
.HasMaxLength(512)
|
.HasMaxLength(512)
|
||||||
@@ -185,8 +148,7 @@ namespace API.Migrations.Manga
|
|||||||
modelBuilder.Entity("API.Schema.MangaContext.MangaConnectorId<API.Schema.MangaContext.Chapter>", b =>
|
modelBuilder.Entity("API.Schema.MangaContext.MangaConnectorId<API.Schema.MangaContext.Chapter>", b =>
|
||||||
{
|
{
|
||||||
b.Property<string>("Key")
|
b.Property<string>("Key")
|
||||||
.HasMaxLength(64)
|
.HasColumnType("text");
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.Property<string>("IdOnConnectorSite")
|
b.Property<string>("IdOnConnectorSite")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
@@ -212,6 +174,8 @@ namespace API.Migrations.Manga
|
|||||||
|
|
||||||
b.HasKey("Key");
|
b.HasKey("Key");
|
||||||
|
|
||||||
|
b.HasIndex("MangaConnectorName");
|
||||||
|
|
||||||
b.HasIndex("ObjId");
|
b.HasIndex("ObjId");
|
||||||
|
|
||||||
b.ToTable("MangaConnectorToChapter");
|
b.ToTable("MangaConnectorToChapter");
|
||||||
@@ -220,8 +184,7 @@ namespace API.Migrations.Manga
|
|||||||
modelBuilder.Entity("API.Schema.MangaContext.MangaConnectorId<API.Schema.MangaContext.Manga>", b =>
|
modelBuilder.Entity("API.Schema.MangaContext.MangaConnectorId<API.Schema.MangaContext.Manga>", b =>
|
||||||
{
|
{
|
||||||
b.Property<string>("Key")
|
b.Property<string>("Key")
|
||||||
.HasMaxLength(64)
|
.HasColumnType("text");
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.Property<string>("IdOnConnectorSite")
|
b.Property<string>("IdOnConnectorSite")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
@@ -247,11 +210,46 @@ namespace API.Migrations.Manga
|
|||||||
|
|
||||||
b.HasKey("Key");
|
b.HasKey("Key");
|
||||||
|
|
||||||
|
b.HasIndex("MangaConnectorName");
|
||||||
|
|
||||||
b.HasIndex("ObjId");
|
b.HasIndex("ObjId");
|
||||||
|
|
||||||
b.ToTable("MangaConnectorToManga");
|
b.ToTable("MangaConnectorToManga");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Schema.MangaContext.MangaConnectors.MangaConnector", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasMaxLength(32)
|
||||||
|
.HasColumnType("character varying(32)");
|
||||||
|
|
||||||
|
b.PrimitiveCollection<string[]>("BaseUris")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("text[]");
|
||||||
|
|
||||||
|
b.Property<bool>("Enabled")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<string>("IconUrl")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(2048)
|
||||||
|
.HasColumnType("character varying(2048)");
|
||||||
|
|
||||||
|
b.PrimitiveCollection<string[]>("SupportedLanguages")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(8)
|
||||||
|
.HasColumnType("text[]");
|
||||||
|
|
||||||
|
b.HasKey("Name");
|
||||||
|
|
||||||
|
b.ToTable("MangaConnectors");
|
||||||
|
|
||||||
|
b.HasDiscriminator<string>("Name").HasValue("MangaConnector");
|
||||||
|
|
||||||
|
b.UseTphMappingStrategy();
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.MangaContext.MangaTag", b =>
|
modelBuilder.Entity("API.Schema.MangaContext.MangaTag", b =>
|
||||||
{
|
{
|
||||||
b.Property<string>("Tag")
|
b.Property<string>("Tag")
|
||||||
@@ -273,7 +271,7 @@ namespace API.Migrations.Manga
|
|||||||
|
|
||||||
b.Property<string>("MangaId")
|
b.Property<string>("MangaId")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasColumnType("character varying(64)");
|
.HasColumnType("text");
|
||||||
|
|
||||||
b.HasKey("MetadataFetcherName", "Identifier");
|
b.HasKey("MetadataFetcherName", "Identifier");
|
||||||
|
|
||||||
@@ -304,10 +302,10 @@ namespace API.Migrations.Manga
|
|||||||
modelBuilder.Entity("AuthorToManga", b =>
|
modelBuilder.Entity("AuthorToManga", b =>
|
||||||
{
|
{
|
||||||
b.Property<string>("AuthorIds")
|
b.Property<string>("AuthorIds")
|
||||||
.HasColumnType("character varying(64)");
|
.HasColumnType("text");
|
||||||
|
|
||||||
b.Property<string>("MangaIds")
|
b.Property<string>("MangaIds")
|
||||||
.HasColumnType("character varying(64)");
|
.HasColumnType("text");
|
||||||
|
|
||||||
b.HasKey("AuthorIds", "MangaIds");
|
b.HasKey("AuthorIds", "MangaIds");
|
||||||
|
|
||||||
@@ -322,7 +320,7 @@ namespace API.Migrations.Manga
|
|||||||
.HasColumnType("character varying(64)");
|
.HasColumnType("character varying(64)");
|
||||||
|
|
||||||
b.Property<string>("MangaIds")
|
b.Property<string>("MangaIds")
|
||||||
.HasColumnType("character varying(64)");
|
.HasColumnType("text");
|
||||||
|
|
||||||
b.HasKey("MangaTagIds", "MangaIds");
|
b.HasKey("MangaTagIds", "MangaIds");
|
||||||
|
|
||||||
@@ -331,23 +329,23 @@ namespace API.Migrations.Manga
|
|||||||
b.ToTable("MangaTagToManga");
|
b.ToTable("MangaTagToManga");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("API.MangaConnectors.ComickIo", b =>
|
modelBuilder.Entity("API.Schema.MangaContext.MangaConnectors.ComickIo", b =>
|
||||||
{
|
{
|
||||||
b.HasBaseType("API.MangaConnectors.MangaConnector");
|
b.HasBaseType("API.Schema.MangaContext.MangaConnectors.MangaConnector");
|
||||||
|
|
||||||
b.HasDiscriminator().HasValue("ComickIo");
|
b.HasDiscriminator().HasValue("ComickIo");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("API.MangaConnectors.Global", b =>
|
modelBuilder.Entity("API.Schema.MangaContext.MangaConnectors.Global", b =>
|
||||||
{
|
{
|
||||||
b.HasBaseType("API.MangaConnectors.MangaConnector");
|
b.HasBaseType("API.Schema.MangaContext.MangaConnectors.MangaConnector");
|
||||||
|
|
||||||
b.HasDiscriminator().HasValue("Global");
|
b.HasDiscriminator().HasValue("Global");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("API.MangaConnectors.MangaDex", b =>
|
modelBuilder.Entity("API.Schema.MangaContext.MangaConnectors.MangaDex", b =>
|
||||||
{
|
{
|
||||||
b.HasBaseType("API.MangaConnectors.MangaConnector");
|
b.HasBaseType("API.Schema.MangaContext.MangaConnectors.MangaConnector");
|
||||||
|
|
||||||
b.HasDiscriminator().HasValue("MangaDex");
|
b.HasDiscriminator().HasValue("MangaDex");
|
||||||
});
|
});
|
||||||
@@ -380,8 +378,7 @@ namespace API.Migrations.Manga
|
|||||||
b.OwnsMany("API.Schema.MangaContext.AltTitle", "AltTitles", b1 =>
|
b.OwnsMany("API.Schema.MangaContext.AltTitle", "AltTitles", b1 =>
|
||||||
{
|
{
|
||||||
b1.Property<string>("Key")
|
b1.Property<string>("Key")
|
||||||
.HasMaxLength(64)
|
.HasColumnType("text");
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b1.Property<string>("Language")
|
b1.Property<string>("Language")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
@@ -390,7 +387,7 @@ namespace API.Migrations.Manga
|
|||||||
|
|
||||||
b1.Property<string>("MangaKey")
|
b1.Property<string>("MangaKey")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasColumnType("character varying(64)");
|
.HasColumnType("text");
|
||||||
|
|
||||||
b1.Property<string>("Title")
|
b1.Property<string>("Title")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
@@ -410,8 +407,7 @@ namespace API.Migrations.Manga
|
|||||||
b.OwnsMany("API.Schema.MangaContext.Link", "Links", b1 =>
|
b.OwnsMany("API.Schema.MangaContext.Link", "Links", b1 =>
|
||||||
{
|
{
|
||||||
b1.Property<string>("Key")
|
b1.Property<string>("Key")
|
||||||
.HasMaxLength(64)
|
.HasColumnType("text");
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b1.Property<string>("LinkProvider")
|
b1.Property<string>("LinkProvider")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
@@ -425,7 +421,7 @@ namespace API.Migrations.Manga
|
|||||||
|
|
||||||
b1.Property<string>("MangaKey")
|
b1.Property<string>("MangaKey")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasColumnType("character varying(64)");
|
.HasColumnType("text");
|
||||||
|
|
||||||
b1.HasKey("Key");
|
b1.HasKey("Key");
|
||||||
|
|
||||||
@@ -446,23 +442,39 @@ namespace API.Migrations.Manga
|
|||||||
|
|
||||||
modelBuilder.Entity("API.Schema.MangaContext.MangaConnectorId<API.Schema.MangaContext.Chapter>", b =>
|
modelBuilder.Entity("API.Schema.MangaContext.MangaConnectorId<API.Schema.MangaContext.Chapter>", b =>
|
||||||
{
|
{
|
||||||
|
b.HasOne("API.Schema.MangaContext.MangaConnectors.MangaConnector", "MangaConnector")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("MangaConnectorName")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
b.HasOne("API.Schema.MangaContext.Chapter", "Obj")
|
b.HasOne("API.Schema.MangaContext.Chapter", "Obj")
|
||||||
.WithMany("MangaConnectorIds")
|
.WithMany("MangaConnectorIds")
|
||||||
.HasForeignKey("ObjId")
|
.HasForeignKey("ObjId")
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
.OnDelete(DeleteBehavior.NoAction)
|
||||||
.IsRequired();
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("MangaConnector");
|
||||||
|
|
||||||
b.Navigation("Obj");
|
b.Navigation("Obj");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.MangaContext.MangaConnectorId<API.Schema.MangaContext.Manga>", b =>
|
modelBuilder.Entity("API.Schema.MangaContext.MangaConnectorId<API.Schema.MangaContext.Manga>", b =>
|
||||||
{
|
{
|
||||||
|
b.HasOne("API.Schema.MangaContext.MangaConnectors.MangaConnector", "MangaConnector")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("MangaConnectorName")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
b.HasOne("API.Schema.MangaContext.Manga", "Obj")
|
b.HasOne("API.Schema.MangaContext.Manga", "Obj")
|
||||||
.WithMany("MangaConnectorIds")
|
.WithMany("MangaConnectorIds")
|
||||||
.HasForeignKey("ObjId")
|
.HasForeignKey("ObjId")
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
.IsRequired();
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("MangaConnector");
|
||||||
|
|
||||||
b.Navigation("Obj");
|
b.Navigation("Obj");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -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
|
#pragma warning disable 612, 618
|
||||||
modelBuilder
|
modelBuilder
|
||||||
.HasAnnotation("ProductVersion", "9.0.8")
|
.HasAnnotation("ProductVersion", "9.0.5")
|
||||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||||
|
|
||||||
NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "hstore");
|
NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "hstore");
|
||||||
@@ -27,8 +27,7 @@ namespace API.Migrations.Notifications
|
|||||||
modelBuilder.Entity("API.Schema.NotificationsContext.Notification", b =>
|
modelBuilder.Entity("API.Schema.NotificationsContext.Notification", b =>
|
||||||
{
|
{
|
||||||
b.Property<string>("Key")
|
b.Property<string>("Key")
|
||||||
.HasMaxLength(64)
|
.HasColumnType("text");
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.Property<DateTime>("Date")
|
b.Property<DateTime>("Date")
|
||||||
.HasColumnType("timestamp with time zone");
|
.HasColumnType("timestamp with time zone");
|
||||||
|
@@ -34,7 +34,7 @@ public class NamedSwaggerGenOptions : IConfigureNamedOptions<SwaggerGenOptions>
|
|||||||
{
|
{
|
||||||
var info = new OpenApiInfo()
|
var info = new OpenApiInfo()
|
||||||
{
|
{
|
||||||
Title = "Tranga " + description.GroupName,
|
Title = "Test API " + description.GroupName,
|
||||||
Version = description.ApiVersion.ToString()
|
Version = description.ApiVersion.ToString()
|
||||||
};
|
};
|
||||||
if (description.IsDeprecated)
|
if (description.IsDeprecated)
|
||||||
|
@@ -2,6 +2,7 @@ using System.Reflection;
|
|||||||
using API;
|
using API;
|
||||||
using API.Schema.LibraryContext;
|
using API.Schema.LibraryContext;
|
||||||
using API.Schema.MangaContext;
|
using API.Schema.MangaContext;
|
||||||
|
using API.Schema.MangaContext.MangaConnectors;
|
||||||
using API.Schema.NotificationsContext;
|
using API.Schema.NotificationsContext;
|
||||||
using Asp.Versioning;
|
using Asp.Versioning;
|
||||||
using Asp.Versioning.Builder;
|
using Asp.Versioning.Builder;
|
||||||
@@ -75,7 +76,7 @@ builder.Services.AddControllers().AddNewtonsoftJson(opts =>
|
|||||||
opts.SerializerSettings.Converters.Add(new StringEnumConverter());
|
opts.SerializerSettings.Converters.Add(new StringEnumConverter());
|
||||||
opts.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore;
|
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");
|
builder.WebHost.UseUrls("http://*:6531");
|
||||||
|
|
||||||
@@ -108,10 +109,18 @@ using (IServiceScope scope = app.Services.CreateScope())
|
|||||||
MangaContext context = scope.ServiceProvider.GetRequiredService<MangaContext>();
|
MangaContext context = scope.ServiceProvider.GetRequiredService<MangaContext>();
|
||||||
context.Database.Migrate();
|
context.Database.Migrate();
|
||||||
|
|
||||||
|
MangaConnector[] connectors =
|
||||||
|
[
|
||||||
|
new MangaDex(),
|
||||||
|
new ComickIo(),
|
||||||
|
new Global(scope.ServiceProvider.GetService<MangaContext>()!)
|
||||||
|
];
|
||||||
|
MangaConnector[] newConnectors = connectors.Where(c => !context.MangaConnectors.Contains(c)).ToArray();
|
||||||
|
context.MangaConnectors.AddRange(newConnectors);
|
||||||
if (!context.FileLibraries.Any())
|
if (!context.FileLibraries.Any())
|
||||||
context.FileLibraries.Add(new FileLibrary(Tranga.Settings.DownloadLocation, "Default FileLibrary"));
|
context.FileLibraries.Add(new FileLibrary(Tranga.Settings.DownloadLocation, "Default FileLibrary"));
|
||||||
|
|
||||||
await context.Sync(CancellationToken.None);
|
context.Sync();
|
||||||
}
|
}
|
||||||
|
|
||||||
using (IServiceScope scope = app.Services.CreateScope())
|
using (IServiceScope scope = app.Services.CreateScope())
|
||||||
@@ -123,7 +132,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=)"};
|
string[] emojis = { "(•‿•)", "(づ \u25d5‿\u25d5 )づ", "( \u02d8\u25bd\u02d8)っ\u2668", "=\uff3e\u25cf \u22cf \u25cf\uff3e=", "(ΦωΦ)", "(\u272a\u3268\u272a)", "( ノ・o・ )ノ", "(〜^\u2207^ )〜", "~(\u2267ω\u2266)~","૮ \u00b4• ﻌ \u00b4• ა", "(\u02c3ᆺ\u02c2)", "(=\ud83d\udf66 \u0f1d \ud83d\udf66=)"};
|
||||||
context.Notifications.Add(new Notification("Tranga Started", emojis[Random.Shared.Next(0, emojis.Length - 1)], NotificationUrgency.High));
|
context.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())
|
using (IServiceScope scope = app.Services.CreateScope())
|
||||||
@@ -131,12 +140,12 @@ using (IServiceScope scope = app.Services.CreateScope())
|
|||||||
LibraryContext context = scope.ServiceProvider.GetRequiredService<LibraryContext>();
|
LibraryContext context = scope.ServiceProvider.GetRequiredService<LibraryContext>();
|
||||||
context.Database.Migrate();
|
context.Database.Migrate();
|
||||||
|
|
||||||
await context.Sync(CancellationToken.None);
|
context.Sync();
|
||||||
}
|
}
|
||||||
|
|
||||||
Tranga.SetServiceProvider(app.Services);
|
Tranga.StartLogger();
|
||||||
Tranga.StartLogger(new FileInfo("Log4Net.config.xml"));
|
|
||||||
Tranga.AddDefaultWorkers();
|
Tranga.PeriodicWorkerStarterThread.Start(app.Services);
|
||||||
|
|
||||||
app.UseCors("AllowAll");
|
app.UseCors("AllowAll");
|
||||||
|
|
||||||
|
@@ -1,4 +1,3 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
namespace API.Schema;
|
namespace API.Schema;
|
||||||
@@ -6,18 +5,16 @@ namespace API.Schema;
|
|||||||
[PrimaryKey("Key")]
|
[PrimaryKey("Key")]
|
||||||
public abstract class Identifiable
|
public abstract class Identifiable
|
||||||
{
|
{
|
||||||
protected Identifiable()
|
public Identifiable()
|
||||||
{
|
{
|
||||||
this.Key = TokenGen.CreateToken(this.GetType());
|
this.Key = TokenGen.CreateToken(this.GetType());
|
||||||
}
|
}
|
||||||
|
|
||||||
protected Identifiable(string key)
|
public Identifiable(string key)
|
||||||
{
|
{
|
||||||
this.Key = key;
|
this.Key = key;
|
||||||
}
|
}
|
||||||
|
|
||||||
[Required]
|
|
||||||
[StringLength(TokenGen.MaximumLength, MinimumLength = TokenGen.MinimumLength)]
|
|
||||||
public string Key { get; init; }
|
public string Key { get; init; }
|
||||||
|
|
||||||
public override string ToString() => Key;
|
public override string ToString() => Key;
|
||||||
|
@@ -9,10 +9,19 @@ namespace API.Schema.LibraryContext.LibraryConnectors;
|
|||||||
[PrimaryKey("Key")]
|
[PrimaryKey("Key")]
|
||||||
public abstract class LibraryConnector : Identifiable
|
public abstract class LibraryConnector : Identifiable
|
||||||
{
|
{
|
||||||
|
[Required]
|
||||||
public LibraryType LibraryType { get; init; }
|
public LibraryType LibraryType { get; init; }
|
||||||
[StringLength(256)] [Url] public string BaseUrl { get; init; }
|
[StringLength(256)]
|
||||||
[StringLength(256)] public string Auth { get; init; }
|
[Required]
|
||||||
[NotMapped] protected ILog Log { get; init; }
|
[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)
|
protected LibraryConnector(LibraryType libraryType, string baseUrl, string auth)
|
||||||
: base()
|
: base()
|
||||||
|
@@ -7,6 +7,7 @@ namespace API.Schema.MangaContext;
|
|||||||
public class Author(string authorName) : Identifiable(TokenGen.CreateToken(typeof(Author), authorName))
|
public class Author(string authorName) : Identifiable(TokenGen.CreateToken(typeof(Author), authorName))
|
||||||
{
|
{
|
||||||
[StringLength(128)]
|
[StringLength(128)]
|
||||||
|
[Required]
|
||||||
public string AuthorName { get; init; } = authorName;
|
public string AuthorName { get; init; } = authorName;
|
||||||
|
|
||||||
public override string ToString() => $"{base.ToString()} {AuthorName}";
|
public override string ToString() => $"{base.ToString()} {AuthorName}";
|
||||||
|
@@ -4,29 +4,52 @@ using System.Text;
|
|||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using System.Xml.Linq;
|
using System.Xml.Linq;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
namespace API.Schema.MangaContext;
|
namespace API.Schema.MangaContext;
|
||||||
|
|
||||||
[PrimaryKey("Key")]
|
[PrimaryKey("Key")]
|
||||||
public class Chapter : Identifiable, IComparable<Chapter>
|
public class Chapter : Identifiable, IComparable<Chapter>
|
||||||
{
|
{
|
||||||
[StringLength(64)] public string ParentMangaId { get; init; } = null!;
|
[StringLength(64)] [Required] public string ParentMangaId { get; init; } = null!;
|
||||||
public Manga ParentManga = null!;
|
private Manga? _parentManga;
|
||||||
|
|
||||||
[NotMapped] public Dictionary<string, string> IdsOnMangaConnectors =>
|
[JsonIgnore]
|
||||||
|
public Manga ParentManga
|
||||||
|
{
|
||||||
|
get => _lazyLoader.Load(this, ref _parentManga) ?? throw new InvalidOperationException();
|
||||||
|
init
|
||||||
|
{
|
||||||
|
ParentMangaId = value.Key;
|
||||||
|
_parentManga = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[NotMapped]
|
||||||
|
public Dictionary<string, string> IdsOnMangaConnectors =>
|
||||||
MangaConnectorIds.ToDictionary(id => id.MangaConnectorName, id => id.IdOnConnectorSite);
|
MangaConnectorIds.ToDictionary(id => id.MangaConnectorName, id => id.IdOnConnectorSite);
|
||||||
public ICollection<MangaConnectorId<Chapter>> MangaConnectorIds = null!;
|
|
||||||
|
private ICollection<MangaConnectorId<Chapter>>? _mangaConnectorIds;
|
||||||
|
[JsonIgnore]
|
||||||
|
public ICollection<MangaConnectorId<Chapter>> MangaConnectorIds
|
||||||
|
{
|
||||||
|
get => _lazyLoader.Load(this, ref _mangaConnectorIds) ?? throw new InvalidOperationException();
|
||||||
|
init => _mangaConnectorIds = value;
|
||||||
|
}
|
||||||
|
|
||||||
public int? VolumeNumber { get; private set; }
|
public int? VolumeNumber { get; private set; }
|
||||||
[StringLength(10)] 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? 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);
|
[NotMapped] public string FullArchiveFilePath => Path.Join(ParentManga.FullDirectoryPath, FileName);
|
||||||
|
|
||||||
|
private readonly ILazyLoader _lazyLoader = null!;
|
||||||
|
|
||||||
public Chapter(Manga parentManga, string chapterNumber,
|
public Chapter(Manga parentManga, string chapterNumber,
|
||||||
int? volumeNumber, string? title = null)
|
int? volumeNumber, string? title = null)
|
||||||
: base(TokenGen.CreateToken(typeof(Chapter), parentManga.Key, chapterNumber))
|
: base(TokenGen.CreateToken(typeof(Chapter), parentManga.Key, chapterNumber))
|
||||||
@@ -38,15 +61,15 @@ public class Chapter : Identifiable, IComparable<Chapter>
|
|||||||
this.Title = title;
|
this.Title = title;
|
||||||
this.FileName = GetArchiveFilePath();
|
this.FileName = GetArchiveFilePath();
|
||||||
this.Downloaded = false;
|
this.Downloaded = false;
|
||||||
this.MangaConnectorIds = [];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// EF ONLY!!!
|
/// EF ONLY!!!
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal Chapter(string key, int? volumeNumber, string chapterNumber, string? title, string fileName, bool downloaded)
|
internal Chapter(ILazyLoader lazyLoader, string key, int? volumeNumber, string chapterNumber, string? title, string fileName, bool downloaded)
|
||||||
: base(key)
|
: base(key)
|
||||||
{
|
{
|
||||||
|
this._lazyLoader = lazyLoader;
|
||||||
this.VolumeNumber = volumeNumber;
|
this.VolumeNumber = volumeNumber;
|
||||||
this.ChapterNumber = chapterNumber;
|
this.ChapterNumber = chapterNumber;
|
||||||
this.Title = title;
|
this.Title = title;
|
||||||
@@ -66,27 +89,11 @@ public class Chapter : Identifiable, IComparable<Chapter>
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Checks the filesystem if an archive at the ArchiveFilePath exists
|
/// Checks the filesystem if an archive at the ArchiveFilePath exists
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="context"></param>
|
|
||||||
/// <param name="token"></param>
|
|
||||||
/// <returns>True if archive exists on disk</returns>
|
/// <returns>True if archive exists on disk</returns>
|
||||||
/// <exception cref="KeyNotFoundException">Unable to load Chapter, Parent or Library</exception>
|
public bool CheckDownloaded() => File.Exists(FullArchiveFilePath);
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Placeholders:
|
/// Placeholders:
|
||||||
/// %M Obj Name
|
/// %M Obj Name
|
||||||
@@ -156,20 +163,6 @@ public class Chapter : Identifiable, IComparable<Chapter>
|
|||||||
return stringBuilder.ToString();
|
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)
|
private static int CompareChapterNumbers(string ch1, string ch2)
|
||||||
{
|
{
|
||||||
int[] ch1Arr = ch1.Split('.').Select(c => int.TryParse(c, out int result) ? result : -1).ToArray();
|
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)
|
public class FileLibrary(string basePath, string libraryName)
|
||||||
: Identifiable(TokenGen.CreateToken(typeof(FileLibrary), basePath))
|
: 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}";
|
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))
|
public class Link(string linkProvider, string linkUrl) : Identifiable(TokenGen.CreateToken(typeof(Link), linkProvider, linkUrl))
|
||||||
{
|
{
|
||||||
[StringLength(64)]
|
[StringLength(64)]
|
||||||
|
[Required]
|
||||||
public string LinkProvider { get; init; } = linkProvider;
|
public string LinkProvider { get; init; } = linkProvider;
|
||||||
[StringLength(2048)]
|
[StringLength(2048)]
|
||||||
|
[Required]
|
||||||
[Url]
|
[Url]
|
||||||
public string LinkUrl { get; init; } = linkUrl;
|
public string LinkUrl { get; init; } = linkUrl;
|
||||||
|
|
||||||
|
@@ -4,10 +4,8 @@ using System.Runtime.InteropServices;
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
using API.Workers;
|
using API.Workers;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using SixLabors.ImageSharp;
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
using SixLabors.ImageSharp.Formats.Jpeg;
|
using Newtonsoft.Json;
|
||||||
using SixLabors.ImageSharp.Processing;
|
|
||||||
using SixLabors.ImageSharp.Processing.Processors.Transforms;
|
|
||||||
using static System.IO.UnixFileMode;
|
using static System.IO.UnixFileMode;
|
||||||
|
|
||||||
namespace API.Schema.MangaContext;
|
namespace API.Schema.MangaContext;
|
||||||
@@ -15,31 +13,57 @@ namespace API.Schema.MangaContext;
|
|||||||
[PrimaryKey("Key")]
|
[PrimaryKey("Key")]
|
||||||
public class Manga : Identifiable
|
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; }
|
[Required] public string Description { get; internal set; }
|
||||||
[Url] [StringLength(512)] public string CoverUrl { get; internal set; }
|
[JsonIgnore] [Url] [StringLength(512)] public string CoverUrl { get; internal set; }
|
||||||
public MangaReleaseStatus ReleaseStatus { get; internal set; }
|
[Required] public MangaReleaseStatus ReleaseStatus { get; internal set; }
|
||||||
[StringLength(64)] public string? LibraryId { get; private set; }
|
[StringLength(64)] public string? LibraryId { get; private set; }
|
||||||
public FileLibrary? Library = null!;
|
private FileLibrary? _library;
|
||||||
|
[JsonIgnore]
|
||||||
|
public FileLibrary? Library
|
||||||
|
{
|
||||||
|
get => _lazyLoader.Load(this, ref _library);
|
||||||
|
set
|
||||||
|
{
|
||||||
|
LibraryId = value?.Key;
|
||||||
|
_library = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public ICollection<Author> Authors { get; internal set; } = null!;
|
public ICollection<Author> Authors { get; internal set; }= null!;
|
||||||
public ICollection<MangaTag> MangaTags { get; internal set; } = null!;
|
public ICollection<MangaTag> MangaTags { get; internal set; }= null!;
|
||||||
public ICollection<Link> Links { get; internal set; } = null!;
|
public ICollection<Link> Links { get; internal set; }= null!;
|
||||||
public ICollection<AltTitle> AltTitles { 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(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; }
|
public uint? Year { get; internal init; }
|
||||||
[StringLength(8)] public string? OriginalLanguage { 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();
|
[NotMapped] public ICollection<string> ChapterIds => Chapters.Select(c => c.Key).ToList();
|
||||||
public ICollection<Chapter> Chapters = null!;
|
private ICollection<Chapter>? _chapters;
|
||||||
|
[JsonIgnore]
|
||||||
|
public ICollection<Chapter> Chapters
|
||||||
|
{
|
||||||
|
get => _lazyLoader.Load(this, ref _chapters) ?? throw new InvalidOperationException();
|
||||||
|
init => _chapters = value;
|
||||||
|
}
|
||||||
|
|
||||||
[NotMapped] public Dictionary<string, string> IdsOnMangaConnectors => MangaConnectorIds.ToDictionary(id => id.MangaConnectorName, id => id.IdOnConnectorSite);
|
[NotMapped] public Dictionary<string, string> IdsOnMangaConnectors =>
|
||||||
[NotMapped] public ICollection<string> MangaConnectorIdsIds => MangaConnectorIds.Select(id => id.Key).ToList();
|
MangaConnectorIds.ToDictionary(id => id.MangaConnectorName, id => id.IdOnConnectorSite);
|
||||||
public ICollection<MangaConnectorId<Manga>> MangaConnectorIds = null!;
|
private ICollection<MangaConnectorId<Manga>>? _mangaConnectorIds;
|
||||||
|
[JsonIgnore]
|
||||||
|
public ICollection<MangaConnectorId<Manga>> MangaConnectorIds
|
||||||
|
{
|
||||||
|
get => _lazyLoader.Load(this, ref _mangaConnectorIds) ?? throw new InvalidOperationException();
|
||||||
|
private set => _mangaConnectorIds = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly ILazyLoader _lazyLoader = null!;
|
||||||
|
|
||||||
public Manga(string name, string description, string coverUrl, MangaReleaseStatus releaseStatus,
|
public Manga(string name, string description, string coverUrl, MangaReleaseStatus releaseStatus,
|
||||||
ICollection<Author> authors, ICollection<MangaTag> mangaTags, ICollection<Link> links, ICollection<AltTitle> altTitles,
|
ICollection<Author> authors, ICollection<MangaTag> mangaTags, ICollection<Link> links, ICollection<AltTitle> altTitles,
|
||||||
@@ -60,17 +84,17 @@ public class Manga : Identifiable
|
|||||||
this.Year = year;
|
this.Year = year;
|
||||||
this.OriginalLanguage = originalLanguage;
|
this.OriginalLanguage = originalLanguage;
|
||||||
this.Chapters = [];
|
this.Chapters = [];
|
||||||
this.MangaConnectorIds = [];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// EF ONLY!!!
|
/// EF ONLY!!!
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Manga(string key, string name, string description, string coverUrl,
|
public Manga(ILazyLoader lazyLoader, string key, string name, string description, string coverUrl,
|
||||||
MangaReleaseStatus releaseStatus,
|
MangaReleaseStatus releaseStatus,
|
||||||
string directoryName, float ignoreChaptersBefore, string? libraryId, uint? year, string? originalLanguage)
|
string directoryName, float ignoreChaptersBefore, string? libraryId, uint? year, string? originalLanguage)
|
||||||
: base(key)
|
: base(key)
|
||||||
{
|
{
|
||||||
|
this._lazyLoader = lazyLoader;
|
||||||
this.Name = name;
|
this.Name = name;
|
||||||
this.Description = description;
|
this.Description = description;
|
||||||
this.CoverUrl = coverUrl;
|
this.CoverUrl = coverUrl;
|
||||||
@@ -159,18 +183,6 @@ public class Manga : Identifiable
|
|||||||
return newJobs.ToArray();
|
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}";
|
public override string ToString() => $"{base.ToString()} {Name}";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,27 +1,52 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using API.MangaConnectors;
|
using API.Schema.MangaContext.MangaConnectors;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
namespace API.Schema.MangaContext;
|
namespace API.Schema.MangaContext;
|
||||||
|
|
||||||
[PrimaryKey("Key")]
|
[PrimaryKey("Key")]
|
||||||
public class MangaConnectorId<T> : Identifiable where T : Identifiable
|
public class MangaConnectorId<T> : Identifiable where T : Identifiable
|
||||||
{
|
{
|
||||||
public T Obj = null!;
|
[StringLength(64)] [Required] public string ObjId { get; private set; } = null!;
|
||||||
[StringLength(64)] public string ObjId { get; internal set; }
|
[JsonIgnore] private T? _obj;
|
||||||
|
|
||||||
[StringLength(32)] public string MangaConnectorName { get; private set; }
|
[JsonIgnore]
|
||||||
|
public T Obj
|
||||||
|
{
|
||||||
|
get => _lazyLoader.Load(this, ref _obj) ?? throw new InvalidOperationException();
|
||||||
|
internal set
|
||||||
|
{
|
||||||
|
ObjId = value.Key;
|
||||||
|
_obj = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[StringLength(256)] public string IdOnConnectorSite { get; init; }
|
[StringLength(32)] [Required] public string MangaConnectorName { get; private set; } = null!;
|
||||||
|
[JsonIgnore] private MangaConnector? _mangaConnector;
|
||||||
|
[JsonIgnore]
|
||||||
|
public MangaConnector MangaConnector
|
||||||
|
{
|
||||||
|
get => _lazyLoader.Load(this, ref _mangaConnector) ?? throw new InvalidOperationException();
|
||||||
|
init
|
||||||
|
{
|
||||||
|
MangaConnectorName = value.Name;
|
||||||
|
_mangaConnector = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[StringLength(256)] [Required] public string IdOnConnectorSite { get; init; }
|
||||||
[Url] [StringLength(512)] public string? WebsiteUrl { get; internal init; }
|
[Url] [StringLength(512)] public string? WebsiteUrl { get; internal init; }
|
||||||
public bool UseForDownload { get; internal set; }
|
public bool UseForDownload { get; internal set; }
|
||||||
|
|
||||||
|
private readonly ILazyLoader _lazyLoader = null!;
|
||||||
|
|
||||||
public MangaConnectorId(T obj, MangaConnector mangaConnector, string idOnConnectorSite, string? websiteUrl, bool useForDownload = false)
|
public MangaConnectorId(T obj, MangaConnector mangaConnector, string idOnConnectorSite, string? websiteUrl, bool useForDownload = false)
|
||||||
: base(TokenGen.CreateToken(typeof(MangaConnectorId<T>), mangaConnector.Name, idOnConnectorSite))
|
: base(TokenGen.CreateToken(typeof(MangaConnectorId<T>), mangaConnector.Name, idOnConnectorSite))
|
||||||
{
|
{
|
||||||
this.Obj = obj;
|
this.Obj = obj;
|
||||||
this.ObjId = obj.Key;
|
this.MangaConnector = mangaConnector;
|
||||||
this.MangaConnectorName = mangaConnector.Name;
|
|
||||||
this.IdOnConnectorSite = idOnConnectorSite;
|
this.IdOnConnectorSite = idOnConnectorSite;
|
||||||
this.WebsiteUrl = websiteUrl;
|
this.WebsiteUrl = websiteUrl;
|
||||||
this.UseForDownload = useForDownload;
|
this.UseForDownload = useForDownload;
|
||||||
@@ -30,9 +55,10 @@ public class MangaConnectorId<T> : Identifiable where T : Identifiable
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// EF CORE ONLY!!!
|
/// EF CORE ONLY!!!
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public MangaConnectorId(string key, string objId, string mangaConnectorName, string idOnConnectorSite, bool useForDownload, string? websiteUrl)
|
public MangaConnectorId(ILazyLoader lazyLoader, string key, string objId, string mangaConnectorName, string idOnConnectorSite, bool useForDownload, string? websiteUrl)
|
||||||
: base(key)
|
: base(key)
|
||||||
{
|
{
|
||||||
|
this._lazyLoader = lazyLoader;
|
||||||
this.ObjId = objId;
|
this.ObjId = objId;
|
||||||
this.MangaConnectorName = mangaConnectorName;
|
this.MangaConnectorName = mangaConnectorName;
|
||||||
this.IdOnConnectorSite = idOnConnectorSite;
|
this.IdOnConnectorSite = idOnConnectorSite;
|
||||||
@@ -40,5 +66,5 @@ public class MangaConnectorId<T> : Identifiable where T : Identifiable
|
|||||||
this.UseForDownload = useForDownload;
|
this.UseForDownload = useForDownload;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override string ToString() => $"{base.ToString()} {Obj}";
|
public override string ToString() => $"{base.ToString()} {_obj}";
|
||||||
}
|
}
|
@@ -1,9 +1,8 @@
|
|||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using API.MangaDownloadClients;
|
using API.MangaDownloadClients;
|
||||||
using API.Schema.MangaContext;
|
|
||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
|
|
||||||
namespace API.MangaConnectors;
|
namespace API.Schema.MangaContext.MangaConnectors;
|
||||||
|
|
||||||
public class ComickIo : MangaConnector
|
public class ComickIo : MangaConnector
|
||||||
{
|
{
|
||||||
@@ -177,19 +176,19 @@ public class ComickIo : MangaConnector
|
|||||||
byte whatever = 0;
|
byte whatever = 0;
|
||||||
List<AltTitle> altTitles = altTitlesArray?
|
List<AltTitle> altTitles = altTitlesArray?
|
||||||
.Select(token => new AltTitle(token.Value<string>("lang")??whatever++.ToString(), token.Value<string>("title")!))
|
.Select(token => new AltTitle(token.Value<string>("lang")??whatever++.ToString(), token.Value<string>("title")!))
|
||||||
.ToList()??[];
|
.ToList()!;
|
||||||
|
|
||||||
JArray? authorsArray = json["authors"] as JArray;
|
JArray? authorsArray = json["authors"] as JArray;
|
||||||
JArray? artistsArray = json["artists"] as JArray;
|
JArray? artistsArray = json["artists"] as JArray;
|
||||||
List<Author> authors = authorsArray?.Concat(artistsArray!)
|
List<Author> authors = authorsArray?.Concat(artistsArray!)
|
||||||
.Select(token => new Author(token.Value<string>("name")!))
|
.Select(token => new Author(token.Value<string>("name")!))
|
||||||
.DistinctBy(a => a.Key)
|
.DistinctBy(a => a.Key)
|
||||||
.ToList()??[];
|
.ToList()!;
|
||||||
|
|
||||||
JArray? genreArray = json["comic"]?["md_comic_md_genres"] as JArray;
|
JArray? genreArray = json["comic"]?["md_comic_md_genres"] as JArray;
|
||||||
List<MangaTag> tags = genreArray?
|
List<MangaTag> tags = genreArray?
|
||||||
.Select(token => new MangaTag(token["md_genres"]?.Value<string>("name")!))
|
.Select(token => new MangaTag(token["md_genres"]?.Value<string>("name")!))
|
||||||
.ToList()??[];
|
.ToList()!;
|
||||||
|
|
||||||
JArray? linksArray = json["comic"]?["links"] as JArray;
|
JArray? linksArray = json["comic"]?["links"] as JArray;
|
||||||
List<Link> links = linksArray?
|
List<Link> links = linksArray?
|
||||||
@@ -221,7 +220,7 @@ public class ComickIo : MangaConnector
|
|||||||
_ => kv.Key
|
_ => kv.Key
|
||||||
};
|
};
|
||||||
return new Link(key, fullUrl);
|
return new Link(key, fullUrl);
|
||||||
}).ToList()??[];
|
}).ToList()!;
|
||||||
|
|
||||||
if(hid is null)
|
if(hid is null)
|
||||||
throw new Exception("hid is null");
|
throw new Exception("hid is null");
|
||||||
@@ -232,9 +231,7 @@ public class ComickIo : MangaConnector
|
|||||||
|
|
||||||
Manga manga = new (name, description??"", coverUrl, status, authors, tags, links, altTitles,
|
Manga manga = new (name, description??"", coverUrl, status, authors, tags, links, altTitles,
|
||||||
year: year, originalLanguage: originalLanguage);
|
year: year, originalLanguage: originalLanguage);
|
||||||
MangaConnectorId<Manga> mcId = new (manga, this, hid, url);
|
return (manga, new MangaConnectorId<Manga>(manga, this, hid, url));
|
||||||
manga.MangaConnectorIds.Add(mcId);
|
|
||||||
return (manga, mcId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<(Chapter, MangaConnectorId<Chapter>)> ParseChapters(MangaConnectorId<Manga> mcIdManga, JArray chaptersArray)
|
private List<(Chapter, MangaConnectorId<Chapter>)> ParseChapters(MangaConnectorId<Manga> mcIdManga, JArray chaptersArray)
|
||||||
@@ -253,9 +250,8 @@ public class ComickIo : MangaConnector
|
|||||||
continue;
|
continue;
|
||||||
|
|
||||||
Chapter ch = new (mcIdManga.Obj, chapterNum, volumeNum, title);
|
Chapter ch = new (mcIdManga.Obj, chapterNum, volumeNum, title);
|
||||||
MangaConnectorId<Chapter> mcId = new(ch, this, hid, url);
|
|
||||||
ch.MangaConnectorIds.Add(mcId);
|
chapters.Add((ch, new (ch, this, hid, url)));
|
||||||
chapters.Add((ch, mcId));
|
|
||||||
}
|
}
|
||||||
return chapters;
|
return chapters;
|
||||||
}
|
}
|
@@ -1,17 +1,17 @@
|
|||||||
using API.Schema.MangaContext;
|
namespace API.Schema.MangaContext.MangaConnectors;
|
||||||
|
|
||||||
namespace API.MangaConnectors;
|
|
||||||
|
|
||||||
public class Global : MangaConnector
|
public class Global : MangaConnector
|
||||||
{
|
{
|
||||||
public Global() : base("Global", ["all"], [""], "https://avatars.githubusercontent.com/u/13404778")
|
private MangaContext context { get; init; }
|
||||||
|
public Global(MangaContext context) : base("Global", ["all"], [""], "")
|
||||||
{
|
{
|
||||||
|
this.context = context;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override (Manga, MangaConnectorId<Manga>)[] SearchManga(string mangaSearchName)
|
public override (Manga, MangaConnectorId<Manga>)[] SearchManga(string mangaSearchName)
|
||||||
{
|
{
|
||||||
//Get all enabled Connectors
|
//Get all enabled Connectors
|
||||||
MangaConnector[] enabledConnectors = Tranga.MangaConnectors.Where(c => c.Enabled && c.Name != "Global").ToArray();
|
MangaConnector[] enabledConnectors = context.MangaConnectors.Where(c => c.Enabled && c.Name != "Global").ToArray();
|
||||||
|
|
||||||
//Create Task for each MangaConnector to search simultaneously
|
//Create Task for each MangaConnector to search simultaneously
|
||||||
Task<(Manga, MangaConnectorId<Manga>)[]>[] tasks =
|
Task<(Manga, MangaConnectorId<Manga>)[]>[] tasks =
|
||||||
@@ -32,7 +32,7 @@ public class Global : MangaConnector
|
|||||||
|
|
||||||
public override (Manga, MangaConnectorId<Manga>)? GetMangaFromUrl(string url)
|
public override (Manga, MangaConnectorId<Manga>)? GetMangaFromUrl(string url)
|
||||||
{
|
{
|
||||||
MangaConnector? mc = Tranga.MangaConnectors.FirstOrDefault(c => c.UrlMatchesConnector(url));
|
MangaConnector? mc = context.MangaConnectors.ToArray().FirstOrDefault(c => c.UrlMatchesConnector(url));
|
||||||
return mc?.GetMangaFromUrl(url) ?? null;
|
return mc?.GetMangaFromUrl(url) ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,15 +44,11 @@ public class Global : MangaConnector
|
|||||||
public override (Chapter, MangaConnectorId<Chapter>)[] GetChapters(MangaConnectorId<Manga> manga,
|
public override (Chapter, MangaConnectorId<Chapter>)[] GetChapters(MangaConnectorId<Manga> manga,
|
||||||
string? language = null)
|
string? language = null)
|
||||||
{
|
{
|
||||||
if (!Tranga.TryGetMangaConnector(manga.MangaConnectorName, out MangaConnector? mangaConnector))
|
return manga.MangaConnector.GetChapters(manga, language);
|
||||||
return [];
|
|
||||||
return mangaConnector.GetChapters(manga, language);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
internal override string[] GetChapterImageUrls(MangaConnectorId<Chapter> chapterId)
|
internal override string[] GetChapterImageUrls(MangaConnectorId<Chapter> chapterId)
|
||||||
{
|
{
|
||||||
if (!Tranga.TryGetMangaConnector(chapterId.MangaConnectorName, out MangaConnector? mangaConnector))
|
return chapterId.MangaConnector.GetChapterImageUrls(chapterId);
|
||||||
return [];
|
|
||||||
return mangaConnector.GetChapterImageUrls(chapterId);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
75
API/Schema/MangaContext/MangaConnectors/MangaConnector.cs
Normal file
75
API/Schema/MangaContext/MangaConnectors/MangaConnector.cs
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using API.MangaDownloadClients;
|
||||||
|
using log4net;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace API.Schema.MangaContext.MangaConnectors;
|
||||||
|
|
||||||
|
[PrimaryKey("Name")]
|
||||||
|
public abstract class MangaConnector(string name, string[] supportedLanguages, string[] baseUris, string iconUrl)
|
||||||
|
{
|
||||||
|
[JsonIgnore]
|
||||||
|
[NotMapped]
|
||||||
|
internal DownloadClient downloadClient { get; init; } = null!;
|
||||||
|
|
||||||
|
[JsonIgnore]
|
||||||
|
[NotMapped]
|
||||||
|
protected ILog Log { get; init; } = LogManager.GetLogger(name);
|
||||||
|
|
||||||
|
[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);
|
||||||
|
|
||||||
|
public abstract (Manga, MangaConnectorId<Manga>)? GetMangaFromUrl(string url);
|
||||||
|
|
||||||
|
public abstract (Manga, MangaConnectorId<Manga>)? GetMangaFromId(string mangaIdOnSite);
|
||||||
|
|
||||||
|
public abstract (Chapter, MangaConnectorId<Chapter>)[] GetChapters(MangaConnectorId<Manga> mangaId,
|
||||||
|
string? language = null);
|
||||||
|
|
||||||
|
internal abstract string[] GetChapterImageUrls(MangaConnectorId<Chapter> chapterId);
|
||||||
|
|
||||||
|
public bool UrlMatchesConnector(string url) => BaseUris.Any(baseUri => Regex.IsMatch(url, "https?://" + baseUri + "/.*"));
|
||||||
|
|
||||||
|
internal string? SaveCoverImageToCache(MangaConnectorId<Manga> mangaId, int retries = 3)
|
||||||
|
{
|
||||||
|
if(retries < 0)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
Regex urlRex = new (@"https?:\/\/((?:[a-zA-Z0-9-]+\.)+[a-zA-Z0-9]+)\/(?:.+\/)*(.+\.([a-zA-Z]+))");
|
||||||
|
//https?:\/\/[a-zA-Z0-9-]+\.([a-zA-Z0-9-]+\.[a-zA-Z0-9]+)\/(?:.+\/)*(.+\.([a-zA-Z]+)) for only second level domains
|
||||||
|
Match match = urlRex.Match(mangaId.Obj.CoverUrl);
|
||||||
|
string filename = $"{match.Groups[1].Value}-{mangaId.Key}.{match.Groups[3].Value}";
|
||||||
|
string saveImagePath = Path.Join(TrangaSettings.coverImageCache, filename);
|
||||||
|
|
||||||
|
if (File.Exists(saveImagePath))
|
||||||
|
return saveImagePath;
|
||||||
|
|
||||||
|
RequestResult coverResult = downloadClient.MakeRequest(mangaId.Obj.CoverUrl, RequestType.MangaCover, $"https://{match.Groups[1].Value}");
|
||||||
|
if ((int)coverResult.statusCode < 200 || (int)coverResult.statusCode >= 300)
|
||||||
|
return SaveCoverImageToCache(mangaId, --retries);
|
||||||
|
|
||||||
|
using MemoryStream ms = new();
|
||||||
|
coverResult.result.CopyTo(ms);
|
||||||
|
Directory.CreateDirectory(TrangaSettings.coverImageCache);
|
||||||
|
File.WriteAllBytes(saveImagePath, ms.ToArray());
|
||||||
|
|
||||||
|
return saveImagePath;
|
||||||
|
}
|
||||||
|
}
|
@@ -1,9 +1,8 @@
|
|||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using API.MangaDownloadClients;
|
using API.MangaDownloadClients;
|
||||||
using API.Schema.MangaContext;
|
|
||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
|
|
||||||
namespace API.MangaConnectors;
|
namespace API.Schema.MangaContext.MangaConnectors;
|
||||||
|
|
||||||
public class MangaDex : MangaConnector
|
public class MangaDex : MangaConnector
|
||||||
{
|
{
|
||||||
@@ -277,9 +276,9 @@ public class MangaDex : MangaConnector
|
|||||||
_ => kv.Key
|
_ => kv.Key
|
||||||
};
|
};
|
||||||
return new Link(key, url);
|
return new Link(key, url);
|
||||||
}).ToList()??[];
|
}).ToList()!;
|
||||||
|
|
||||||
List<AltTitle> altTitles = altTitlesJArray?
|
List<AltTitle> altTitles = (altTitlesJArray??[])
|
||||||
.Select(t =>
|
.Select(t =>
|
||||||
{
|
{
|
||||||
JObject? j = t as JObject;
|
JObject? j = t as JObject;
|
||||||
@@ -287,19 +286,19 @@ public class MangaDex : MangaConnector
|
|||||||
if (p is null)
|
if (p is null)
|
||||||
return null;
|
return null;
|
||||||
return new AltTitle(p.Name, p.Value.ToString());
|
return new AltTitle(p.Name, p.Value.ToString());
|
||||||
}).Where(x => x is not null).Cast<AltTitle>().ToList()??[];
|
}).Where(x => x is not null).ToList()!;
|
||||||
|
|
||||||
List<MangaTag> tags = tagsJArray?
|
List<MangaTag> tags = (tagsJArray??[])
|
||||||
.Where(t => t.Value<string>("type") == "tag")
|
.Where(t => t.Value<string>("type") == "tag")
|
||||||
.Select(t => t["attributes"]?["name"]?.Value<string>("en")??t["attributes"]?["name"]?.First?.First?.Value<string>())
|
.Select(t => t["attributes"]?["name"]?.Value<string>("en")??t["attributes"]?["name"]?.First?.First?.Value<string>())
|
||||||
.Select(str => str is not null ? new MangaTag(str) : null)
|
.Select(str => str is not null ? new MangaTag(str) : null)
|
||||||
.Where(x => x is not null).Cast<MangaTag>().ToList()??[];
|
.Where(x => x is not null).ToList()!;
|
||||||
|
|
||||||
List<Author> authors = relationships
|
List<Author> authors = relationships
|
||||||
.Where(r => r["type"]?.Value<string>() == "author")
|
.Where(r => r["type"]?.Value<string>() == "author")
|
||||||
.Select(t => t["attributes"]?.Value<string>("name"))
|
.Select(t => t["attributes"]?.Value<string>("name"))
|
||||||
.Select(str => str is not null ? new Author(str) : null)
|
.Select(str => str is not null ? new Author(str) : null)
|
||||||
.Where(x => x is not null).Cast<Author>().ToList();
|
.Where(x => x is not null).ToList()!;
|
||||||
|
|
||||||
|
|
||||||
MangaReleaseStatus releaseStatus = status switch
|
MangaReleaseStatus releaseStatus = status switch
|
||||||
@@ -313,11 +312,9 @@ public class MangaDex : MangaConnector
|
|||||||
string websiteUrl = $"https://mangadex.org/title/{id}";
|
string websiteUrl = $"https://mangadex.org/title/{id}";
|
||||||
string coverUrl = $"https://uploads.mangadex.org/covers/{id}/{coverFileName}";
|
string coverUrl = $"https://uploads.mangadex.org/covers/{id}/{coverFileName}";
|
||||||
|
|
||||||
Manga manga = new (name, description, coverUrl, releaseStatus, authors, tags, links,altTitles,
|
Manga manga = new Manga(name, description, coverUrl, releaseStatus, authors, tags, links,altTitles,
|
||||||
null, 0f, year, originalLanguage);
|
null, 0f, year, originalLanguage);
|
||||||
MangaConnectorId<Manga> mcId = new (manga, this, id, websiteUrl);
|
return (manga, new MangaConnectorId<Manga>(manga, this, id, websiteUrl));
|
||||||
manga.MangaConnectorIds.Add(mcId);
|
|
||||||
return (manga, mcId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private (Chapter chapter, MangaConnectorId<Chapter> id) ParseChapterFromJToken(MangaConnectorId<Manga> mcIdManga, JToken jToken)
|
private (Chapter chapter, MangaConnectorId<Chapter> id) ParseChapterFromJToken(MangaConnectorId<Manga> mcIdManga, JToken jToken)
|
||||||
@@ -336,8 +333,6 @@ public class MangaDex : MangaConnector
|
|||||||
|
|
||||||
string websiteUrl = $"https://mangadex.org/chapter/{id}";
|
string websiteUrl = $"https://mangadex.org/chapter/{id}";
|
||||||
Chapter chapter = new (mcIdManga.Obj, chapterStr, volumeNumber, title);
|
Chapter chapter = new (mcIdManga.Obj, chapterStr, volumeNumber, title);
|
||||||
MangaConnectorId<Chapter> mcId = new(chapter, this, id, websiteUrl);
|
return (chapter, new MangaConnectorId<Chapter>(chapter, this, id, websiteUrl));
|
||||||
chapter.MangaConnectorIds.Add(mcId);
|
|
||||||
return (chapter, mcId);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -1,12 +1,12 @@
|
|||||||
using API.MangaConnectors;
|
using API.Schema.MangaContext.MangaConnectors;
|
||||||
using API.Schema.MangaContext.MetadataFetchers;
|
using API.Schema.MangaContext.MetadataFetchers;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Query;
|
|
||||||
|
|
||||||
namespace API.Schema.MangaContext;
|
namespace API.Schema.MangaContext;
|
||||||
|
|
||||||
public class MangaContext(DbContextOptions<MangaContext> options) : TrangaBaseContext<MangaContext>(options)
|
public class MangaContext(DbContextOptions<MangaContext> options) : TrangaBaseContext<MangaContext>(options)
|
||||||
{
|
{
|
||||||
|
public DbSet<MangaConnector> MangaConnectors { get; set; }
|
||||||
public DbSet<Manga> Mangas { get; set; }
|
public DbSet<Manga> Mangas { get; set; }
|
||||||
public DbSet<FileLibrary> FileLibraries { get; set; }
|
public DbSet<FileLibrary> FileLibraries { get; set; }
|
||||||
public DbSet<Chapter> Chapters { get; set; }
|
public DbSet<Chapter> Chapters { get; set; }
|
||||||
@@ -31,12 +31,26 @@ public class MangaContext(DbContextOptions<MangaContext> options) : TrangaBaseCo
|
|||||||
.WithOne(c => c.ParentManga)
|
.WithOne(c => c.ParentManga)
|
||||||
.HasForeignKey(c => c.ParentMangaId)
|
.HasForeignKey(c => c.ParentMangaId)
|
||||||
.OnDelete(DeleteBehavior.Cascade);
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
modelBuilder.Entity<Manga>()
|
||||||
|
.Navigation(m => m.Chapters)
|
||||||
|
.EnableLazyLoading();
|
||||||
|
modelBuilder.Entity<Chapter>()
|
||||||
|
.Navigation(c => c.ParentManga)
|
||||||
|
.EnableLazyLoading();
|
||||||
//Chapter has MangaConnectorIds
|
//Chapter has MangaConnectorIds
|
||||||
modelBuilder.Entity<Chapter>()
|
modelBuilder.Entity<Chapter>()
|
||||||
.HasMany<MangaConnectorId<Chapter>>(c => c.MangaConnectorIds)
|
.HasMany<MangaConnectorId<Chapter>>(c => c.MangaConnectorIds)
|
||||||
.WithOne(id => id.Obj)
|
.WithOne(id => id.Obj)
|
||||||
.HasForeignKey(id => id.ObjId)
|
.HasForeignKey(id => id.ObjId)
|
||||||
|
.OnDelete(DeleteBehavior.NoAction);
|
||||||
|
modelBuilder.Entity<MangaConnectorId<Chapter>>()
|
||||||
|
.HasOne<MangaConnector>(id => id.MangaConnector)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey(id => id.MangaConnectorName)
|
||||||
.OnDelete(DeleteBehavior.Cascade);
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
modelBuilder.Entity<MangaConnectorId<Chapter>>()
|
||||||
|
.Navigation(entry => entry.MangaConnector)
|
||||||
|
.EnableLazyLoading();
|
||||||
//Manga owns MangaAltTitles
|
//Manga owns MangaAltTitles
|
||||||
modelBuilder.Entity<Manga>()
|
modelBuilder.Entity<Manga>()
|
||||||
.OwnsMany<AltTitle>(m => m.AltTitles)
|
.OwnsMany<AltTitle>(m => m.AltTitles)
|
||||||
@@ -81,6 +95,17 @@ public class MangaContext(DbContextOptions<MangaContext> options) : TrangaBaseCo
|
|||||||
.WithOne(id => id.Obj)
|
.WithOne(id => id.Obj)
|
||||||
.HasForeignKey(id => id.ObjId)
|
.HasForeignKey(id => id.ObjId)
|
||||||
.OnDelete(DeleteBehavior.Cascade);
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
modelBuilder.Entity<Manga>()
|
||||||
|
.Navigation(m => m.MangaConnectorIds)
|
||||||
|
.EnableLazyLoading();
|
||||||
|
modelBuilder.Entity<MangaConnectorId<Manga>>()
|
||||||
|
.HasOne<MangaConnector>(id => id.MangaConnector)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey(id => id.MangaConnectorName)
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
modelBuilder.Entity<MangaConnectorId<Manga>>()
|
||||||
|
.Navigation(entry => entry.MangaConnector)
|
||||||
|
.EnableLazyLoading();
|
||||||
|
|
||||||
|
|
||||||
//FileLibrary has many Mangas
|
//FileLibrary has many Mangas
|
||||||
@@ -89,6 +114,9 @@ public class MangaContext(DbContextOptions<MangaContext> options) : TrangaBaseCo
|
|||||||
.WithOne(m => m.Library)
|
.WithOne(m => m.Library)
|
||||||
.HasForeignKey(m => m.LibraryId)
|
.HasForeignKey(m => m.LibraryId)
|
||||||
.OnDelete(DeleteBehavior.SetNull);
|
.OnDelete(DeleteBehavior.SetNull);
|
||||||
|
modelBuilder.Entity<Manga>()
|
||||||
|
.Navigation(m => m.Library)
|
||||||
|
.EnableLazyLoading();
|
||||||
|
|
||||||
modelBuilder.Entity<MetadataFetcher>()
|
modelBuilder.Entity<MetadataFetcher>()
|
||||||
.HasDiscriminator<string>(nameof(MetadataEntry))
|
.HasDiscriminator<string>(nameof(MetadataEntry))
|
||||||
@@ -103,25 +131,4 @@ public class MangaContext(DbContextOptions<MangaContext> options) : TrangaBaseCo
|
|||||||
.WithMany()
|
.WithMany()
|
||||||
.OnDelete(DeleteBehavior.Cascade);
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Manga?> FindMangaLike(Manga other, CancellationToken token)
|
|
||||||
{
|
|
||||||
if (await MangaIncludeAll().FirstOrDefaultAsync(m => m.Key == other.Key, token) 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);
|
|
||||||
}
|
|
||||||
|
|
||||||
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.Identifier = identifier;
|
||||||
this.MetadataFetcherName = metadataFetcherName;
|
this.MetadataFetcherName = metadataFetcherName;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override string ToString() => $"{GetType().FullName} {MangaId} {MetadataFetcherName}";
|
|
||||||
}
|
}
|
@@ -31,5 +31,5 @@ public abstract class MetadataFetcher
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Updates the Manga linked in the MetadataEntry
|
/// Updates the Manga linked in the MetadataEntry
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public abstract Task UpdateMetadata(MetadataEntry metadataEntry, MangaContext dbContext, CancellationToken token);
|
public abstract void UpdateMetadata(MetadataEntry metadataEntry, MangaContext dbContext);
|
||||||
}
|
}
|
@@ -1,7 +1,6 @@
|
|||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using JikanDotNet;
|
using JikanDotNet;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.ChangeTracking;
|
|
||||||
|
|
||||||
namespace API.Schema.MangaContext.MetadataFetchers;
|
namespace API.Schema.MangaContext.MetadataFetchers;
|
||||||
|
|
||||||
@@ -43,49 +42,37 @@ public class MyAnimeList : MetadataFetcher
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="metadataEntry"></param>
|
/// <param name="metadataEntry"></param>
|
||||||
/// <param name="dbContext"></param>
|
/// <param name="dbContext"></param>
|
||||||
/// <param name="token"></param>
|
|
||||||
/// <exception cref="FormatException"></exception>
|
/// <exception cref="FormatException"></exception>
|
||||||
/// <exception cref="DbUpdateException"></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!
|
Manga dbManga = dbContext.Mangas.Find(metadataEntry.MangaId)!;
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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);
|
|
||||||
|
|
||||||
MangaFull resultData;
|
MangaFull resultData;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
long id = long.Parse(metadataEntry.Identifier);
|
long id = long.Parse(metadataEntry.Identifier);
|
||||||
if(await Jikan.GetMangaFullDataAsync(id, token) is not { } response)
|
resultData = Jikan.GetMangaFullDataAsync(id).Result.Data;
|
||||||
throw new DbUpdateException("Manga Data not found");
|
|
||||||
resultData = response.Data;
|
|
||||||
}
|
}
|
||||||
catch (Exception)
|
catch (Exception)
|
||||||
{
|
{
|
||||||
throw new FormatException("ID was not in correct format");
|
throw new FormatException("ID was not in correct format");
|
||||||
}
|
}
|
||||||
|
|
||||||
dbManga.Name = resultData.Titles.First().Title;
|
try
|
||||||
dbManga.Description = resultData.Synopsis;
|
{
|
||||||
dbManga.AltTitles.Clear();
|
dbManga.Name = resultData.Titles.First().Title;
|
||||||
dbManga.AltTitles = resultData.Titles.Select(t => new AltTitle(t.Type, t.Title)).ToList();
|
dbManga.Description = resultData.Synopsis;
|
||||||
dbManga.Authors.Clear();
|
dbManga.AltTitles.Clear();
|
||||||
dbManga.Authors = resultData.Authors.Select(a => new Author(a.Name)).ToList();
|
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,49 +10,74 @@ namespace API.Schema.NotificationsContext.NotificationConnectors;
|
|||||||
[PrimaryKey("Name")]
|
[PrimaryKey("Name")]
|
||||||
public class NotificationConnector(string name, string url, Dictionary<string, string> headers, string httpMethod, string body)
|
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 } }
|
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)
|
public void SendNotification(string title, string notificationText)
|
||||||
{
|
{
|
||||||
Log.Info($"Sending notification: {title} - {notificationText}");
|
Log.Info($"Sending notification: {title} - {notificationText}");
|
||||||
string formattedUrl = FormatStr(Url, title, notificationText);
|
CustomWebhookFormatProvider formatProvider = new (title, notificationText);
|
||||||
string formattedBody = FormatStr(Body, title, notificationText);
|
string formattedUrl = string.Format(formatProvider, Url);
|
||||||
|
string formattedBody = string.Format(formatProvider, Body, title, notificationText);
|
||||||
Dictionary<string, string> formattedHeaders = Headers.ToDictionary(h => h.Key,
|
Dictionary<string, string> formattedHeaders = Headers.ToDictionary(h => h.Key,
|
||||||
h => FormatStr(h.Value, title, notificationText));
|
h => string.Format(formatProvider, h.Value, title, notificationText));
|
||||||
|
|
||||||
HttpRequestMessage request = new(System.Net.Http.HttpMethod.Parse(HttpMethod), formattedUrl);
|
HttpRequestMessage request = new(System.Net.Http.HttpMethod.Parse(HttpMethod), formattedUrl);
|
||||||
foreach (var (key, value) in formattedHeaders)
|
foreach (var (key, value) in formattedHeaders)
|
||||||
request.Headers.Add(key, value);
|
request.Headers.Add(key, value);
|
||||||
request.Content = new StringContent(formattedBody);
|
request.Content = new StringContent(formattedBody);
|
||||||
request.Content.Headers.ContentType = new ("application/json");
|
|
||||||
Log.Debug($"Request: {request}");
|
Log.Debug($"Request: {request}");
|
||||||
|
|
||||||
HttpResponseMessage response = Client.Send(request);
|
HttpResponseMessage response = Client.Send(request);
|
||||||
Log.Debug($"Response status code: {response.StatusCode} {response.Content.ReadAsStringAsync().Result}");
|
Log.Debug($"Response status code: {response.StatusCode}");
|
||||||
}
|
}
|
||||||
|
|
||||||
private string FormatStr(string str, string title, string text)
|
private class CustomWebhookFormatProvider(string title, string text) : IFormatProvider
|
||||||
{
|
{
|
||||||
StringBuilder sb = new (str);
|
public object? GetFormat(Type? formatType)
|
||||||
sb.Replace("%title", title);
|
{
|
||||||
sb.Replace("%text", text);
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
return sb.ToString();
|
public string Format(string fmt, object arg, IFormatProvider provider)
|
||||||
|
{
|
||||||
|
if(arg.GetType() != typeof(string))
|
||||||
|
return arg.ToString() ?? string.Empty;
|
||||||
|
|
||||||
|
StringBuilder sb = new StringBuilder(fmt);
|
||||||
|
sb.Replace("%title", title);
|
||||||
|
sb.Replace("%text", text);
|
||||||
|
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public override string ToString() => $"{GetType().Name} {Name}";
|
public override string ToString() => $"{GetType().Name} {Name}";
|
||||||
|
@@ -22,11 +22,11 @@ public abstract class TrangaBaseContext<T> : DbContext where T : DbContext
|
|||||||
}, Array.Empty<string>(), LogLevel.Warning, DbContextLoggerOptions.Level | DbContextLoggerOptions.Category | DbContextLoggerOptions.UtcTime);
|
}, 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
|
try
|
||||||
{
|
{
|
||||||
await this.SaveChangesAsync(token);
|
this.SaveChanges();
|
||||||
return (true, null);
|
return (true, null);
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
|
@@ -5,8 +5,8 @@ namespace API;
|
|||||||
|
|
||||||
public static class TokenGen
|
public static class TokenGen
|
||||||
{
|
{
|
||||||
public const int MinimumLength = 16;
|
private const int MinimumLength = 16;
|
||||||
public const int MaximumLength = 64;
|
private const int MaximumLength = 64;
|
||||||
private const string Chars = "abcdefghijklmnopqrstuvwxyz0123456789";
|
private const string Chars = "abcdefghijklmnopqrstuvwxyz0123456789";
|
||||||
|
|
||||||
public static string CreateToken(Type t, params string[] identifiers) => CreateToken(t.Name, identifiers);
|
public static string CreateToken(Type t, params string[] identifiers) => CreateToken(t.Name, identifiers);
|
||||||
|
277
API/Tranga.cs
277
API/Tranga.cs
@@ -1,6 +1,4 @@
|
|||||||
using System.Collections.Concurrent;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.Diagnostics.CodeAnalysis;
|
|
||||||
using API.MangaConnectors;
|
|
||||||
using API.Schema.LibraryContext;
|
using API.Schema.LibraryContext;
|
||||||
using API.Schema.MangaContext;
|
using API.Schema.MangaContext;
|
||||||
using API.Schema.MangaContext.MetadataFetchers;
|
using API.Schema.MangaContext.MetadataFetchers;
|
||||||
@@ -9,17 +7,24 @@ using API.Workers;
|
|||||||
using API.Workers.MaintenanceWorkers;
|
using API.Workers.MaintenanceWorkers;
|
||||||
using log4net;
|
using log4net;
|
||||||
using log4net.Config;
|
using log4net.Config;
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
|
|
||||||
namespace API;
|
namespace API;
|
||||||
|
|
||||||
public static class Tranga
|
public static class Tranga
|
||||||
{
|
{
|
||||||
private static IServiceProvider? ServiceProvider;
|
|
||||||
|
|
||||||
|
// ReSharper disable once InconsistentNaming
|
||||||
|
private const string TRANGA =
|
||||||
|
"\n\n" +
|
||||||
|
" _______ v2\n" +
|
||||||
|
"|_ _|.----..---.-..-----..-----..---.-.\n" +
|
||||||
|
" | | | _|| _ || || _ || _ |\n" +
|
||||||
|
" |___| |__| |___._||__|__||___ ||___._|\n" +
|
||||||
|
" |_____| \n\n";
|
||||||
|
|
||||||
|
public static Thread PeriodicWorkerStarterThread { get; } = new (WorkerStarter);
|
||||||
private static readonly ILog Log = LogManager.GetLogger(typeof(Tranga));
|
private static readonly ILog Log = LogManager.GetLogger(typeof(Tranga));
|
||||||
internal static readonly MetadataFetcher[] MetadataFetchers = [new MyAnimeList()];
|
internal static readonly MetadataFetcher[] MetadataFetchers = [new MyAnimeList()];
|
||||||
internal static readonly MangaConnector[] MangaConnectors = [new Global(), new MangaDex(), new ComickIo(), new Mangaworld()];
|
|
||||||
internal static TrangaSettings Settings = TrangaSettings.Load();
|
internal static TrangaSettings Settings = TrangaSettings.Load();
|
||||||
|
|
||||||
internal static readonly UpdateMetadataWorker UpdateMetadataWorker = new ();
|
internal static readonly UpdateMetadataWorker UpdateMetadataWorker = new ();
|
||||||
@@ -29,17 +34,13 @@ public static class Tranga
|
|||||||
internal static readonly CleanupMangaCoversWorker CleanupMangaCoversWorker = new();
|
internal static readonly CleanupMangaCoversWorker CleanupMangaCoversWorker = new();
|
||||||
internal static readonly StartNewChapterDownloadsWorker StartNewChapterDownloadsWorker = new();
|
internal static readonly StartNewChapterDownloadsWorker StartNewChapterDownloadsWorker = new();
|
||||||
internal static readonly RemoveOldNotificationsWorker RemoveOldNotificationsWorker = new();
|
internal static readonly RemoveOldNotificationsWorker RemoveOldNotificationsWorker = new();
|
||||||
internal static readonly UpdateCoversWorker UpdateCoversWorker = new();
|
|
||||||
|
|
||||||
internal static void StartLogger(FileInfo loggerConfigFile)
|
internal static void StartLogger()
|
||||||
{
|
{
|
||||||
XmlConfigurator.ConfigureAndWatch(loggerConfigFile);
|
BasicConfigurator.Configure();
|
||||||
Log.Info("Logger Configured.");
|
Log.Info("Logger Configured.");
|
||||||
Log.Info(Constants.TRANGA);
|
Log.Info(TRANGA);
|
||||||
}
|
|
||||||
|
|
||||||
internal static void AddDefaultWorkers()
|
|
||||||
{
|
|
||||||
AddWorker(UpdateMetadataWorker);
|
AddWorker(UpdateMetadataWorker);
|
||||||
AddWorker(SendNotificationsWorker);
|
AddWorker(SendNotificationsWorker);
|
||||||
AddWorker(UpdateChaptersDownloadedWorker);
|
AddWorker(UpdateChaptersDownloadedWorker);
|
||||||
@@ -47,181 +48,155 @@ public static class Tranga
|
|||||||
AddWorker(CleanupMangaCoversWorker);
|
AddWorker(CleanupMangaCoversWorker);
|
||||||
AddWorker(StartNewChapterDownloadsWorker);
|
AddWorker(StartNewChapterDownloadsWorker);
|
||||||
AddWorker(RemoveOldNotificationsWorker);
|
AddWorker(RemoveOldNotificationsWorker);
|
||||||
AddWorker(UpdateCoversWorker);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static void SetServiceProvider(IServiceProvider serviceProvider)
|
internal static HashSet<BaseWorker> AllWorkers { get; private set; } = new ();
|
||||||
{
|
public static void AddWorker(BaseWorker worker) => AllWorkers.Add(worker);
|
||||||
ServiceProvider = serviceProvider;
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static bool TryGetMangaConnector(string name, [NotNullWhen(true)]out MangaConnector? mangaConnector)
|
|
||||||
{
|
|
||||||
mangaConnector =
|
|
||||||
MangaConnectors.FirstOrDefault(c => c.Name.Equals(name, StringComparison.InvariantCultureIgnoreCase));
|
|
||||||
return mangaConnector != null;
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static readonly ConcurrentDictionary<IPeriodic, Task> PeriodicWorkers = new ();
|
|
||||||
|
|
||||||
public static void AddWorker(BaseWorker worker)
|
|
||||||
{
|
|
||||||
Log.Debug($"Adding Worker {worker}");
|
|
||||||
StartWorker(worker);
|
|
||||||
if(worker is IPeriodic periodic)
|
|
||||||
AddPeriodicWorker(worker, periodic);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void AddPeriodicWorker(BaseWorker worker, IPeriodic periodic)
|
|
||||||
{
|
|
||||||
Log.Debug($"Adding Periodic {worker}");
|
|
||||||
Task periodicTask = PeriodicTask(worker, periodic);
|
|
||||||
PeriodicWorkers.TryAdd((worker as IPeriodic)!, periodicTask);
|
|
||||||
periodicTask.Start();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Task PeriodicTask(BaseWorker worker, IPeriodic periodic) => new (() =>
|
|
||||||
{
|
|
||||||
Log.Debug($"Waiting {periodic.Interval} for next run of {worker}");
|
|
||||||
Thread.Sleep(periodic.Interval);
|
|
||||||
StartWorker(worker, RefreshTask(worker, periodic));
|
|
||||||
});
|
|
||||||
|
|
||||||
private static Action RefreshTask(BaseWorker worker, IPeriodic periodic) => () =>
|
|
||||||
{
|
|
||||||
if (worker.State < WorkerExecutionState.Created) //Failed
|
|
||||||
return;
|
|
||||||
Log.Debug($"Refreshing {worker}");
|
|
||||||
Task periodicTask = PeriodicTask(worker, periodic);
|
|
||||||
PeriodicWorkers.AddOrUpdate((worker as IPeriodic)!, periodicTask, (_, _) => periodicTask);
|
|
||||||
periodicTask.Start();
|
|
||||||
};
|
|
||||||
|
|
||||||
public static void AddWorkers(IEnumerable<BaseWorker> workers)
|
public static void AddWorkers(IEnumerable<BaseWorker> workers)
|
||||||
{
|
{
|
||||||
foreach (BaseWorker baseWorker in workers)
|
foreach (BaseWorker baseWorker in workers)
|
||||||
|
{
|
||||||
AddWorker(baseWorker);
|
AddWorker(baseWorker);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static readonly ConcurrentDictionary<BaseWorker, Task<BaseWorker[]>> RunningWorkers = new();
|
public static void RemoveWorker(BaseWorker removeWorker)
|
||||||
public static BaseWorker[] GetRunningWorkers() => RunningWorkers.Keys.ToArray();
|
|
||||||
|
|
||||||
internal static void StartWorker(BaseWorker worker, Action? callback = null)
|
|
||||||
{
|
{
|
||||||
Log.Debug($"Starting {worker}");
|
IEnumerable<BaseWorker> baseWorkers = AllWorkers.Where(w => w.DependenciesAndSelf.Any(worker => worker == removeWorker));
|
||||||
if (ServiceProvider is null)
|
|
||||||
|
foreach (BaseWorker worker in baseWorkers)
|
||||||
{
|
{
|
||||||
Log.Fatal("ServiceProvider is null");
|
StopWorker(worker);
|
||||||
|
AllWorkers.Remove(worker);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static readonly Dictionary<BaseWorker, Task<BaseWorker[]>> RunningWorkers = new();
|
||||||
|
public static BaseWorker[] GetRunningWorkers() => RunningWorkers.Keys.ToArray();
|
||||||
|
private static readonly HashSet<BaseWorker> StartWorkers = new();
|
||||||
|
private static void WorkerStarter(object? serviceProviderObj)
|
||||||
|
{
|
||||||
|
Log.Info("WorkerStarter Thread running.");
|
||||||
|
if (serviceProviderObj is null)
|
||||||
|
{
|
||||||
|
Log.Error("serviceProviderObj is null");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Action afterWorkCallback = AfterWork(worker, callback);
|
IServiceProvider serviceProvider = (IServiceProvider)serviceProviderObj;
|
||||||
|
|
||||||
while (RunningWorkers.Count > Settings.MaxConcurrentWorkers)
|
while (true)
|
||||||
{
|
{
|
||||||
Log.Warn($"{worker}: Max worker concurrency reached ({Settings.MaxConcurrentWorkers})! Waiting {Settings.WorkCycleTimeoutMs}ms...");
|
CheckRunningWorkers();
|
||||||
|
|
||||||
|
foreach (BaseWorker baseWorker in AllWorkers.DueWorkers())
|
||||||
|
StartWorkers.Add(baseWorker);
|
||||||
|
|
||||||
|
foreach (BaseWorker worker in StartWorkers.ToArray())
|
||||||
|
{
|
||||||
|
if(RunningWorkers.ContainsKey(worker))
|
||||||
|
continue;
|
||||||
|
if (worker is BaseWorkerWithContext<MangaContext> mangaContextWorker)
|
||||||
|
{
|
||||||
|
mangaContextWorker.SetScope(serviceProvider.CreateScope());
|
||||||
|
RunningWorkers.Add(mangaContextWorker, mangaContextWorker.DoWork());
|
||||||
|
}else if (worker is BaseWorkerWithContext<NotificationsContext> notificationContextWorker)
|
||||||
|
{
|
||||||
|
notificationContextWorker.SetScope(serviceProvider.CreateScope());
|
||||||
|
RunningWorkers.Add(notificationContextWorker, notificationContextWorker.DoWork());
|
||||||
|
}else if (worker is BaseWorkerWithContext<LibraryContext> libraryContextWorker)
|
||||||
|
{
|
||||||
|
libraryContextWorker.SetScope(serviceProvider.CreateScope());
|
||||||
|
RunningWorkers.Add(libraryContextWorker, libraryContextWorker.DoWork());
|
||||||
|
}else
|
||||||
|
RunningWorkers.Add(worker, worker.DoWork());
|
||||||
|
|
||||||
|
StartWorkers.Remove(worker);
|
||||||
|
}
|
||||||
Thread.Sleep(Settings.WorkCycleTimeoutMs);
|
Thread.Sleep(Settings.WorkCycleTimeoutMs);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (worker is BaseWorkerWithContext<MangaContext> mangaContextWorker)
|
|
||||||
{
|
|
||||||
mangaContextWorker.SetScope(ServiceProvider.CreateScope());
|
|
||||||
RunningWorkers.TryAdd(mangaContextWorker, mangaContextWorker.DoWork(afterWorkCallback));
|
|
||||||
}else if (worker is BaseWorkerWithContext<NotificationsContext> notificationContextWorker)
|
|
||||||
{
|
|
||||||
notificationContextWorker.SetScope(ServiceProvider.CreateScope());
|
|
||||||
RunningWorkers.TryAdd(notificationContextWorker, notificationContextWorker.DoWork(afterWorkCallback));
|
|
||||||
}else if (worker is BaseWorkerWithContext<LibraryContext> libraryContextWorker)
|
|
||||||
{
|
|
||||||
libraryContextWorker.SetScope(ServiceProvider.CreateScope());
|
|
||||||
RunningWorkers.TryAdd(libraryContextWorker, libraryContextWorker.DoWork(afterWorkCallback));
|
|
||||||
}else
|
|
||||||
RunningWorkers.TryAdd(worker, worker.DoWork(afterWorkCallback));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Action AfterWork(BaseWorker worker, Action? callback = null) => () =>
|
private static void CheckRunningWorkers()
|
||||||
{
|
{
|
||||||
Log.Debug($"AfterWork {worker}");
|
KeyValuePair<BaseWorker, Task<BaseWorker[]>>[] done = RunningWorkers.Where(kv => kv.Value.IsCompleted).ToArray();
|
||||||
if (RunningWorkers.TryGetValue(worker, out Task<BaseWorker[]>? task))
|
if (done.Length < 1)
|
||||||
|
return;
|
||||||
|
Log.Info($"Done: {done.Length}");
|
||||||
|
Log.Debug(string.Join("\n", done.Select(d => d.Key.ToString())));
|
||||||
|
foreach ((BaseWorker worker, Task<BaseWorker[]> task) in done)
|
||||||
{
|
{
|
||||||
Log.Debug($"Waiting for Children to exit {worker}");
|
RunningWorkers.Remove(worker);
|
||||||
task.Wait();
|
foreach (BaseWorker newWorker in task.Result)
|
||||||
if (task.IsCompleted)
|
AllWorkers.Add(newWorker);
|
||||||
{
|
task.Dispose();
|
||||||
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();
|
|
||||||
};
|
private static IEnumerable<BaseWorker> DueWorkers(this IEnumerable<BaseWorker> workers)
|
||||||
|
{
|
||||||
|
return workers.Where(w =>
|
||||||
|
{
|
||||||
|
if (w.State is >= WorkerExecutionState.Running and < WorkerExecutionState.Completed)
|
||||||
|
return false;
|
||||||
|
if (w is IPeriodic periodicWorker)
|
||||||
|
return periodicWorker.IsDue;
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static void MarkWorkerForStart(BaseWorker worker) => StartWorkers.Add(worker);
|
||||||
|
|
||||||
internal static void StopWorker(BaseWorker worker)
|
internal static void StopWorker(BaseWorker worker)
|
||||||
{
|
{
|
||||||
Log.Debug($"Stopping {worker}");
|
StartWorkers.Remove(worker);
|
||||||
if(worker is IPeriodic periodicWorker)
|
|
||||||
PeriodicWorkers.Remove(periodicWorker, out _);
|
|
||||||
worker.Cancel();
|
worker.Cancel();
|
||||||
RunningWorkers.Remove(worker, out _);
|
RunningWorkers.Remove(worker);
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static async Task<bool> AddMangaToContext(this MangaContext context, (Manga, MangaConnectorId<Manga>) addManga, CancellationToken token) =>
|
internal static bool AddMangaToContext((Manga, MangaConnectorId<Manga>) addManga, MangaContext context, [NotNullWhen(true)]out Manga? manga) => AddMangaToContext(addManga.Item1, addManga.Item2, context, out manga);
|
||||||
await AddMangaToContext(context, addManga.Item1, addManga.Item2, token);
|
|
||||||
|
|
||||||
internal static async Task<bool> AddMangaToContext(this MangaContext context, Manga addManga, MangaConnectorId<Manga> addMcId,
|
internal static bool AddMangaToContext(Manga addManga, MangaConnectorId<Manga> addMcId, MangaContext context, [NotNullWhen(true)]out Manga? manga)
|
||||||
CancellationToken token)
|
|
||||||
{
|
{
|
||||||
context.ChangeTracker.Clear();
|
manga = context.Mangas.Find(addManga.Key) ?? addManga;
|
||||||
Manga? manga = await context.FindMangaLike(addManga, token);
|
MangaConnectorId<Manga> mcId = context.MangaConnectorToManga.Find(addMcId.Key) ?? addMcId;
|
||||||
if (manga is not null)
|
mcId.Obj = manga;
|
||||||
|
|
||||||
|
IEnumerable<MangaTag> mergedTags = manga.MangaTags.Select(mt =>
|
||||||
{
|
{
|
||||||
foreach (MangaConnectorId<Manga> mcId in addManga.MangaConnectorIds)
|
MangaTag? inDb = context.Tags.Find(mt.Tag);
|
||||||
{
|
return inDb ?? mt;
|
||||||
mcId.Obj = manga;
|
});
|
||||||
mcId.ObjId = manga.Key;
|
manga.MangaTags = mergedTags.ToList();
|
||||||
}
|
|
||||||
manga.MangaTags = manga.MangaTags.UnionBy(addManga.MangaTags, tag => tag.Tag).ToList();
|
|
||||||
manga.Authors = manga.Authors.UnionBy(addManga.Authors, author => author.Key).ToList();
|
|
||||||
manga.Links = manga.Links.UnionBy(addManga.Links, link => link.Key).ToList();
|
|
||||||
manga.AltTitles = manga.AltTitles.UnionBy(addManga.AltTitles, altTitle => altTitle.Key).ToList();
|
|
||||||
manga.Chapters = manga.Chapters.UnionBy(addManga.Chapters, chapter => chapter.Key).ToList();
|
|
||||||
manga.MangaConnectorIds = manga.MangaConnectorIds.UnionBy(addManga.MangaConnectorIds, id => id.MangaConnectorName).ToList();
|
|
||||||
|
|
||||||
addManga = manga;
|
IEnumerable<Author> mergedAuthors = manga.Authors.Select(ma =>
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
{
|
||||||
manga = addManga;
|
Author? inDb = context.Authors.Find(ma.Key);
|
||||||
IEnumerable<MangaTag> mergedTags = manga.MangaTags.Select(mt =>
|
return inDb ?? ma;
|
||||||
{
|
});
|
||||||
MangaTag? inDb = context.Tags.Find(mt.Tag);
|
manga.Authors = mergedAuthors.ToList();
|
||||||
return inDb ?? mt;
|
|
||||||
});
|
|
||||||
manga.MangaTags = mergedTags.ToList();
|
|
||||||
|
|
||||||
IEnumerable<Author> mergedAuthors = manga.Authors.Select(ma =>
|
if(context.MangaConnectorToManga.Find(addMcId.Key) is null)
|
||||||
{
|
context.MangaConnectorToManga.Add(mcId);
|
||||||
Author? inDb = context.Authors.Find(ma.Key);
|
|
||||||
return inDb ?? ma;
|
|
||||||
});
|
|
||||||
manga.Authors = mergedAuthors.ToList();
|
|
||||||
|
|
||||||
context.Mangas.Add(manga);
|
if (context.Sync() is { success: false })
|
||||||
}
|
|
||||||
|
|
||||||
if (await context.Sync(token) is { success: false })
|
|
||||||
return false;
|
return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
DownloadCoverFromMangaconnectorWorker downloadCoverWorker = new (addMcId);
|
internal static bool AddChapterToContext((Chapter, MangaConnectorId<Chapter>) addChapter, MangaContext context,
|
||||||
AddWorker(downloadCoverWorker);
|
[NotNullWhen(true)] out Chapter? chapter) => AddChapterToContext(addChapter.Item1, addChapter.Item2, context, out chapter);
|
||||||
|
|
||||||
|
internal static bool AddChapterToContext(Chapter addChapter, MangaConnectorId<Chapter> addChId, MangaContext context, [NotNullWhen(true)] out Chapter? chapter)
|
||||||
|
{
|
||||||
|
chapter = context.Chapters.Find(addChapter.Key) ?? addChapter;
|
||||||
|
MangaConnectorId<Chapter> chId = context.MangaConnectorToChapter.Find(addChId.Key) ?? addChId;
|
||||||
|
chId.Obj = chapter;
|
||||||
|
|
||||||
|
if(context.MangaConnectorToChapter.Find(chId.Key) is null)
|
||||||
|
context.MangaConnectorToChapter.Add(chId);
|
||||||
|
|
||||||
|
if (context.Sync() is { success: false })
|
||||||
|
return false;
|
||||||
return true;
|
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");
|
public static string workingDirectory => Path.Join(RuntimeInformation.IsOSPlatform(OSPlatform.Linux) ? "/usr/share" : Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "tranga-api");
|
||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
public static string settingsFilePath => Path.Join(workingDirectory, "settings.json");
|
public static string settingsFilePath => Path.Join(workingDirectory, "settings.json");
|
||||||
[JsonIgnore] public static string coverImageCache => Path.Join(workingDirectory, "imageCache");
|
[JsonIgnore]
|
||||||
[JsonIgnore] public static string coverImageCacheOriginal => Path.Join(coverImageCache, "original");
|
public static string coverImageCache => Path.Join(workingDirectory, "imageCache");
|
||||||
[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");
|
|
||||||
public string DownloadLocation => RuntimeInformation.IsOSPlatform(OSPlatform.Linux) ? "/Manga" : Path.Join(Directory.GetCurrentDirectory(), "Manga");
|
public string DownloadLocation => RuntimeInformation.IsOSPlatform(OSPlatform.Linux) ? "/Manga" : Path.Join(Directory.GetCurrentDirectory(), "Manga");
|
||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
internal static readonly string DefaultUserAgent = $"Tranga/2.0 ({Enum.GetName(Environment.OSVersion.Platform)}; {(Environment.Is64BitOperatingSystem ? "x64" : "")})";
|
internal static readonly string DefaultUserAgent = $"Tranga/2.0 ({Enum.GetName(Environment.OSVersion.Platform)}; {(Environment.Is64BitOperatingSystem ? "x64" : "")})";
|
||||||
@@ -42,21 +39,17 @@ public struct TrangaSettings()
|
|||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
internal static readonly Dictionary<RequestType, int> DefaultRequestLimits = new ()
|
internal static readonly Dictionary<RequestType, int> DefaultRequestLimits = new ()
|
||||||
{
|
{
|
||||||
{RequestType.MangaInfo, 360},
|
{RequestType.MangaInfo, 60},
|
||||||
{RequestType.MangaDexFeed, 360},
|
{RequestType.MangaDexFeed, 60},
|
||||||
{RequestType.MangaDexImage, 60},
|
{RequestType.MangaDexImage, 60},
|
||||||
{RequestType.MangaImage, 240},
|
{RequestType.MangaImage, 240},
|
||||||
{RequestType.MangaCover, 60},
|
{RequestType.MangaCover, 60},
|
||||||
{RequestType.Default, 360}
|
{RequestType.Default, 60}
|
||||||
};
|
};
|
||||||
public Dictionary<RequestType, int> RequestLimits { get; set; } = DefaultRequestLimits;
|
public Dictionary<RequestType, int> RequestLimits { get; set; } = DefaultRequestLimits;
|
||||||
|
|
||||||
public string DownloadLanguage { get; set; } = "en";
|
public string DownloadLanguage { get; set; } = "en";
|
||||||
|
|
||||||
public int MaxConcurrentDownloads { get; set; } = 5;
|
|
||||||
|
|
||||||
public int MaxConcurrentWorkers { get; set; } = 10;
|
|
||||||
|
|
||||||
public static TrangaSettings Load()
|
public static TrangaSettings Load()
|
||||||
{
|
{
|
||||||
if (!File.Exists(settingsFilePath))
|
if (!File.Exists(settingsFilePath))
|
||||||
@@ -66,7 +59,7 @@ public struct TrangaSettings()
|
|||||||
|
|
||||||
public void Save()
|
public void Save()
|
||||||
{
|
{
|
||||||
File.WriteAllText(settingsFilePath, JsonConvert.SerializeObject(this, Formatting.Indented));
|
File.WriteAllText(settingsFilePath, JsonConvert.SerializeObject(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SetUserAgent(string value)
|
public void SetUserAgent(string value)
|
||||||
@@ -116,10 +109,4 @@ public struct TrangaSettings()
|
|||||||
this.DownloadLanguage = language;
|
this.DownloadLanguage = language;
|
||||||
Save();
|
Save();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SetMaxConcurrentDownloads(int value)
|
|
||||||
{
|
|
||||||
this.MaxConcurrentDownloads = value;
|
|
||||||
Save();
|
|
||||||
}
|
|
||||||
}
|
}
|
@@ -8,23 +8,22 @@ public abstract class BaseWorker : Identifiable
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Workers this Worker depends on being completed before running.
|
/// Workers this Worker depends on being completed before running.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private BaseWorker[] DependsOn { get; init; }
|
public BaseWorker[] DependsOn { get; init; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Dependencies and dependencies of dependencies. See also <see cref="DependsOn"/>.
|
/// Dependencies and dependencies of dependencies. See also <see cref="DependsOn"/>.
|
||||||
/// </summary>
|
/// </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>
|
/// <summary>
|
||||||
/// <see cref="AllDependencies"/> and Self.
|
/// <see cref="AllDependencies"/> and Self.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal IEnumerable<BaseWorker> DependenciesAndSelf => AllDependencies.Append(this);
|
public IEnumerable<BaseWorker> DependenciesAndSelf => AllDependencies.Append(this);
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// <see cref="DependsOn"/> where <see cref="WorkerExecutionState"/> is less than Completed.
|
/// <see cref="DependsOn"/> where <see cref="WorkerExecutionState"/> is less than Completed.
|
||||||
/// </summary>
|
/// </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);
|
public bool AllDependenciesFulfilled => DependsOn.All(d => d.State >= WorkerExecutionState.Completed);
|
||||||
internal WorkerExecutionState State { get; private set; }
|
internal WorkerExecutionState State { get; private set; }
|
||||||
private CancellationTokenSource _cancellationTokenSource = new ();
|
private static readonly CancellationTokenSource CancellationTokenSource = new(TimeSpan.FromMinutes(10));
|
||||||
protected CancellationToken CancellationToken => _cancellationTokenSource.Token;
|
|
||||||
protected ILog Log { get; init; }
|
protected ILog Log { get; init; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -34,7 +33,7 @@ public abstract class BaseWorker : Identifiable
|
|||||||
{
|
{
|
||||||
Log.Debug($"Cancelled {this}");
|
Log.Debug($"Cancelled {this}");
|
||||||
this.State = WorkerExecutionState.Cancelled;
|
this.State = WorkerExecutionState.Cancelled;
|
||||||
_cancellationTokenSource.Cancel();
|
CancellationTokenSource.Cancel();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -44,10 +43,10 @@ public abstract class BaseWorker : Identifiable
|
|||||||
{
|
{
|
||||||
Log.Debug($"Failed {this}");
|
Log.Debug($"Failed {this}");
|
||||||
this.State = WorkerExecutionState.Failed;
|
this.State = WorkerExecutionState.Failed;
|
||||||
_cancellationTokenSource.Cancel();
|
CancellationTokenSource.Cancel();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected BaseWorker(IEnumerable<BaseWorker>? dependsOn = null)
|
public BaseWorker(IEnumerable<BaseWorker>? dependsOn = null)
|
||||||
{
|
{
|
||||||
this.DependsOn = dependsOn?.ToArray() ?? [];
|
this.DependsOn = dependsOn?.ToArray() ?? [];
|
||||||
this.Log = LogManager.GetLogger(GetType());
|
this.Log = LogManager.GetLogger(GetType());
|
||||||
@@ -69,14 +68,11 @@ public abstract class BaseWorker : Identifiable
|
|||||||
/// <item>If <see cref="BaseWorker"/> has run, additional <see cref="BaseWorker"/>.</item>
|
/// <item>If <see cref="BaseWorker"/> has run, additional <see cref="BaseWorker"/>.</item>
|
||||||
/// </list>
|
/// </list>
|
||||||
/// </returns>
|
/// </returns>
|
||||||
public Task<BaseWorker[]> DoWork(Action? callback = null)
|
public Task<BaseWorker[]> DoWork()
|
||||||
{
|
{
|
||||||
// Start the worker
|
|
||||||
Log.Debug($"Checking {this}");
|
Log.Debug($"Checking {this}");
|
||||||
this._cancellationTokenSource = new(TimeSpan.FromMinutes(10));
|
|
||||||
this.State = WorkerExecutionState.Waiting;
|
this.State = WorkerExecutionState.Waiting;
|
||||||
|
|
||||||
// Wait for dependencies, start them if necessary
|
|
||||||
BaseWorker[] missingDependenciesThatNeedStarting = MissingDependencies.Where(d => d.State < WorkerExecutionState.Waiting).ToArray();
|
BaseWorker[] missingDependenciesThatNeedStarting = MissingDependencies.Where(d => d.State < WorkerExecutionState.Waiting).ToArray();
|
||||||
if(missingDependenciesThatNeedStarting.Any())
|
if(missingDependenciesThatNeedStarting.Any())
|
||||||
return new Task<BaseWorker[]>(() => missingDependenciesThatNeedStarting);
|
return new Task<BaseWorker[]>(() => missingDependenciesThatNeedStarting);
|
||||||
@@ -84,32 +80,28 @@ public abstract class BaseWorker : Identifiable
|
|||||||
if (MissingDependencies.Any())
|
if (MissingDependencies.Any())
|
||||||
return new Task<BaseWorker[]>(WaitForDependencies);
|
return new Task<BaseWorker[]>(WaitForDependencies);
|
||||||
|
|
||||||
// Run the actual work
|
|
||||||
Log.Info($"Running {this}");
|
Log.Info($"Running {this}");
|
||||||
DateTime startTime = DateTime.UtcNow;
|
DateTime startTime = DateTime.UtcNow;
|
||||||
Task<BaseWorker[]> task = new Task<BaseWorker[]>(() => DoWorkInternal().Result);
|
Task<BaseWorker[]> task = new (DoWorkInternal, CancellationTokenSource.Token);
|
||||||
task.GetAwaiter().OnCompleted(Finish(startTime, callback));
|
task.GetAwaiter().OnCompleted(() =>
|
||||||
this.State = WorkerExecutionState.Running;
|
{
|
||||||
|
DateTime endTime = DateTime.UtcNow;
|
||||||
|
Log.Info($"Completed {this}\n\t{endTime.Subtract(startTime).TotalMilliseconds} ms");
|
||||||
|
this.State = WorkerExecutionState.Completed;
|
||||||
|
if(this is IPeriodic periodic)
|
||||||
|
periodic.LastExecution = DateTime.UtcNow;
|
||||||
|
});
|
||||||
task.Start();
|
task.Start();
|
||||||
|
this.State = WorkerExecutionState.Running;
|
||||||
return task;
|
return task;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Action Finish(DateTime startTime, Action? callback = null) => () =>
|
protected abstract BaseWorker[] DoWorkInternal();
|
||||||
{
|
|
||||||
DateTime endTime = DateTime.UtcNow;
|
|
||||||
Log.Info($"Completed {this}\n\t{endTime.Subtract(startTime).TotalMilliseconds} ms");
|
|
||||||
this.State = WorkerExecutionState.Completed;
|
|
||||||
if(this is IPeriodic periodic)
|
|
||||||
periodic.LastExecution = DateTime.UtcNow;
|
|
||||||
callback?.Invoke();
|
|
||||||
};
|
|
||||||
|
|
||||||
protected abstract Task<BaseWorker[]> DoWorkInternal();
|
|
||||||
|
|
||||||
private BaseWorker[] WaitForDependencies()
|
private BaseWorker[] WaitForDependencies()
|
||||||
{
|
{
|
||||||
Log.Info($"Waiting for {MissingDependencies.Count()} Dependencies {this}:\n\t{string.Join("\n\t", MissingDependencies.Select(d => d.ToString()))}");
|
Log.Info($"Waiting for {MissingDependencies.Count()} Dependencies {this}:\n\t{string.Join("\n\t", MissingDependencies.Select(d => d.ToString()))}");
|
||||||
while (_cancellationTokenSource.IsCancellationRequested == false && MissingDependencies.Any())
|
while (CancellationTokenSource.IsCancellationRequested == false && MissingDependencies.Any())
|
||||||
{
|
{
|
||||||
Thread.Sleep(Tranga.Settings.WorkCycleTimeoutMs);
|
Thread.Sleep(Tranga.Settings.WorkCycleTimeoutMs);
|
||||||
}
|
}
|
||||||
|
@@ -1,10 +1,8 @@
|
|||||||
using System.IO.Compression;
|
using System.IO.Compression;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using API.MangaConnectors;
|
|
||||||
using API.MangaDownloadClients;
|
using API.MangaDownloadClients;
|
||||||
using API.Schema.MangaContext;
|
using API.Schema.MangaContext;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using API.Schema.MangaContext.MangaConnectors;
|
||||||
using Microsoft.EntityFrameworkCore.ChangeTracking;
|
|
||||||
using SixLabors.ImageSharp;
|
using SixLabors.ImageSharp;
|
||||||
using SixLabors.ImageSharp.Formats.Jpeg;
|
using SixLabors.ImageSharp.Formats.Jpeg;
|
||||||
using SixLabors.ImageSharp.Processing;
|
using SixLabors.ImageSharp.Processing;
|
||||||
@@ -13,62 +11,26 @@ using static System.IO.UnixFileMode;
|
|||||||
|
|
||||||
namespace API.Workers;
|
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)
|
public class DownloadChapterFromMangaconnectorWorker(MangaConnectorId<Chapter> chId, IEnumerable<BaseWorker>? dependsOn = null)
|
||||||
: BaseWorkerWithContext<MangaContext>(dependsOn)
|
: BaseWorkerWithContext<MangaContext>(dependsOn)
|
||||||
{
|
{
|
||||||
internal readonly string MangaConnectorIdId = chId.Key;
|
internal readonly string MangaConnectorIdId = chId.Key;
|
||||||
protected override async Task<BaseWorker[]> DoWorkInternal()
|
protected override BaseWorker[] DoWorkInternal()
|
||||||
{
|
{
|
||||||
Log.Debug($"Downloading chapter for MangaConnectorId {MangaConnectorIdId}...");
|
if (DbContext.MangaConnectorToChapter.Find(MangaConnectorIdId) is not { } MangaConnectorId)
|
||||||
// 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.");
|
|
||||||
return []; //TODO Exception?
|
return []; //TODO Exception?
|
||||||
}
|
MangaConnector mangaConnector = MangaConnectorId.MangaConnector;
|
||||||
|
Chapter chapter = MangaConnectorId.Obj;
|
||||||
// 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}...");
|
|
||||||
|
|
||||||
Chapter chapter = mangaConnectorId.Obj;
|
|
||||||
if (chapter.Downloaded)
|
if (chapter.Downloaded)
|
||||||
{
|
{
|
||||||
Log.Info("Chapter was already downloaded.");
|
Log.Info("Chapter was already downloaded.");
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
if (chapter.ParentManga.LibraryId is null)
|
|
||||||
{
|
|
||||||
Log.Info($"Library is not set for {chapter.ParentManga} {chapter}");
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
string[] imageUrls = mangaConnector.GetChapterImageUrls(mangaConnectorId);
|
string[] imageUrls = mangaConnector.GetChapterImageUrls(MangaConnectorId);
|
||||||
if (imageUrls.Length < 1)
|
if (imageUrls.Length < 1)
|
||||||
{
|
{
|
||||||
Log.Info($"No imageUrls for chapter {chapter}");
|
Log.Info($"No imageUrls for chapter {chapter}");
|
||||||
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 [];
|
return [];
|
||||||
}
|
}
|
||||||
string saveArchiveFilePath = chapter.FullArchiveFilePath;
|
string saveArchiveFilePath = chapter.FullArchiveFilePath;
|
||||||
@@ -117,12 +79,10 @@ public class DownloadChapterFromMangaconnectorWorker(MangaConnectorId<Chapter> c
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await CopyCoverFromCacheToDownloadLocation(chapter.ParentManga);
|
CopyCoverFromCacheToDownloadLocation(chapter.ParentManga);
|
||||||
|
|
||||||
Log.Debug($"Creating ComicInfo.xml {chapter}");
|
Log.Debug($"Creating ComicInfo.xml {chapter}");
|
||||||
foreach (CollectionEntry collectionEntry in DbContext.Entry(chapter.ParentManga).Collections)
|
File.WriteAllText(Path.Join(tempFolder, "ComicInfo.xml"), chapter.GetComicInfoXmlString());
|
||||||
await collectionEntry.LoadAsync(CancellationToken);
|
|
||||||
await File.WriteAllTextAsync(Path.Join(tempFolder, "ComicInfo.xml"), chapter.GetComicInfoXmlString(), CancellationToken);
|
|
||||||
|
|
||||||
Log.Debug($"Packaging images to archive {chapter}");
|
Log.Debug($"Packaging images to archive {chapter}");
|
||||||
//ZIP-it and ship-it
|
//ZIP-it and ship-it
|
||||||
@@ -131,11 +91,8 @@ public class DownloadChapterFromMangaconnectorWorker(MangaConnectorId<Chapter> c
|
|||||||
File.SetUnixFileMode(saveArchiveFilePath, UserRead | UserWrite | UserExecute | GroupRead | GroupWrite | GroupExecute | OtherRead | OtherExecute);
|
File.SetUnixFileMode(saveArchiveFilePath, UserRead | UserWrite | UserExecute | GroupRead | GroupWrite | GroupExecute | OtherRead | OtherExecute);
|
||||||
Directory.Delete(tempFolder, true); //Cleanup
|
Directory.Delete(tempFolder, true); //Cleanup
|
||||||
|
|
||||||
DbContext.Entry(chapter).Property(c => c.Downloaded).CurrentValue = true;
|
chapter.Downloaded = true;
|
||||||
if(await DbContext.Sync(CancellationToken) is { success: false } e)
|
DbContext.Sync();
|
||||||
Log.Error($"Failed to save database changes: {e.exceptionMessage}");
|
|
||||||
|
|
||||||
Log.Debug($"Downloaded chapter {chapter}.");
|
|
||||||
|
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
@@ -178,7 +135,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
|
//Check if Publication already has a Folder and cover
|
||||||
string publicationFolder = manga.CreatePublicationFolder();
|
string publicationFolder = manga.CreatePublicationFolder();
|
||||||
@@ -190,29 +147,21 @@ public class DownloadChapterFromMangaconnectorWorker(MangaConnectorId<Chapter> c
|
|||||||
}
|
}
|
||||||
|
|
||||||
//TODO MangaConnector Selection
|
//TODO MangaConnector Selection
|
||||||
await DbContext.Entry(manga).Collection(m => m.MangaConnectorIds).LoadAsync(CancellationToken);
|
MangaConnectorId<Manga> mcId = manga.MangaConnectorIds.First();
|
||||||
MangaConnectorId<Manga> mangaConnectorId = manga.MangaConnectorIds.First();
|
|
||||||
if (!Tranga.TryGetMangaConnector(mangaConnectorId.MangaConnectorName, out MangaConnector? mangaConnector))
|
|
||||||
{
|
|
||||||
Log.Error($"MangaConnector with name {mangaConnectorId.MangaConnectorName} could not be found");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Log.Info($"Copying cover to {publicationFolder}");
|
Log.Info($"Copying cover to {publicationFolder}");
|
||||||
await DbContext.Entry(mangaConnectorId).Navigation(nameof(MangaConnectorId<Manga>.Obj)).LoadAsync(CancellationToken);
|
string? fileInCache = manga.CoverFileNameInCache ?? mcId.MangaConnector.SaveCoverImageToCache(mcId);
|
||||||
string? coverFileNameInCache = manga.CoverFileNameInCache ?? mangaConnector.SaveCoverImageToCache(mangaConnectorId);
|
if (fileInCache is null)
|
||||||
if (coverFileNameInCache is null)
|
|
||||||
{
|
{
|
||||||
Log.Error($"File {coverFileNameInCache} does not exist");
|
Log.Error($"File {fileInCache} does not exist");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
string fullCoverPath = Path.Join(TrangaSettings.coverImageCacheOriginal, coverFileNameInCache);
|
string newFilePath = Path.Join(publicationFolder, $"cover.{Path.GetFileName(fileInCache).Split('.')[^1]}" );
|
||||||
string newFilePath = Path.Join(publicationFolder, $"cover.{Path.GetFileName(coverFileNameInCache).Split('.')[^1]}" );
|
File.Copy(fileInCache, newFilePath, true);
|
||||||
File.Copy(fullCoverPath, newFilePath, true);
|
|
||||||
if(RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
if(RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
||||||
File.SetUnixFileMode(newFilePath, GroupRead | GroupWrite | UserRead | UserWrite | OtherRead | OtherWrite);
|
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)
|
private bool DownloadImage(string imageUrl, string savePath)
|
||||||
|
@@ -1,45 +1,22 @@
|
|||||||
using API.MangaConnectors;
|
|
||||||
using API.Schema.MangaContext;
|
using API.Schema.MangaContext;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using API.Schema.MangaContext.MangaConnectors;
|
||||||
|
|
||||||
namespace API.Workers;
|
namespace API.Workers;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Downloads the cover for Manga from Mangaconnector
|
|
||||||
/// </summary>
|
|
||||||
public class DownloadCoverFromMangaconnectorWorker(MangaConnectorId<Manga> mcId, IEnumerable<BaseWorker>? dependsOn = null)
|
public class DownloadCoverFromMangaconnectorWorker(MangaConnectorId<Manga> mcId, IEnumerable<BaseWorker>? dependsOn = null)
|
||||||
: BaseWorkerWithContext<MangaContext>(dependsOn)
|
: BaseWorkerWithContext<MangaContext>(dependsOn)
|
||||||
{
|
{
|
||||||
internal readonly string MangaConnectorIdId = mcId.Key;
|
internal readonly string MangaConnectorIdId = mcId.Key;
|
||||||
protected override async Task<BaseWorker[]> DoWorkInternal()
|
protected override BaseWorker[] DoWorkInternal()
|
||||||
{
|
{
|
||||||
Log.Debug($"Getting Cover for MangaConnectorId {MangaConnectorIdId}...");
|
if (DbContext.MangaConnectorToManga.Find(MangaConnectorIdId) is not { } MangaConnectorId)
|
||||||
// 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.");
|
|
||||||
return []; //TODO Exception?
|
return []; //TODO Exception?
|
||||||
}
|
MangaConnector mangaConnector = MangaConnectorId.MangaConnector;
|
||||||
if (!Tranga.TryGetMangaConnector(mangaConnectorId.MangaConnectorName, out MangaConnector? mangaConnector))
|
Manga manga = MangaConnectorId.Obj;
|
||||||
{
|
|
||||||
Log.Error("Could not get MangaConnector.");
|
|
||||||
return []; //TODO Exception?
|
|
||||||
}
|
|
||||||
Log.Debug($"Getting Cover for MangaConnectorId {mangaConnectorId}...");
|
|
||||||
|
|
||||||
string? coverFileName = mangaConnector.SaveCoverImageToCache(mangaConnectorId);
|
manga.CoverFileNameInCache = 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;
|
|
||||||
|
|
||||||
if(await DbContext.Sync(CancellationToken) is { success: false } e)
|
|
||||||
Log.Error($"Failed to save database changes: {e.exceptionMessage}");
|
|
||||||
|
|
||||||
|
DbContext.Sync();
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,80 +1,32 @@
|
|||||||
using API.MangaConnectors;
|
|
||||||
using API.Schema.MangaContext;
|
using API.Schema.MangaContext;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using API.Schema.MangaContext.MangaConnectors;
|
||||||
|
|
||||||
namespace API.Workers;
|
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)
|
public class RetrieveMangaChaptersFromMangaconnectorWorker(MangaConnectorId<Manga> mcId, string language, IEnumerable<BaseWorker>? dependsOn = null)
|
||||||
: BaseWorkerWithContext<MangaContext>(dependsOn)
|
: BaseWorkerWithContext<MangaContext>(dependsOn)
|
||||||
{
|
{
|
||||||
internal readonly string MangaConnectorIdId = mcId.Key;
|
internal readonly string MangaConnectorIdId = mcId.Key;
|
||||||
protected override async Task<BaseWorker[]> DoWorkInternal()
|
protected override BaseWorker[] DoWorkInternal()
|
||||||
{
|
{
|
||||||
Log.Debug($"Getting Chapters for MangaConnectorId {MangaConnectorIdId}...");
|
if (DbContext.MangaConnectorToManga.Find(MangaConnectorIdId) is not { } MangaConnectorId)
|
||||||
// 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.");
|
|
||||||
return []; //TODO Exception?
|
return []; //TODO Exception?
|
||||||
}
|
MangaConnector mangaConnector = MangaConnectorId.MangaConnector;
|
||||||
if (!Tranga.TryGetMangaConnector(mangaConnectorId.MangaConnectorName, out MangaConnector? mangaConnector))
|
Manga manga = MangaConnectorId.Obj;
|
||||||
|
// This gets all chapters that are not downloaded
|
||||||
|
(Chapter, MangaConnectorId<Chapter>)[] allChapters =
|
||||||
|
mangaConnector.GetChapters(MangaConnectorId, language).DistinctBy(c => c.Item1.Key).ToArray();
|
||||||
|
|
||||||
|
int addedChapters = 0;
|
||||||
|
foreach ((Chapter chapter, MangaConnectorId<Chapter> mcId) newChapter in allChapters)
|
||||||
{
|
{
|
||||||
Log.Error("Could not get MangaConnector.");
|
if (Tranga.AddChapterToContext(newChapter, DbContext, out Chapter? addedChapter) == false)
|
||||||
return []; //TODO Exception?
|
continue;
|
||||||
|
manga.Chapters.Add(addedChapter);
|
||||||
}
|
}
|
||||||
Log.Debug($"Getting Chapters for MangaConnectorId {mangaConnectorId}...");
|
Log.Info($"{manga.Chapters.Count} existing + {addedChapters} new chapters.");
|
||||||
|
|
||||||
Manga manga = mangaConnectorId.Obj;
|
DbContext.Sync();
|
||||||
|
|
||||||
// Retrieve available Chapters from Connector
|
|
||||||
(Chapter chapter, MangaConnectorId<Chapter> chapterId)[] 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)
|
|
||||||
{
|
|
||||||
foreach ((Chapter _, MangaConnectorId<Chapter> chapterId) in newChapters)
|
|
||||||
{
|
|
||||||
chapterId.UseForDownload = mangaConnectorId.UseForDownload;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if(await DbContext.Sync(CancellationToken) is { success: false } e)
|
|
||||||
Log.Error($"Failed to save database changes: {e.exceptionMessage}");
|
|
||||||
|
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
@@ -6,7 +6,7 @@ public class MoveFileOrFolderWorker(string toLocation, string fromLocation, IEnu
|
|||||||
public readonly string FromLocation = fromLocation;
|
public readonly string FromLocation = fromLocation;
|
||||||
public readonly string ToLocation = toLocation;
|
public readonly string ToLocation = toLocation;
|
||||||
|
|
||||||
protected override Task<BaseWorker[]> DoWorkInternal()
|
protected override BaseWorker[] DoWorkInternal()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -14,13 +14,13 @@ public class MoveFileOrFolderWorker(string toLocation, string fromLocation, IEnu
|
|||||||
if (!fi.Exists)
|
if (!fi.Exists)
|
||||||
{
|
{
|
||||||
Log.Error($"File does not exist at {FromLocation}");
|
Log.Error($"File does not exist at {FromLocation}");
|
||||||
return new Task<BaseWorker[]>(() => []);
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (File.Exists(ToLocation))//Do not override existing
|
if (File.Exists(ToLocation))//Do not override existing
|
||||||
{
|
{
|
||||||
Log.Error($"File already exists at {ToLocation}");
|
Log.Error($"File already exists at {ToLocation}");
|
||||||
return new Task<BaseWorker[]>(() => []);
|
return [];
|
||||||
}
|
}
|
||||||
if(fi.Attributes.HasFlag(FileAttributes.Directory))
|
if(fi.Attributes.HasFlag(FileAttributes.Directory))
|
||||||
MoveDirectory(fi, ToLocation);
|
MoveDirectory(fi, ToLocation);
|
||||||
@@ -32,7 +32,7 @@ public class MoveFileOrFolderWorker(string toLocation, string fromLocation, IEnu
|
|||||||
Log.Error(e);
|
Log.Error(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Task<BaseWorker[]>(() => []);
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
private void MoveDirectory(FileInfo from, string toLocation)
|
private void MoveDirectory(FileInfo from, string toLocation)
|
||||||
|
@@ -1,44 +1,24 @@
|
|||||||
using API.Schema.MangaContext;
|
using API.Schema.MangaContext;
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
|
|
||||||
namespace API.Workers;
|
namespace API.Workers;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Moves a Manga to a different Library
|
|
||||||
/// </summary>
|
|
||||||
public class MoveMangaLibraryWorker(Manga manga, FileLibrary toLibrary, IEnumerable<BaseWorker>? dependsOn = null)
|
public class MoveMangaLibraryWorker(Manga manga, FileLibrary toLibrary, IEnumerable<BaseWorker>? dependsOn = null)
|
||||||
: BaseWorkerWithContext<MangaContext>(dependsOn)
|
: BaseWorkerWithContext<MangaContext>(dependsOn)
|
||||||
{
|
{
|
||||||
internal readonly string MangaId = manga.Key;
|
internal readonly string MangaId = manga.Key;
|
||||||
internal readonly string LibraryId = toLibrary.Key;
|
internal readonly string LibraryId = toLibrary.Key;
|
||||||
protected override async Task<BaseWorker[]> DoWorkInternal()
|
protected override BaseWorker[] DoWorkInternal()
|
||||||
{
|
{
|
||||||
Log.Debug("Moving Manga...");
|
if (DbContext.Mangas.Find(MangaId) is not { } 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.");
|
|
||||||
return []; //TODO Exception?
|
return []; //TODO Exception?
|
||||||
}
|
if (DbContext.FileLibraries.Find(LibraryId) is not { } toLibrary)
|
||||||
// Get new Library
|
|
||||||
if (await DbContext.FileLibraries.FirstOrDefaultAsync(l => l.Key == LibraryId, CancellationToken) is not { } toLibrary)
|
|
||||||
{
|
|
||||||
Log.Error("Could not find Library.");
|
|
||||||
return []; //TODO Exception?
|
return []; //TODO Exception?
|
||||||
}
|
|
||||||
|
|
||||||
// Save old Path (to later move chapters)
|
|
||||||
Dictionary<Chapter, string> oldPath = manga.Chapters.ToDictionary(c => c, c => c.FullArchiveFilePath);
|
Dictionary<Chapter, string> oldPath = manga.Chapters.ToDictionary(c => c, c => c.FullArchiveFilePath);
|
||||||
// Set new Path
|
|
||||||
manga.Library = toLibrary;
|
manga.Library = toLibrary;
|
||||||
|
|
||||||
if (await DbContext.Sync(CancellationToken) is { success: false })
|
if (DbContext.Sync() is { success: false })
|
||||||
return [];
|
return [];
|
||||||
|
|
||||||
// Create Jobs to move chapters from old to new Path
|
|
||||||
return manga.Chapters.Select(c => new MoveFileOrFolderWorker(c.FullArchiveFilePath, oldPath[c])).ToArray<BaseWorker>();
|
return manga.Chapters.Select(c => new MoveFileOrFolderWorker(c.FullArchiveFilePath, oldPath[c])).ToArray<BaseWorker>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,28 +1,20 @@
|
|||||||
using API.Schema.MangaContext;
|
using API.Schema.MangaContext;
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
|
|
||||||
namespace API.Workers;
|
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)
|
public class CheckForNewChaptersWorker(TimeSpan? interval = null, IEnumerable<BaseWorker>? dependsOn = null)
|
||||||
: BaseWorkerWithContext<MangaContext>(dependsOn), IPeriodic
|
: BaseWorkerWithContext<MangaContext>(dependsOn), IPeriodic
|
||||||
{
|
{
|
||||||
public DateTime LastExecution { get; set; } = DateTime.UnixEpoch;
|
public DateTime LastExecution { get; set; } = DateTime.UnixEpoch;
|
||||||
public TimeSpan Interval { get; set; } = interval??TimeSpan.FromMinutes(60);
|
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...");
|
IQueryable<MangaConnectorId<Manga>> connectorIdsManga = DbContext.MangaConnectorToManga.Where(id => id.UseForDownload);
|
||||||
List<MangaConnectorId<Manga>> connectorIdsManga = await DbContext.MangaConnectorToManga
|
|
||||||
.Include(id => id.Obj)
|
|
||||||
.Where(id => id.UseForDownload)
|
|
||||||
.ToListAsync(CancellationToken);
|
|
||||||
Log.Debug($"Creating {connectorIdsManga.Count} update jobs...");
|
|
||||||
|
|
||||||
List<BaseWorker> newWorkers = connectorIdsManga.Select(id => new RetrieveMangaChaptersFromMangaconnectorWorker(id, Tranga.Settings.DownloadLanguage))
|
List<BaseWorker> newWorkers = new();
|
||||||
.ToList<BaseWorker>();
|
foreach (MangaConnectorId<Manga> mangaConnectorId in connectorIdsManga)
|
||||||
|
newWorkers.Add(new RetrieveMangaChaptersFromMangaconnectorWorker(mangaConnectorId, Tranga.Settings.DownloadLanguage));
|
||||||
|
|
||||||
return newWorkers.ToArray();
|
return newWorkers.ToArray();
|
||||||
}
|
}
|
||||||
|
@@ -8,25 +8,14 @@ public class CleanupMangaCoversWorker(TimeSpan? interval = null, IEnumerable<Bas
|
|||||||
public DateTime LastExecution { get; set; } = DateTime.UnixEpoch;
|
public DateTime LastExecution { get; set; } = DateTime.UnixEpoch;
|
||||||
public TimeSpan Interval { get; set; } = interval ?? TimeSpan.FromHours(24);
|
public TimeSpan Interval { get; set; } = interval ?? TimeSpan.FromHours(24);
|
||||||
|
|
||||||
protected override Task<BaseWorker[]> DoWorkInternal()
|
protected override BaseWorker[] DoWorkInternal()
|
||||||
{
|
{
|
||||||
Log.Info("Removing stale files...");
|
Log.Info("Removing stale files...");
|
||||||
|
if (!Directory.Exists(TrangaSettings.coverImageCache))
|
||||||
|
return [];
|
||||||
string[] usedFiles = DbContext.Mangas.Select(m => m.CoverFileNameInCache).Where(s => s != null).ToArray()!;
|
string[] usedFiles = DbContext.Mangas.Select(m => m.CoverFileNameInCache).Where(s => s != null).ToArray()!;
|
||||||
CleanupImageCache(usedFiles, TrangaSettings.coverImageCacheOriginal);
|
string[] extraneousFiles = new DirectoryInfo(TrangaSettings.coverImageCache).GetFiles()
|
||||||
CleanupImageCache(usedFiles, TrangaSettings.coverImageCacheLarge);
|
.Where(f => usedFiles.Contains(f.FullName) == false)
|
||||||
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)
|
|
||||||
.Select(f => f.FullName)
|
.Select(f => f.FullName)
|
||||||
.ToArray();
|
.ToArray();
|
||||||
foreach (string path in extraneousFiles)
|
foreach (string path in extraneousFiles)
|
||||||
@@ -34,5 +23,7 @@ public class CleanupMangaCoversWorker(TimeSpan? interval = null, IEnumerable<Bas
|
|||||||
Log.Info($"Deleting {path}");
|
Log.Info($"Deleting {path}");
|
||||||
File.Delete(path);
|
File.Delete(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -1,27 +1,18 @@
|
|||||||
using API.Schema.NotificationsContext;
|
using API.Schema.NotificationsContext;
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
|
|
||||||
namespace API.Workers.MaintenanceWorkers;
|
namespace API.Workers.MaintenanceWorkers;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Removes sent notifications from database
|
|
||||||
/// </summary>
|
|
||||||
public class RemoveOldNotificationsWorker(TimeSpan? interval = null, IEnumerable<BaseWorker>? dependsOn = null)
|
public class RemoveOldNotificationsWorker(TimeSpan? interval = null, IEnumerable<BaseWorker>? dependsOn = null)
|
||||||
: BaseWorkerWithContext<NotificationsContext>(dependsOn), IPeriodic
|
: BaseWorkerWithContext<NotificationsContext>(dependsOn), IPeriodic
|
||||||
{
|
{
|
||||||
public DateTime LastExecution { get; set; } = DateTime.UnixEpoch;
|
public DateTime LastExecution { get; set; } = DateTime.UnixEpoch;
|
||||||
public TimeSpan Interval { get; set; } = interval ?? TimeSpan.FromHours(1);
|
public TimeSpan Interval { get; set; } = interval ?? TimeSpan.FromHours(1);
|
||||||
|
|
||||||
protected override async Task<BaseWorker[]> DoWorkInternal()
|
protected override BaseWorker[] DoWorkInternal()
|
||||||
{
|
{
|
||||||
Log.Debug("Removing old notifications...");
|
IQueryable<Notification> toRemove = DbContext.Notifications.Where(n => n.IsSent || DateTime.UtcNow - n.Date > Interval);
|
||||||
List<Notification> toRemove = await DbContext.Notifications.Where(n => n.IsSent).ToListAsync(CancellationToken);
|
|
||||||
Log.Debug($"Removing {toRemove.Count} old notifications...");
|
|
||||||
DbContext.RemoveRange(toRemove);
|
DbContext.RemoveRange(toRemove);
|
||||||
|
DbContext.Sync();
|
||||||
if(await DbContext.Sync(CancellationToken) is { success: false } e)
|
|
||||||
Log.Error($"Failed to save database changes: {e.exceptionMessage}");
|
|
||||||
|
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,41 +1,28 @@
|
|||||||
using API.Schema.NotificationsContext;
|
using API.Schema.NotificationsContext;
|
||||||
using API.Schema.NotificationsContext.NotificationConnectors;
|
using API.Schema.NotificationsContext.NotificationConnectors;
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
|
|
||||||
namespace API.Workers;
|
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)
|
public class SendNotificationsWorker(TimeSpan? interval = null, IEnumerable<BaseWorker>? dependsOn = null)
|
||||||
: BaseWorkerWithContext<NotificationsContext>(dependsOn), IPeriodic
|
: BaseWorkerWithContext<NotificationsContext>(dependsOn), IPeriodic
|
||||||
{
|
{
|
||||||
public DateTime LastExecution { get; set; } = DateTime.UnixEpoch;
|
public DateTime LastExecution { get; set; } = DateTime.UnixEpoch;
|
||||||
public TimeSpan Interval { get; set; } = interval??TimeSpan.FromMinutes(1);
|
public TimeSpan Interval { get; set; } = interval??TimeSpan.FromMinutes(1);
|
||||||
protected override async Task<BaseWorker[]> DoWorkInternal()
|
protected override BaseWorker[] DoWorkInternal()
|
||||||
{
|
{
|
||||||
Log.Debug("Sending notifications...");
|
NotificationConnector[] connectors = DbContext.NotificationConnectors.ToArray();
|
||||||
List<NotificationConnector> connectors = await DbContext.NotificationConnectors.ToListAsync(CancellationToken);
|
Notification[] notifications = DbContext.Notifications.Where(n => n.IsSent == false).ToArray();
|
||||||
List<Notification> unsentNotifications = await DbContext.Notifications.Where(n => n.IsSent == false).ToListAsync(CancellationToken);
|
|
||||||
|
|
||||||
Log.Debug($"Sending {unsentNotifications.Count} notifications to {connectors.Count} connectors...");
|
foreach (Notification notification in notifications)
|
||||||
|
|
||||||
unsentNotifications.ForEach(notification =>
|
|
||||||
{
|
{
|
||||||
connectors.ForEach(connector =>
|
foreach (NotificationConnector connector in connectors)
|
||||||
{
|
{
|
||||||
connector.SendNotification(notification.Title, notification.Message);
|
connector.SendNotification(notification.Title, notification.Message);
|
||||||
DbContext.Entry(notification).Property(n => n.IsSent).CurrentValue = true;
|
notification.IsSent = true;
|
||||||
});
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
Log.Debug("Notifications sent.");
|
|
||||||
|
|
||||||
if(await DbContext.Sync(CancellationToken) is { success: false } e)
|
|
||||||
Log.Error($"Failed to save database changes: {e.exceptionMessage}");
|
|
||||||
|
|
||||||
|
DbContext.Sync();
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,38 +1,20 @@
|
|||||||
using API.Schema.MangaContext;
|
using API.Schema.MangaContext;
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
|
|
||||||
namespace API.Workers;
|
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)
|
public class StartNewChapterDownloadsWorker(TimeSpan? interval = null, IEnumerable<BaseWorker>? dependsOn = null)
|
||||||
: BaseWorkerWithContext<MangaContext>(dependsOn), IPeriodic
|
: BaseWorkerWithContext<MangaContext>(dependsOn), IPeriodic
|
||||||
{
|
{
|
||||||
|
|
||||||
public DateTime LastExecution { get; set; } = DateTime.UnixEpoch;
|
public DateTime LastExecution { get; set; } = DateTime.UnixEpoch;
|
||||||
public TimeSpan Interval { get; set; } = interval ?? TimeSpan.FromMinutes(1);
|
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...");
|
IQueryable<MangaConnectorId<Chapter>> mangaConnectorIds = DbContext.MangaConnectorToChapter.Where(id => id.Obj.Downloaded == false && id.UseForDownload);
|
||||||
|
|
||||||
// Get missing chapters
|
List<BaseWorker> newWorkers = new();
|
||||||
List<MangaConnectorId<Chapter>> missingChapters = await DbContext.MangaConnectorToChapter
|
foreach (MangaConnectorId<Chapter> mangaConnectorId in mangaConnectorIds)
|
||||||
.Include(id => id.Obj)
|
newWorkers.Add(new DownloadChapterFromMangaconnectorWorker(mangaConnectorId));
|
||||||
.Where(id => id.Obj.Downloaded == false && id.UseForDownload)
|
|
||||||
.ToListAsync(CancellationToken);
|
|
||||||
|
|
||||||
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>();
|
|
||||||
|
|
||||||
return newWorkers.ToArray();
|
return newWorkers.ToArray();
|
||||||
}
|
}
|
||||||
|
@@ -1,36 +1,17 @@
|
|||||||
using API.Schema.MangaContext;
|
using API.Schema.MangaContext;
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
|
|
||||||
namespace API.Workers;
|
namespace API.Workers;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Updates the database to reflect changes made on disk
|
|
||||||
/// </summary>
|
|
||||||
public class UpdateChaptersDownloadedWorker(TimeSpan? interval = null, IEnumerable<BaseWorker>? dependsOn = null)
|
public class UpdateChaptersDownloadedWorker(TimeSpan? interval = null, IEnumerable<BaseWorker>? dependsOn = null)
|
||||||
: BaseWorkerWithContext<MangaContext>(dependsOn), IPeriodic
|
: BaseWorkerWithContext<MangaContext>(dependsOn), IPeriodic
|
||||||
{
|
{
|
||||||
public DateTime LastExecution { get; set; } = DateTime.UnixEpoch;
|
public DateTime LastExecution { get; set; } = DateTime.UnixEpoch;
|
||||||
public TimeSpan Interval { get; set; } = interval??TimeSpan.FromMinutes(60);
|
public TimeSpan Interval { get; set; } = interval??TimeSpan.FromMinutes(60);
|
||||||
protected override async Task<BaseWorker[]> DoWorkInternal()
|
protected override BaseWorker[] DoWorkInternal()
|
||||||
{
|
{
|
||||||
Log.Debug("Checking chapter files...");
|
foreach (Chapter dbContextChapter in DbContext.Chapters)
|
||||||
List<Chapter> chapters = await DbContext.Chapters.ToListAsync(CancellationToken);
|
dbContextChapter.Downloaded = dbContextChapter.CheckDownloaded();
|
||||||
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}");
|
|
||||||
|
|
||||||
|
DbContext.Sync();
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -1,24 +0,0 @@
|
|||||||
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
|
|
||||||
{
|
|
||||||
|
|
||||||
public DateTime LastExecution { get; set; } = DateTime.UnixEpoch;
|
|
||||||
public TimeSpan Interval { get; set; } = interval ?? TimeSpan.FromHours(6);
|
|
||||||
|
|
||||||
protected override async Task<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();
|
|
||||||
}
|
|
||||||
}
|
|
@@ -4,11 +4,6 @@ using Microsoft.EntityFrameworkCore;
|
|||||||
|
|
||||||
namespace API.Workers;
|
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)
|
public class UpdateMetadataWorker(TimeSpan? interval = null, IEnumerable<BaseWorker>? dependsOn = null)
|
||||||
: BaseWorkerWithContext<MangaContext>(dependsOn), IPeriodic
|
: BaseWorkerWithContext<MangaContext>(dependsOn), IPeriodic
|
||||||
{
|
{
|
||||||
@@ -16,29 +11,20 @@ public class UpdateMetadataWorker(TimeSpan? interval = null, IEnumerable<BaseWor
|
|||||||
public DateTime LastExecution { get; set; } = DateTime.UnixEpoch;
|
public DateTime LastExecution { get; set; } = DateTime.UnixEpoch;
|
||||||
public TimeSpan Interval { get; set; } = interval ?? TimeSpan.FromHours(12);
|
public TimeSpan Interval { get; set; } = interval ?? TimeSpan.FromHours(12);
|
||||||
|
|
||||||
protected override async Task<BaseWorker[]> DoWorkInternal()
|
protected override BaseWorker[] DoWorkInternal()
|
||||||
{
|
{
|
||||||
Log.Debug("Updating metadata...");
|
IQueryable<string> mangaIds = DbContext.MangaConnectorToManga
|
||||||
// Get MetadataEntries of Manga marked for download
|
.Where(m => m.UseForDownload)
|
||||||
List<MetadataEntry> metadataEntriesToUpdate = await DbContext.MangaConnectorToManga
|
.Select(m => m.ObjId);
|
||||||
.Where(m => m.UseForDownload) // Get marked Manga
|
IQueryable<MetadataEntry> metadataEntriesToUpdate = DbContext.MetadataEntries
|
||||||
.Join(
|
.Include(e => e.MetadataFetcher)
|
||||||
DbContext.MetadataEntries.Include(e => e.MetadataFetcher).Include(e => e.Manga),
|
.Where(e =>
|
||||||
mcId => mcId.ObjId,
|
mangaIds.Any(id => id == e.MangaId));
|
||||||
e => e.MangaId,
|
|
||||||
(mcId, e) => e) // return MetadataEntry
|
|
||||||
.ToListAsync(CancellationToken);
|
|
||||||
Log.Debug($"Updating metadata of {metadataEntriesToUpdate.Count} manga...");
|
|
||||||
|
|
||||||
foreach (MetadataEntry metadataEntry in metadataEntriesToUpdate)
|
foreach (MetadataEntry metadataEntry in metadataEntriesToUpdate)
|
||||||
{
|
metadataEntry.MetadataFetcher.UpdateMetadata(metadataEntry, DbContext);
|
||||||
Log.Debug($"Updating metadata of {metadataEntry}...");
|
|
||||||
await metadataEntry.MetadataFetcher.UpdateMetadata(metadataEntry, DbContext, CancellationToken);
|
|
||||||
}
|
|
||||||
Log.Debug("Updated metadata.");
|
|
||||||
|
|
||||||
if(await DbContext.Sync(CancellationToken) is { success: false } e)
|
DbContext.Sync();
|
||||||
Log.Error($"Failed to save database changes: {e.exceptionMessage}");
|
|
||||||
|
|
||||||
return [];
|
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>
|
<element>
|
||||||
<id>UMLClass</id>
|
<id>UMLClass</id>
|
||||||
<coordinates>
|
<coordinates>
|
||||||
<x>930</x>
|
<x>1160</x>
|
||||||
<y>590</y>
|
<y>680</y>
|
||||||
<w>100</w>
|
<w>100</w>
|
||||||
<h>40</h>
|
<h>40</h>
|
||||||
</coordinates>
|
</coordinates>
|
||||||
@@ -15,8 +15,8 @@
|
|||||||
<element>
|
<element>
|
||||||
<id>UMLClass</id>
|
<id>UMLClass</id>
|
||||||
<coordinates>
|
<coordinates>
|
||||||
<x>670</x>
|
<x>900</x>
|
||||||
<y>590</y>
|
<y>680</y>
|
||||||
<w>140</w>
|
<w>140</w>
|
||||||
<h>40</h>
|
<h>40</h>
|
||||||
</coordinates>
|
</coordinates>
|
||||||
@@ -26,53 +26,53 @@
|
|||||||
<element>
|
<element>
|
||||||
<id>UMLClass</id>
|
<id>UMLClass</id>
|
||||||
<coordinates>
|
<coordinates>
|
||||||
<x>250</x>
|
<x>500</x>
|
||||||
<y>710</y>
|
<y>800</y>
|
||||||
<w>120</w>
|
<w>80</w>
|
||||||
<h>40</h>
|
<h>40</h>
|
||||||
</coordinates>
|
</coordinates>
|
||||||
<panel_attributes>/BaseWorker/</panel_attributes>
|
<panel_attributes>/Job/</panel_attributes>
|
||||||
<additional_attributes/>
|
<additional_attributes/>
|
||||||
</element>
|
</element>
|
||||||
<element>
|
<element>
|
||||||
<id>UMLClass</id>
|
<id>UMLClass</id>
|
||||||
<coordinates>
|
<coordinates>
|
||||||
<x>450</x>
|
<x>680</x>
|
||||||
<y>710</y>
|
<y>800</y>
|
||||||
<w>160</w>
|
<w>160</w>
|
||||||
<h>40</h>
|
<h>40</h>
|
||||||
</coordinates>
|
</coordinates>
|
||||||
<panel_attributes>/BaseWorkerWithContext/</panel_attributes>
|
<panel_attributes>/JobWithDownload/</panel_attributes>
|
||||||
<additional_attributes/>
|
<additional_attributes/>
|
||||||
</element>
|
</element>
|
||||||
<element>
|
<element>
|
||||||
<id>UMLClass</id>
|
<id>UMLClass</id>
|
||||||
<coordinates>
|
<coordinates>
|
||||||
<x>440</x>
|
<x>680</x>
|
||||||
<y>590</y>
|
<y>680</y>
|
||||||
<w>180</w>
|
<w>160</w>
|
||||||
<h>40</h>
|
<h>40</h>
|
||||||
</coordinates>
|
</coordinates>
|
||||||
<panel_attributes>DownloadChapterFrom
|
<panel_attributes>RetrieveChaptersJob
|
||||||
MangaConnectorWorker</panel_attributes>
|
</panel_attributes>
|
||||||
<additional_attributes/>
|
<additional_attributes/>
|
||||||
</element>
|
</element>
|
||||||
<element>
|
<element>
|
||||||
<id>Relation</id>
|
<id>Relation</id>
|
||||||
<coordinates>
|
<coordinates>
|
||||||
<x>360</x>
|
<x>570</x>
|
||||||
<y>720</y>
|
<y>810</y>
|
||||||
<w>110</w>
|
<w>130</w>
|
||||||
<h>30</h>
|
<h>30</h>
|
||||||
</coordinates>
|
</coordinates>
|
||||||
<panel_attributes>lt=<<-</panel_attributes>
|
<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>
|
||||||
<element>
|
<element>
|
||||||
<id>Relation</id>
|
<id>Relation</id>
|
||||||
<coordinates>
|
<coordinates>
|
||||||
<x>520</x>
|
<x>750</x>
|
||||||
<y>620</y>
|
<y>710</y>
|
||||||
<w>30</w>
|
<w>30</w>
|
||||||
<h>110</h>
|
<h>110</h>
|
||||||
</coordinates>
|
</coordinates>
|
||||||
@@ -82,8 +82,8 @@ MangaConnectorWorker</panel_attributes>
|
|||||||
<element>
|
<element>
|
||||||
<id>Relation</id>
|
<id>Relation</id>
|
||||||
<coordinates>
|
<coordinates>
|
||||||
<x>940</x>
|
<x>1170</x>
|
||||||
<y>620</y>
|
<y>710</y>
|
||||||
<w>30</w>
|
<w>30</w>
|
||||||
<h>230</h>
|
<h>230</h>
|
||||||
</coordinates>
|
</coordinates>
|
||||||
@@ -93,8 +93,8 @@ MangaConnectorWorker</panel_attributes>
|
|||||||
<element>
|
<element>
|
||||||
<id>Relation</id>
|
<id>Relation</id>
|
||||||
<coordinates>
|
<coordinates>
|
||||||
<x>800</x>
|
<x>1030</x>
|
||||||
<y>730</y>
|
<y>820</y>
|
||||||
<w>150</w>
|
<w>150</w>
|
||||||
<h>140</h>
|
<h>140</h>
|
||||||
</coordinates>
|
</coordinates>
|
||||||
@@ -104,8 +104,8 @@ MangaConnectorWorker</panel_attributes>
|
|||||||
<element>
|
<element>
|
||||||
<id>Relation</id>
|
<id>Relation</id>
|
||||||
<coordinates>
|
<coordinates>
|
||||||
<x>730</x>
|
<x>960</x>
|
||||||
<y>620</y>
|
<y>710</y>
|
||||||
<w>30</w>
|
<w>30</w>
|
||||||
<h>110</h>
|
<h>110</h>
|
||||||
</coordinates>
|
</coordinates>
|
||||||
@@ -115,8 +115,8 @@ MangaConnectorWorker</panel_attributes>
|
|||||||
<element>
|
<element>
|
||||||
<id>Relation</id>
|
<id>Relation</id>
|
||||||
<coordinates>
|
<coordinates>
|
||||||
<x>600</x>
|
<x>830</x>
|
||||||
<y>720</y>
|
<y>810</y>
|
||||||
<w>90</w>
|
<w>90</w>
|
||||||
<h>30</h>
|
<h>30</h>
|
||||||
</coordinates>
|
</coordinates>
|
||||||
@@ -126,8 +126,8 @@ MangaConnectorWorker</panel_attributes>
|
|||||||
<element>
|
<element>
|
||||||
<id>UMLClass</id>
|
<id>UMLClass</id>
|
||||||
<coordinates>
|
<coordinates>
|
||||||
<x>1180</x>
|
<x>1410</x>
|
||||||
<y>590</y>
|
<y>680</y>
|
||||||
<w>100</w>
|
<w>100</w>
|
||||||
<h>40</h>
|
<h>40</h>
|
||||||
</coordinates>
|
</coordinates>
|
||||||
@@ -137,8 +137,8 @@ MangaConnectorWorker</panel_attributes>
|
|||||||
<element>
|
<element>
|
||||||
<id>Relation</id>
|
<id>Relation</id>
|
||||||
<coordinates>
|
<coordinates>
|
||||||
<x>1020</x>
|
<x>1250</x>
|
||||||
<y>600</y>
|
<y>690</y>
|
||||||
<w>180</w>
|
<w>180</w>
|
||||||
<h>30</h>
|
<h>30</h>
|
||||||
</coordinates>
|
</coordinates>
|
||||||
@@ -148,8 +148,8 @@ MangaConnectorWorker</panel_attributes>
|
|||||||
<element>
|
<element>
|
||||||
<id>UMLClass</id>
|
<id>UMLClass</id>
|
||||||
<coordinates>
|
<coordinates>
|
||||||
<x>1180</x>
|
<x>1410</x>
|
||||||
<y>530</y>
|
<y>620</y>
|
||||||
<w>100</w>
|
<w>100</w>
|
||||||
<h>40</h>
|
<h>40</h>
|
||||||
</coordinates>
|
</coordinates>
|
||||||
@@ -159,8 +159,8 @@ MangaConnectorWorker</panel_attributes>
|
|||||||
<element>
|
<element>
|
||||||
<id>UMLClass</id>
|
<id>UMLClass</id>
|
||||||
<coordinates>
|
<coordinates>
|
||||||
<x>1180</x>
|
<x>1410</x>
|
||||||
<y>470</y>
|
<y>560</y>
|
||||||
<w>100</w>
|
<w>100</w>
|
||||||
<h>40</h>
|
<h>40</h>
|
||||||
</coordinates>
|
</coordinates>
|
||||||
@@ -170,8 +170,8 @@ MangaConnectorWorker</panel_attributes>
|
|||||||
<element>
|
<element>
|
||||||
<id>UMLClass</id>
|
<id>UMLClass</id>
|
||||||
<coordinates>
|
<coordinates>
|
||||||
<x>1180</x>
|
<x>1410</x>
|
||||||
<y>410</y>
|
<y>500</y>
|
||||||
<w>100</w>
|
<w>100</w>
|
||||||
<h>40</h>
|
<h>40</h>
|
||||||
</coordinates>
|
</coordinates>
|
||||||
@@ -181,8 +181,8 @@ MangaConnectorWorker</panel_attributes>
|
|||||||
<element>
|
<element>
|
||||||
<id>Relation</id>
|
<id>Relation</id>
|
||||||
<coordinates>
|
<coordinates>
|
||||||
<x>970</x>
|
<x>1200</x>
|
||||||
<y>420</y>
|
<y>510</y>
|
||||||
<w>230</w>
|
<w>230</w>
|
||||||
<h>190</h>
|
<h>190</h>
|
||||||
</coordinates>
|
</coordinates>
|
||||||
@@ -192,8 +192,8 @@ MangaConnectorWorker</panel_attributes>
|
|||||||
<element>
|
<element>
|
||||||
<id>Relation</id>
|
<id>Relation</id>
|
||||||
<coordinates>
|
<coordinates>
|
||||||
<x>1000</x>
|
<x>1230</x>
|
||||||
<y>480</y>
|
<y>570</y>
|
||||||
<w>200</w>
|
<w>200</w>
|
||||||
<h>130</h>
|
<h>130</h>
|
||||||
</coordinates>
|
</coordinates>
|
||||||
@@ -203,8 +203,8 @@ MangaConnectorWorker</panel_attributes>
|
|||||||
<element>
|
<element>
|
||||||
<id>Relation</id>
|
<id>Relation</id>
|
||||||
<coordinates>
|
<coordinates>
|
||||||
<x>1020</x>
|
<x>1250</x>
|
||||||
<y>540</y>
|
<y>630</y>
|
||||||
<w>180</w>
|
<w>180</w>
|
||||||
<h>70</h>
|
<h>70</h>
|
||||||
</coordinates>
|
</coordinates>
|
||||||
@@ -214,8 +214,8 @@ MangaConnectorWorker</panel_attributes>
|
|||||||
<element>
|
<element>
|
||||||
<id>UMLClass</id>
|
<id>UMLClass</id>
|
||||||
<coordinates>
|
<coordinates>
|
||||||
<x>1180</x>
|
<x>1410</x>
|
||||||
<y>350</y>
|
<y>440</y>
|
||||||
<w>100</w>
|
<w>100</w>
|
||||||
<h>40</h>
|
<h>40</h>
|
||||||
</coordinates>
|
</coordinates>
|
||||||
@@ -225,8 +225,8 @@ MangaConnectorWorker</panel_attributes>
|
|||||||
<element>
|
<element>
|
||||||
<id>Relation</id>
|
<id>Relation</id>
|
||||||
<coordinates>
|
<coordinates>
|
||||||
<x>940</x>
|
<x>1170</x>
|
||||||
<y>360</y>
|
<y>450</y>
|
||||||
<w>260</w>
|
<w>260</w>
|
||||||
<h>250</h>
|
<h>250</h>
|
||||||
</coordinates>
|
</coordinates>
|
||||||
@@ -236,8 +236,8 @@ MangaConnectorWorker</panel_attributes>
|
|||||||
<element>
|
<element>
|
||||||
<id>UMLClass</id>
|
<id>UMLClass</id>
|
||||||
<coordinates>
|
<coordinates>
|
||||||
<x>1150</x>
|
<x>1380</x>
|
||||||
<y>710</y>
|
<y>800</y>
|
||||||
<w>160</w>
|
<w>160</w>
|
||||||
<h>40</h>
|
<h>40</h>
|
||||||
</coordinates>
|
</coordinates>
|
||||||
@@ -247,8 +247,8 @@ MangaConnectorWorker</panel_attributes>
|
|||||||
<element>
|
<element>
|
||||||
<id>Relation</id>
|
<id>Relation</id>
|
||||||
<coordinates>
|
<coordinates>
|
||||||
<x>1000</x>
|
<x>1230</x>
|
||||||
<y>620</y>
|
<y>710</y>
|
||||||
<w>170</w>
|
<w>170</w>
|
||||||
<h>130</h>
|
<h>130</h>
|
||||||
</coordinates>
|
</coordinates>
|
||||||
@@ -258,8 +258,8 @@ MangaConnectorWorker</panel_attributes>
|
|||||||
<element>
|
<element>
|
||||||
<id>UMLClass</id>
|
<id>UMLClass</id>
|
||||||
<coordinates>
|
<coordinates>
|
||||||
<x>1400</x>
|
<x>1650</x>
|
||||||
<y>710</y>
|
<y>800</y>
|
||||||
<w>140</w>
|
<w>140</w>
|
||||||
<h>40</h>
|
<h>40</h>
|
||||||
</coordinates>
|
</coordinates>
|
||||||
@@ -269,19 +269,19 @@ MangaConnectorWorker</panel_attributes>
|
|||||||
<element>
|
<element>
|
||||||
<id>Relation</id>
|
<id>Relation</id>
|
||||||
<coordinates>
|
<coordinates>
|
||||||
<x>1300</x>
|
<x>1530</x>
|
||||||
<y>720</y>
|
<y>810</y>
|
||||||
<w>120</w>
|
<w>140</w>
|
||||||
<h>30</h>
|
<h>30</h>
|
||||||
</coordinates>
|
</coordinates>
|
||||||
<panel_attributes>lt=-</panel_attributes>
|
<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>
|
||||||
<element>
|
<element>
|
||||||
<id>UMLUseCase</id>
|
<id>UMLUseCase</id>
|
||||||
<coordinates>
|
<coordinates>
|
||||||
<x>1410</x>
|
<x>1660</x>
|
||||||
<y>590</y>
|
<y>680</y>
|
||||||
<w>120</w>
|
<w>120</w>
|
||||||
<h>40</h>
|
<h>40</h>
|
||||||
</coordinates>
|
</coordinates>
|
||||||
@@ -291,19 +291,19 @@ MangaConnectorWorker</panel_attributes>
|
|||||||
<element>
|
<element>
|
||||||
<id>Relation</id>
|
<id>Relation</id>
|
||||||
<coordinates>
|
<coordinates>
|
||||||
<x>1270</x>
|
<x>1500</x>
|
||||||
<y>600</y>
|
<y>690</y>
|
||||||
<w>160</w>
|
<w>180</w>
|
||||||
<h>30</h>
|
<h>30</h>
|
||||||
</coordinates>
|
</coordinates>
|
||||||
<panel_attributes>lt=-</panel_attributes>
|
<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>
|
||||||
<element>
|
<element>
|
||||||
<id>UMLClass</id>
|
<id>UMLClass</id>
|
||||||
<coordinates>
|
<coordinates>
|
||||||
<x>670</x>
|
<x>900</x>
|
||||||
<y>710</y>
|
<y>800</y>
|
||||||
<w>140</w>
|
<w>140</w>
|
||||||
<h>40</h>
|
<h>40</h>
|
||||||
</coordinates>
|
</coordinates>
|
||||||
@@ -313,8 +313,8 @@ MangaConnectorWorker</panel_attributes>
|
|||||||
<element>
|
<element>
|
||||||
<id>Relation</id>
|
<id>Relation</id>
|
||||||
<coordinates>
|
<coordinates>
|
||||||
<x>800</x>
|
<x>1030</x>
|
||||||
<y>600</y>
|
<y>690</y>
|
||||||
<w>150</w>
|
<w>150</w>
|
||||||
<h>140</h>
|
<h>140</h>
|
||||||
</coordinates>
|
</coordinates>
|
||||||
@@ -324,8 +324,8 @@ MangaConnectorWorker</panel_attributes>
|
|||||||
<element>
|
<element>
|
||||||
<id>UMLClass</id>
|
<id>UMLClass</id>
|
||||||
<coordinates>
|
<coordinates>
|
||||||
<x>930</x>
|
<x>1160</x>
|
||||||
<y>830</y>
|
<y>920</y>
|
||||||
<w>100</w>
|
<w>100</w>
|
||||||
<h>40</h>
|
<h>40</h>
|
||||||
</coordinates>
|
</coordinates>
|
||||||
@@ -336,20 +336,20 @@ MangaConnectorWorker</panel_attributes>
|
|||||||
<element>
|
<element>
|
||||||
<id>UMLClass</id>
|
<id>UMLClass</id>
|
||||||
<coordinates>
|
<coordinates>
|
||||||
<x>230</x>
|
<x>460</x>
|
||||||
<y>590</y>
|
<y>680</y>
|
||||||
<w>160</w>
|
<w>160</w>
|
||||||
<h>40</h>
|
<h>40</h>
|
||||||
</coordinates>
|
</coordinates>
|
||||||
<panel_attributes>MoveFileOrFolder
|
<panel_attributes>UpdateChapters
|
||||||
Worker</panel_attributes>
|
DownloadedJob</panel_attributes>
|
||||||
<additional_attributes/>
|
<additional_attributes/>
|
||||||
</element>
|
</element>
|
||||||
<element>
|
<element>
|
||||||
<id>Relation</id>
|
<id>Relation</id>
|
||||||
<coordinates>
|
<coordinates>
|
||||||
<x>300</x>
|
<x>530</x>
|
||||||
<y>620</y>
|
<y>710</y>
|
||||||
<w>30</w>
|
<w>30</w>
|
||||||
<h>110</h>
|
<h>110</h>
|
||||||
</coordinates>
|
</coordinates>
|
||||||
@@ -359,8 +359,8 @@ Worker</panel_attributes>
|
|||||||
<element>
|
<element>
|
||||||
<id>UMLClass</id>
|
<id>UMLClass</id>
|
||||||
<coordinates>
|
<coordinates>
|
||||||
<x>1630</x>
|
<x>1970</x>
|
||||||
<y>550</y>
|
<y>640</y>
|
||||||
<w>110</w>
|
<w>110</w>
|
||||||
<h>40</h>
|
<h>40</h>
|
||||||
</coordinates>
|
</coordinates>
|
||||||
@@ -371,8 +371,8 @@ Komga</panel_attributes>
|
|||||||
<element>
|
<element>
|
||||||
<id>UMLClass</id>
|
<id>UMLClass</id>
|
||||||
<coordinates>
|
<coordinates>
|
||||||
<x>1630</x>
|
<x>1970</x>
|
||||||
<y>620</y>
|
<y>710</y>
|
||||||
<w>110</w>
|
<w>110</w>
|
||||||
<h>40</h>
|
<h>40</h>
|
||||||
</coordinates>
|
</coordinates>
|
||||||
@@ -383,8 +383,8 @@ Kavita</panel_attributes>
|
|||||||
<element>
|
<element>
|
||||||
<id>UMLPackage</id>
|
<id>UMLPackage</id>
|
||||||
<coordinates>
|
<coordinates>
|
||||||
<x>1570</x>
|
<x>1930</x>
|
||||||
<y>510</y>
|
<y>600</y>
|
||||||
<w>190</w>
|
<w>190</w>
|
||||||
<h>170</h>
|
<h>170</h>
|
||||||
</coordinates>
|
</coordinates>
|
||||||
@@ -394,21 +394,21 @@ Kavita</panel_attributes>
|
|||||||
<element>
|
<element>
|
||||||
<id>Relation</id>
|
<id>Relation</id>
|
||||||
<coordinates>
|
<coordinates>
|
||||||
<x>1520</x>
|
<x>1770</x>
|
||||||
<y>600</y>
|
<y>690</y>
|
||||||
<w>70</w>
|
<w>180</w>
|
||||||
<h>30</h>
|
<h>30</h>
|
||||||
</coordinates>
|
</coordinates>
|
||||||
<panel_attributes>lt=-</panel_attributes>
|
<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>
|
||||||
<element>
|
<element>
|
||||||
<id>UMLClass</id>
|
<id>UMLClass</id>
|
||||||
<coordinates>
|
<coordinates>
|
||||||
<x>480</x>
|
<x>710</x>
|
||||||
<y>820</y>
|
<y>910</y>
|
||||||
<w>100</w>
|
<w>100</w>
|
||||||
<h>40</h>
|
<h>30</h>
|
||||||
</coordinates>
|
</coordinates>
|
||||||
<panel_attributes>/Identifiable/</panel_attributes>
|
<panel_attributes>/Identifiable/</panel_attributes>
|
||||||
<additional_attributes/>
|
<additional_attributes/>
|
||||||
@@ -416,8 +416,8 @@ Kavita</panel_attributes>
|
|||||||
<element>
|
<element>
|
||||||
<id>Relation</id>
|
<id>Relation</id>
|
||||||
<coordinates>
|
<coordinates>
|
||||||
<x>520</x>
|
<x>750</x>
|
||||||
<y>740</y>
|
<y>830</y>
|
||||||
<w>30</w>
|
<w>30</w>
|
||||||
<h>100</h>
|
<h>100</h>
|
||||||
</coordinates>
|
</coordinates>
|
||||||
@@ -427,8 +427,8 @@ Kavita</panel_attributes>
|
|||||||
<element>
|
<element>
|
||||||
<id>Relation</id>
|
<id>Relation</id>
|
||||||
<coordinates>
|
<coordinates>
|
||||||
<x>300</x>
|
<x>530</x>
|
||||||
<y>740</y>
|
<y>830</y>
|
||||||
<w>200</w>
|
<w>200</w>
|
||||||
<h>120</h>
|
<h>120</h>
|
||||||
</coordinates>
|
</coordinates>
|
||||||
@@ -438,92 +438,23 @@ Kavita</panel_attributes>
|
|||||||
<element>
|
<element>
|
||||||
<id>Relation</id>
|
<id>Relation</id>
|
||||||
<coordinates>
|
<coordinates>
|
||||||
<x>520</x>
|
<x>750</x>
|
||||||
<y>850</y>
|
<y>930</y>
|
||||||
<w>480</w>
|
<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>
|
<h>110</h>
|
||||||
</coordinates>
|
</coordinates>
|
||||||
<panel_attributes>lt=<<-</panel_attributes>
|
<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>
|
||||||
<element>
|
<element>
|
||||||
<id>UMLClass</id>
|
<id>Relation</id>
|
||||||
<coordinates>
|
<coordinates>
|
||||||
<x>20</x>
|
<x>800</x>
|
||||||
<y>590</y>
|
<y>670</y>
|
||||||
<w>160</w>
|
<w>380</w>
|
||||||
<h>40</h>
|
<h>280</h>
|
||||||
</coordinates>
|
</coordinates>
|
||||||
<panel_attributes>CheckForNewChapters
|
<panel_attributes>lt=<<-</panel_attributes>
|
||||||
Worker</panel_attributes>
|
<additional_attributes>10.0;260.0;260.0;260.0;260.0;10.0;360.0;10.0</additional_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/>
|
|
||||||
</element>
|
</element>
|
||||||
</diagram>
|
</diagram>
|
||||||
|
12
Dockerfile
12
Dockerfile
@@ -1,8 +1,16 @@
|
|||||||
# syntax=docker/dockerfile:1
|
# syntax=docker/dockerfile:1
|
||||||
ARG DOTNET=9.0
|
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
|
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 install -y libx11-6 libx11-xcb1 libatk1.0-0 libgtk-3-0 libcups2 libdrm2 libxkbcommon0 libxcomposite1 libxdamage1 libxrandr2 libgbm1 libpango-1.0-0 libcairo2 libasound2 libxshmfence1 libnss3 chromium \
|
||||||
|
&& apt-get autopurge -y \
|
||||||
|
&& apt-get autoclean -y
|
||||||
|
|
||||||
FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:$DOTNET AS build-env
|
FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:$DOTNET AS build-env
|
||||||
WORKDIR /src
|
WORKDIR /src
|
||||||
@@ -14,7 +22,7 @@ RUN dotnet restore /src/Tranga.sln
|
|||||||
COPY . /src/
|
COPY . /src/
|
||||||
RUN dotnet publish -c Release --property:OutputPath=/publish -maxcpucount:1
|
RUN dotnet publish -c Release --property:OutputPath=/publish -maxcpucount:1
|
||||||
|
|
||||||
FROM base AS runtime
|
FROM --platform=$TARGETPLATFORM base AS runtime
|
||||||
EXPOSE 6531
|
EXPOSE 6531
|
||||||
ARG UNAME=tranga
|
ARG UNAME=tranga
|
||||||
ARG UID=1000
|
ARG UID=1000
|
||||||
|
678
LICENSE.txt
678
LICENSE.txt
@@ -1,4 +1,674 @@
|
|||||||
Tranga
|
GNU GENERAL PUBLIC LICENSE
|
||||||
Docker-Container to monitor (Manga) Scanlation-Sites for download new chapters.
|
Version 3, 29 June 2007
|
||||||
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/>.
|
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>.
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user