mirror of
https://github.com/Kareadita/Kavita.git
synced 2025-07-09 03:04:19 -04:00
Kavita+ Overhaul & New Changelog (#3507)
This commit is contained in:
parent
d880c1690c
commit
a5707617f2
2
.github/workflows/build-and-test.yml
vendored
2
.github/workflows/build-and-test.yml
vendored
@ -17,7 +17,7 @@ jobs:
|
|||||||
- name: Setup .NET Core
|
- name: Setup .NET Core
|
||||||
uses: actions/setup-dotnet@v4
|
uses: actions/setup-dotnet@v4
|
||||||
with:
|
with:
|
||||||
dotnet-version: 8.0.x
|
dotnet-version: 9.0.x
|
||||||
|
|
||||||
- name: Install Swashbuckle CLI
|
- name: Install Swashbuckle CLI
|
||||||
shell: powershell
|
shell: powershell
|
||||||
|
10
.github/workflows/canary-workflow.yml
vendored
10
.github/workflows/canary-workflow.yml
vendored
@ -9,7 +9,7 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
name: Upload Kavita.Common for Version Bump
|
name: Upload Kavita.Common for Version Bump
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout Repo
|
- name: Checkout Repo
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
@ -24,7 +24,7 @@ jobs:
|
|||||||
version:
|
version:
|
||||||
name: Bump version
|
name: Bump version
|
||||||
needs: [ build ]
|
needs: [ build ]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
@ -33,7 +33,7 @@ jobs:
|
|||||||
- name: Setup .NET Core
|
- name: Setup .NET Core
|
||||||
uses: actions/setup-dotnet@v4
|
uses: actions/setup-dotnet@v4
|
||||||
with:
|
with:
|
||||||
dotnet-version: 8.0.x
|
dotnet-version: 9.0.x
|
||||||
|
|
||||||
- name: Bump versions
|
- name: Bump versions
|
||||||
uses: SiqiLu/dotnet-bump-version@2.0.0
|
uses: SiqiLu/dotnet-bump-version@2.0.0
|
||||||
@ -45,7 +45,7 @@ jobs:
|
|||||||
canary:
|
canary:
|
||||||
name: Build Canary Docker
|
name: Build Canary Docker
|
||||||
needs: [ build, version ]
|
needs: [ build, version ]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-24.04
|
||||||
permissions:
|
permissions:
|
||||||
packages: write
|
packages: write
|
||||||
contents: read
|
contents: read
|
||||||
@ -98,7 +98,7 @@ jobs:
|
|||||||
- name: Compile dotnet app
|
- name: Compile dotnet app
|
||||||
uses: actions/setup-dotnet@v4
|
uses: actions/setup-dotnet@v4
|
||||||
with:
|
with:
|
||||||
dotnet-version: 8.0.x
|
dotnet-version: 9.0.x
|
||||||
|
|
||||||
- name: Install Swashbuckle CLI
|
- name: Install Swashbuckle CLI
|
||||||
run: dotnet tool install -g Swashbuckle.AspNetCore.Cli
|
run: dotnet tool install -g Swashbuckle.AspNetCore.Cli
|
||||||
|
12
.github/workflows/develop-workflow.yml
vendored
12
.github/workflows/develop-workflow.yml
vendored
@ -7,7 +7,7 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
debug:
|
debug:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- name: Debug Info
|
- name: Debug Info
|
||||||
run: |
|
run: |
|
||||||
@ -17,7 +17,7 @@ jobs:
|
|||||||
echo "Matches Develop: ${{ github.ref == 'refs/heads/develop' }}"
|
echo "Matches Develop: ${{ github.ref == 'refs/heads/develop' }}"
|
||||||
build:
|
build:
|
||||||
name: Upload Kavita.Common for Version Bump
|
name: Upload Kavita.Common for Version Bump
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-24.04
|
||||||
if: github.ref == 'refs/heads/develop'
|
if: github.ref == 'refs/heads/develop'
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout Repo
|
- name: Checkout Repo
|
||||||
@ -33,7 +33,7 @@ jobs:
|
|||||||
version:
|
version:
|
||||||
name: Bump version
|
name: Bump version
|
||||||
needs: [ build ]
|
needs: [ build ]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-24.04
|
||||||
if: github.ref == 'refs/heads/develop'
|
if: github.ref == 'refs/heads/develop'
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
@ -43,7 +43,7 @@ jobs:
|
|||||||
- name: Setup .NET Core
|
- name: Setup .NET Core
|
||||||
uses: actions/setup-dotnet@v4
|
uses: actions/setup-dotnet@v4
|
||||||
with:
|
with:
|
||||||
dotnet-version: 8.0.x
|
dotnet-version: 9.0.x
|
||||||
|
|
||||||
- name: Bump versions
|
- name: Bump versions
|
||||||
uses: majora2007/dotnet-bump-version@v0.0.10
|
uses: majora2007/dotnet-bump-version@v0.0.10
|
||||||
@ -55,7 +55,7 @@ jobs:
|
|||||||
develop:
|
develop:
|
||||||
name: Build Nightly Docker
|
name: Build Nightly Docker
|
||||||
needs: [ build, version ]
|
needs: [ build, version ]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-24.04
|
||||||
if: github.ref == 'refs/heads/develop'
|
if: github.ref == 'refs/heads/develop'
|
||||||
permissions:
|
permissions:
|
||||||
packages: write
|
packages: write
|
||||||
@ -128,7 +128,7 @@ jobs:
|
|||||||
- name: Compile dotnet app
|
- name: Compile dotnet app
|
||||||
uses: actions/setup-dotnet@v4
|
uses: actions/setup-dotnet@v4
|
||||||
with:
|
with:
|
||||||
dotnet-version: 8.0.x
|
dotnet-version: 9.0.x
|
||||||
|
|
||||||
- name: Install Swashbuckle CLI
|
- name: Install Swashbuckle CLI
|
||||||
run: dotnet tool install -g Swashbuckle.AspNetCore.Cli
|
run: dotnet tool install -g Swashbuckle.AspNetCore.Cli
|
||||||
|
2
.github/workflows/pr-check.yml
vendored
2
.github/workflows/pr-check.yml
vendored
@ -9,7 +9,7 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
check_pr:
|
check_pr:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- name: Extract branch name
|
- name: Extract branch name
|
||||||
shell: bash
|
shell: bash
|
||||||
|
10
.github/workflows/release-workflow.yml
vendored
10
.github/workflows/release-workflow.yml
vendored
@ -10,7 +10,7 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
debug:
|
debug:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- name: Debug Info
|
- name: Debug Info
|
||||||
run: |
|
run: |
|
||||||
@ -20,13 +20,13 @@ jobs:
|
|||||||
echo "Matches Develop: ${{ github.ref == 'refs/heads/develop' }}"
|
echo "Matches Develop: ${{ github.ref == 'refs/heads/develop' }}"
|
||||||
if_merged:
|
if_merged:
|
||||||
if: github.event.pull_request.merged == true && contains(github.head_ref, 'release')
|
if: github.event.pull_request.merged == true && contains(github.head_ref, 'release')
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- run: |
|
- run: |
|
||||||
echo The PR was merged
|
echo The PR was merged
|
||||||
build:
|
build:
|
||||||
name: Upload Kavita.Common for Version Bump
|
name: Upload Kavita.Common for Version Bump
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-24.04
|
||||||
if: github.event.pull_request.merged == true && contains(github.head_ref, 'release')
|
if: github.event.pull_request.merged == true && contains(github.head_ref, 'release')
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout Repo
|
- name: Checkout Repo
|
||||||
@ -43,7 +43,7 @@ jobs:
|
|||||||
name: Build Stable and Nightly Docker if Release
|
name: Build Stable and Nightly Docker if Release
|
||||||
needs: [ build ]
|
needs: [ build ]
|
||||||
if: github.event.pull_request.merged == true && contains(github.head_ref, 'release')
|
if: github.event.pull_request.merged == true && contains(github.head_ref, 'release')
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-24.04
|
||||||
permissions:
|
permissions:
|
||||||
packages: write
|
packages: write
|
||||||
contents: read
|
contents: read
|
||||||
@ -106,7 +106,7 @@ jobs:
|
|||||||
- name: Compile dotnet app
|
- name: Compile dotnet app
|
||||||
uses: actions/setup-dotnet@v4
|
uses: actions/setup-dotnet@v4
|
||||||
with:
|
with:
|
||||||
dotnet-version: 8.0.x
|
dotnet-version: 9.0.x
|
||||||
- name: Install Swashbuckle CLI
|
- name: Install Swashbuckle CLI
|
||||||
run: dotnet tool install -g Swashbuckle.AspNetCore.Cli
|
run: dotnet tool install -g Swashbuckle.AspNetCore.Cli
|
||||||
|
|
||||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -513,6 +513,7 @@ UI/Web/dist/
|
|||||||
/API/config/stats/
|
/API/config/stats/
|
||||||
/API/config/bookmarks/
|
/API/config/bookmarks/
|
||||||
/API/config/favicons/
|
/API/config/favicons/
|
||||||
|
/API/config/cache-long/
|
||||||
/API/config/kavita.db
|
/API/config/kavita.db
|
||||||
/API/config/kavita.db-shm
|
/API/config/kavita.db-shm
|
||||||
/API/config/kavita.db-wal
|
/API/config/kavita.db-wal
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net8.0</TargetFramework>
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
<OutputType>Exe</OutputType>
|
<OutputType>Exe</OutputType>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
@ -1,22 +1,22 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net8.0</TargetFramework>
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
<IsPackable>false</IsPackable>
|
<IsPackable>false</IsPackable>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="8.0.10" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="9.0.1" />
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
||||||
<PackageReference Include="NSubstitute" Version="5.3.0" />
|
<PackageReference Include="NSubstitute" Version="5.3.0" />
|
||||||
<PackageReference Include="System.IO.Abstractions.TestingHelpers" Version="21.1.3" />
|
<PackageReference Include="System.IO.Abstractions.TestingHelpers" Version="21.2.1" />
|
||||||
<PackageReference Include="TestableIO.System.IO.Abstractions.Wrappers" Version="21.1.3" />
|
<PackageReference Include="TestableIO.System.IO.Abstractions.Wrappers" Version="21.2.1" />
|
||||||
<PackageReference Include="xunit" Version="2.9.2" />
|
<PackageReference Include="xunit" Version="2.9.3" />
|
||||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
|
<PackageReference Include="xunit.runner.visualstudio" Version="3.0.1">
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="coverlet.collector" Version="6.0.2">
|
<PackageReference Include="coverlet.collector" Version="6.0.3">
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<AnalysisMode>Default</AnalysisMode>
|
<AnalysisMode>Default</AnalysisMode>
|
||||||
<TargetFramework>net8.0</TargetFramework>
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
|
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
|
||||||
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
|
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
|
||||||
<TieredPGO>true</TieredPGO>
|
<TieredPGO>true</TieredPGO>
|
||||||
@ -12,10 +12,10 @@
|
|||||||
<LangVersion>latestmajor</LangVersion>
|
<LangVersion>latestmajor</LangVersion>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<!-- <Target Name="PostBuild" AfterTargets="Build" Condition=" '$(Configuration)' == 'Debug' ">-->
|
<Target Name="PostBuild" AfterTargets="Build" Condition=" '$(Configuration)' == 'Debug' ">
|
||||||
<!-- <Delete Files="../openapi.json" />-->
|
<Delete Files="../openapi.json" />
|
||||||
<!-- <Exec Command="swagger tofile --output ../openapi.json bin/$(Configuration)/$(TargetFramework)/$(AssemblyName).dll v1" />-->
|
<Exec Command="swagger tofile --output ../openapi.json bin/$(Configuration)/$(TargetFramework)/$(AssemblyName).dll v1" />
|
||||||
<!-- </Target>-->
|
</Target>
|
||||||
|
|
||||||
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
|
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
|
||||||
<DebugSymbols>false</DebugSymbols>
|
<DebugSymbols>false</DebugSymbols>
|
||||||
@ -55,8 +55,8 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="CsvHelper" Version="33.0.1" />
|
<PackageReference Include="CsvHelper" Version="33.0.1" />
|
||||||
<PackageReference Include="MailKit" Version="4.8.0" />
|
<PackageReference Include="MailKit" Version="4.9.0" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.10">
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.1">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
@ -64,49 +64,49 @@
|
|||||||
<PackageReference Include="Docnet.Core" Version="2.6.0" />
|
<PackageReference Include="Docnet.Core" Version="2.6.0" />
|
||||||
<PackageReference Include="EasyCaching.InMemory" Version="1.9.2" />
|
<PackageReference Include="EasyCaching.InMemory" Version="1.9.2" />
|
||||||
<PackageReference Include="ExCSS" Version="4.3.0" />
|
<PackageReference Include="ExCSS" Version="4.3.0" />
|
||||||
<PackageReference Include="Flurl" Version="3.0.7" />
|
<PackageReference Include="Flurl" Version="4.0.0" />
|
||||||
<PackageReference Include="Flurl.Http" Version="3.2.4" />
|
<PackageReference Include="Flurl.Http" Version="4.0.2" />
|
||||||
<PackageReference Include="Hangfire" Version="1.8.15" />
|
<PackageReference Include="Hangfire" Version="1.8.17" />
|
||||||
<PackageReference Include="Hangfire.InMemory" Version="1.0.0" />
|
<PackageReference Include="Hangfire.InMemory" Version="1.0.0" />
|
||||||
<PackageReference Include="Hangfire.MaximumConcurrentExecutions" Version="1.1.0" />
|
<PackageReference Include="Hangfire.MaximumConcurrentExecutions" Version="1.1.0" />
|
||||||
<PackageReference Include="Hangfire.Storage.SQLite" Version="0.4.2" />
|
<PackageReference Include="Hangfire.Storage.SQLite" Version="0.4.2" />
|
||||||
<PackageReference Include="HtmlAgilityPack" Version="1.11.70" />
|
<PackageReference Include="HtmlAgilityPack" Version="1.11.72" />
|
||||||
<PackageReference Include="MarkdownDeep.NET.Core" Version="1.5.0.4" />
|
<PackageReference Include="MarkdownDeep.NET.Core" Version="1.5.0.4" />
|
||||||
<PackageReference Include="Hangfire.AspNetCore" Version="1.8.15" />
|
<PackageReference Include="Hangfire.AspNetCore" Version="1.8.17" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.SignalR" Version="1.1.0" />
|
<PackageReference Include="Microsoft.AspNetCore.SignalR" Version="1.2.0" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.10" />
|
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.1" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="8.0.10" />
|
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="9.0.1" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.10" />
|
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="9.0.1" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.10" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.1" />
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.1" />
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.1" />
|
||||||
<PackageReference Include="Microsoft.IO.RecyclableMemoryStream" Version="3.0.1" />
|
<PackageReference Include="Microsoft.IO.RecyclableMemoryStream" Version="3.0.1" />
|
||||||
<PackageReference Include="MimeTypeMapOfficial" Version="1.0.17" />
|
<PackageReference Include="MimeTypeMapOfficial" Version="1.0.17" />
|
||||||
<PackageReference Include="Nager.ArticleNumber" Version="1.0.7" />
|
<PackageReference Include="Nager.ArticleNumber" Version="1.0.7" />
|
||||||
<PackageReference Include="NetVips" Version="3.0.0" />
|
<PackageReference Include="NetVips" Version="3.0.0" />
|
||||||
<PackageReference Include="NetVips.Native" Version="8.16.0" />
|
<PackageReference Include="NetVips.Native" Version="8.16.0" />
|
||||||
<PackageReference Include="NReco.Logging.File" Version="1.2.1" />
|
<PackageReference Include="NReco.Logging.File" Version="1.2.2" />
|
||||||
<PackageReference Include="Serilog" Version="4.1.0" />
|
<PackageReference Include="Serilog" Version="4.2.0" />
|
||||||
<PackageReference Include="Serilog.AspNetCore" Version="8.0.3" />
|
<PackageReference Include="Serilog.AspNetCore" Version="9.0.0" />
|
||||||
<PackageReference Include="Serilog.Enrichers.Thread" Version="4.0.0" />
|
<PackageReference Include="Serilog.Enrichers.Thread" Version="4.0.0" />
|
||||||
<PackageReference Include="Serilog.Extensions.Hosting" Version="8.0.0" />
|
<PackageReference Include="Serilog.Extensions.Hosting" Version="9.0.0" />
|
||||||
<PackageReference Include="Serilog.Settings.Configuration" Version="8.0.4" />
|
<PackageReference Include="Serilog.Settings.Configuration" Version="9.0.0" />
|
||||||
<PackageReference Include="Serilog.Sinks.AspNetCore.SignalR" Version="0.4.0" />
|
<PackageReference Include="Serilog.Sinks.AspNetCore.SignalR" Version="0.4.0" />
|
||||||
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
|
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
|
||||||
<PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />
|
<PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />
|
||||||
<PackageReference Include="Serilog.Sinks.SignalR.Core" Version="0.1.2" />
|
<PackageReference Include="Serilog.Sinks.SignalR.Core" Version="0.1.2" />
|
||||||
<PackageReference Include="SharpCompress" Version="0.38.0" />
|
<PackageReference Include="SharpCompress" Version="0.39.0" />
|
||||||
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.5" />
|
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.6" />
|
||||||
<PackageReference Include="SonarAnalyzer.CSharp" Version="10.3.0.106239">
|
<PackageReference Include="SonarAnalyzer.CSharp" Version="10.5.0.109200">
|
||||||
<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="Swashbuckle.AspNetCore" Version="6.9.0" />
|
<PackageReference Include="Swashbuckle.AspNetCore" Version="7.2.0" />
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore.Filters" Version="8.0.2" />
|
<PackageReference Include="Swashbuckle.AspNetCore.Filters" Version="8.0.2" />
|
||||||
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.2.0" />
|
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.3.0" />
|
||||||
<PackageReference Include="System.IO.Abstractions" Version="21.1.3" />
|
<PackageReference Include="System.IO.Abstractions" Version="21.2.1" />
|
||||||
<PackageReference Include="System.Drawing.Common" Version="8.0.10" />
|
<PackageReference Include="System.Drawing.Common" Version="9.0.1" />
|
||||||
<PackageReference Include="VersOne.Epub" Version="3.3.2" />
|
<PackageReference Include="VersOne.Epub" Version="3.3.2" />
|
||||||
<PackageReference Include="YamlDotNet" Version="16.1.3" />
|
<PackageReference Include="YamlDotNet" Version="16.3.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
@ -192,6 +192,7 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<Folder Include="config\cache-long\" />
|
||||||
<Folder Include="config\themes" />
|
<Folder Include="config\themes" />
|
||||||
<Content Include="EmailTemplates\**">
|
<Content Include="EmailTemplates\**">
|
||||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
|
@ -12,6 +12,10 @@ public static class EasyCacheProfiles
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public const string License = "license";
|
public const string License = "license";
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
/// License Information
|
||||||
|
/// </summary>
|
||||||
|
public const string LicenseInfo = "licenseInfo";
|
||||||
|
/// <summary>
|
||||||
/// Cache the libraries on the server
|
/// Cache the libraries on the server
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const string Library = "library";
|
public const string Library = "library";
|
||||||
|
@ -15,6 +15,7 @@ using API.Errors;
|
|||||||
using API.Extensions;
|
using API.Extensions;
|
||||||
using API.Helpers.Builders;
|
using API.Helpers.Builders;
|
||||||
using API.Services;
|
using API.Services;
|
||||||
|
using API.Services.Plus;
|
||||||
using API.SignalR;
|
using API.SignalR;
|
||||||
using AutoMapper;
|
using AutoMapper;
|
||||||
using Hangfire;
|
using Hangfire;
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
using System.Threading.Tasks;
|
using System.Collections.Generic;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using API.Data;
|
||||||
using API.Data.ManualMigrations;
|
using API.Data.ManualMigrations;
|
||||||
|
using API.DTOs;
|
||||||
using API.DTOs.Progress;
|
using API.DTOs.Progress;
|
||||||
using API.Entities;
|
using API.Entities;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
@ -13,10 +16,12 @@ namespace API.Controllers;
|
|||||||
public class AdminController : BaseApiController
|
public class AdminController : BaseApiController
|
||||||
{
|
{
|
||||||
private readonly UserManager<AppUser> _userManager;
|
private readonly UserManager<AppUser> _userManager;
|
||||||
|
private readonly IUnitOfWork _unitOfWork;
|
||||||
|
|
||||||
public AdminController(UserManager<AppUser> userManager)
|
public AdminController(UserManager<AppUser> userManager, IUnitOfWork unitOfWork)
|
||||||
{
|
{
|
||||||
_userManager = userManager;
|
_userManager = userManager;
|
||||||
|
_unitOfWork = unitOfWork;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
26
API/Controllers/EmailController.cs
Normal file
26
API/Controllers/EmailController.cs
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using API.Data;
|
||||||
|
using API.DTOs.Email;
|
||||||
|
using API.Helpers;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace API.Controllers;
|
||||||
|
|
||||||
|
[Authorize(Policy = "RequireAdminRole")]
|
||||||
|
public class EmailController : BaseApiController
|
||||||
|
{
|
||||||
|
private readonly IUnitOfWork _unitOfWork;
|
||||||
|
|
||||||
|
public EmailController(IUnitOfWork unitOfWork)
|
||||||
|
{
|
||||||
|
_unitOfWork = unitOfWork;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("all")]
|
||||||
|
public async Task<ActionResult<IList<EmailHistoryDto>>> GetEmails()
|
||||||
|
{
|
||||||
|
return Ok(await _unitOfWork.EmailHistoryRepository.GetEmailDtos(UserParams.Default));
|
||||||
|
}
|
||||||
|
}
|
@ -2,11 +2,12 @@
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using API.Constants;
|
using API.Constants;
|
||||||
using API.Data;
|
using API.Data;
|
||||||
using API.DTOs.License;
|
using API.DTOs.KavitaPlus.License;
|
||||||
using API.Entities.Enums;
|
using API.Entities.Enums;
|
||||||
using API.Extensions;
|
using API.Extensions;
|
||||||
using API.Services;
|
using API.Services;
|
||||||
using API.Services.Plus;
|
using API.Services.Plus;
|
||||||
|
using Hangfire;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
@ -41,7 +42,7 @@ public class LicenseController(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Has any license
|
/// Has any license registered with the instance. Does not check Kavita+ API
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
[Authorize("RequireAdminRole")]
|
[Authorize("RequireAdminRole")]
|
||||||
@ -53,6 +54,19 @@ public class LicenseController(
|
|||||||
(await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey)).Value));
|
(await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey)).Value));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Asks Kavita+ for the latest license info
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="forceCheck">Force checking the API and skip the 8 hour cache</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
[Authorize("RequireAdminRole")]
|
||||||
|
[HttpGet("info")]
|
||||||
|
[ResponseCache(CacheProfileName = ResponseCacheProfiles.LicenseCache)]
|
||||||
|
public async Task<ActionResult<LicenseInfoDto?>> GetLicenseInfo(bool forceCheck = false)
|
||||||
|
{
|
||||||
|
return Ok(await licenseService.GetLicenseInfo(forceCheck));
|
||||||
|
}
|
||||||
|
|
||||||
[Authorize("RequireAdminRole")]
|
[Authorize("RequireAdminRole")]
|
||||||
[HttpDelete]
|
[HttpDelete]
|
||||||
[ResponseCache(CacheProfileName = ResponseCacheProfiles.LicenseCache)]
|
[ResponseCache(CacheProfileName = ResponseCacheProfiles.LicenseCache)]
|
||||||
@ -67,6 +81,7 @@ public class LicenseController(
|
|||||||
return Ok();
|
return Ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
[Authorize("RequireAdminRole")]
|
[Authorize("RequireAdminRole")]
|
||||||
[HttpPost("reset")]
|
[HttpPost("reset")]
|
||||||
public async Task<ActionResult> ResetLicense(UpdateLicenseDto dto)
|
public async Task<ActionResult> ResetLicense(UpdateLicenseDto dto)
|
||||||
|
40
API/Controllers/ManageController.cs
Normal file
40
API/Controllers/ManageController.cs
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using API.Data;
|
||||||
|
using API.DTOs;
|
||||||
|
using API.DTOs.KavitaPlus.Manage;
|
||||||
|
using API.Services.Plus;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace API.Controllers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// All things centered around Managing the Kavita instance, that isn't aligned with an entity
|
||||||
|
/// </summary>
|
||||||
|
[Authorize("RequireAdminRole")]
|
||||||
|
public class ManageController : BaseApiController
|
||||||
|
{
|
||||||
|
private readonly IUnitOfWork _unitOfWork;
|
||||||
|
private readonly ILicenseService _licenseService;
|
||||||
|
|
||||||
|
public ManageController(IUnitOfWork unitOfWork, ILicenseService licenseService)
|
||||||
|
{
|
||||||
|
_unitOfWork = unitOfWork;
|
||||||
|
_licenseService = licenseService;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns a list of all Series that is Kavita+ applicable to metadata match and the status of it
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
[Authorize("RequireAdminRole")]
|
||||||
|
[HttpPost("series-metadata")]
|
||||||
|
public async Task<ActionResult<IList<ManageMatchSeriesDto>>> SeriesMetadata(ManageMatchFilterDto filter)
|
||||||
|
{
|
||||||
|
if (!await _licenseService.HasActiveLicense()) return Ok(Array.Empty<SeriesDto>());
|
||||||
|
|
||||||
|
return Ok(await _unitOfWork.ExternalSeriesMetadataRepository.GetAllSeries(filter));
|
||||||
|
}
|
||||||
|
}
|
@ -32,12 +32,15 @@ public class MetadataController(IUnitOfWork unitOfWork, ILocalizationService loc
|
|||||||
/// Fetches genres from the instance
|
/// Fetches genres from the instance
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="libraryIds">String separated libraryIds or null for all genres</param>
|
/// <param name="libraryIds">String separated libraryIds or null for all genres</param>
|
||||||
|
/// <param name="context">Context from which this API was invoked</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
[HttpGet("genres")]
|
[HttpGet("genres")]
|
||||||
[ResponseCache(CacheProfileName = ResponseCacheProfiles.Instant, VaryByQueryKeys = ["libraryIds", "context"])]
|
[ResponseCache(CacheProfileName = ResponseCacheProfiles.Instant, VaryByQueryKeys = ["libraryIds", "context"])]
|
||||||
public async Task<ActionResult<IList<GenreTagDto>>> GetAllGenres(string? libraryIds, QueryContext context = QueryContext.None)
|
public async Task<ActionResult<IList<GenreTagDto>>> GetAllGenres(string? libraryIds, QueryContext context = QueryContext.None)
|
||||||
{
|
{
|
||||||
var ids = libraryIds?.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries).Select(int.Parse).ToList();
|
var ids = libraryIds?.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries)
|
||||||
|
.Select(int.Parse)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
return Ok(await unitOfWork.GenreRepository.GetAllGenreDtosForLibrariesAsync(User.GetUserId(), ids, context));
|
return Ok(await unitOfWork.GenreRepository.GetAllGenreDtosForLibrariesAsync(User.GetUserId(), ids, context));
|
||||||
}
|
}
|
||||||
@ -189,12 +192,12 @@ public class MetadataController(IUnitOfWork unitOfWork, ILocalizationService loc
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="seriesId"></param>
|
/// <param name="seriesId"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
[HttpPost("force-refresh")]
|
// [HttpPost("force-refresh")]
|
||||||
public async Task<ActionResult> ForceRefresh(int seriesId)
|
// public async Task<ActionResult> ForceRefresh(int seriesId)
|
||||||
{
|
// {
|
||||||
await metadataService.ForceKavitaPlusRefresh(seriesId);
|
// await metadataService.ForceKavitaPlusRefresh(seriesId);
|
||||||
return Ok();
|
// return Ok();
|
||||||
}
|
// }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Fetches the details needed from Kavita+ for Series Detail page
|
/// Fetches the details needed from Kavita+ for Series Detail page
|
||||||
|
@ -764,6 +764,12 @@ public class OpdsController : BaseApiController
|
|||||||
return CreateXmlResult(SerializeXml(feed));
|
return CreateXmlResult(SerializeXml(feed));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// OPDS Search endpoint
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="apiKey"></param>
|
||||||
|
/// <param name="query"></param>
|
||||||
|
/// <returns></returns>
|
||||||
[HttpGet("{apiKey}/series")]
|
[HttpGet("{apiKey}/series")]
|
||||||
[Produces("application/xml")]
|
[Produces("application/xml")]
|
||||||
public async Task<IActionResult> SearchSeries(string apiKey, [FromQuery] string query)
|
public async Task<IActionResult> SearchSeries(string apiKey, [FromQuery] string query)
|
||||||
@ -781,20 +787,21 @@ public class OpdsController : BaseApiController
|
|||||||
query = query.Replace(@"%", string.Empty);
|
query = query.Replace(@"%", string.Empty);
|
||||||
// Get libraries user has access to
|
// Get libraries user has access to
|
||||||
var libraries = (await _unitOfWork.LibraryRepository.GetLibrariesForUserIdAsync(userId)).ToList();
|
var libraries = (await _unitOfWork.LibraryRepository.GetLibrariesForUserIdAsync(userId)).ToList();
|
||||||
if (!libraries.Any()) return BadRequest(await _localizationService.Translate(userId, "libraries-restricted"));
|
if (libraries.Count == 0) return BadRequest(await _localizationService.Translate(userId, "libraries-restricted"));
|
||||||
|
|
||||||
var isAdmin = await _unitOfWork.UserRepository.IsUserAdminAsync(user);
|
var isAdmin = await _unitOfWork.UserRepository.IsUserAdminAsync(user);
|
||||||
|
|
||||||
var series = await _unitOfWork.SeriesRepository.SearchSeries(userId, isAdmin, libraries.Select(l => l.Id).ToArray(), query);
|
var searchResults = await _unitOfWork.SeriesRepository.SearchSeries(userId, isAdmin,
|
||||||
|
libraries.Select(l => l.Id).ToArray(), query, includeChapterAndFiles: false);
|
||||||
|
|
||||||
var feed = CreateFeed(query, $"{apiKey}/series?query=" + query, apiKey, prefix);
|
var feed = CreateFeed(query, $"{apiKey}/series?query=" + query, apiKey, prefix);
|
||||||
SetFeedId(feed, "search-series");
|
SetFeedId(feed, "search-series");
|
||||||
foreach (var seriesDto in series.Series)
|
foreach (var seriesDto in searchResults.Series)
|
||||||
{
|
{
|
||||||
feed.Entries.Add(CreateSeries(seriesDto, apiKey, prefix, baseUrl));
|
feed.Entries.Add(CreateSeries(seriesDto, apiKey, prefix, baseUrl));
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var collection in series.Collections)
|
foreach (var collection in searchResults.Collections)
|
||||||
{
|
{
|
||||||
feed.Entries.Add(new FeedEntry()
|
feed.Entries.Add(new FeedEntry()
|
||||||
{
|
{
|
||||||
@ -813,7 +820,7 @@ public class OpdsController : BaseApiController
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var readingListDto in series.ReadingLists)
|
foreach (var readingListDto in searchResults.ReadingLists)
|
||||||
{
|
{
|
||||||
feed.Entries.Add(new FeedEntry()
|
feed.Entries.Add(new FeedEntry()
|
||||||
{
|
{
|
||||||
@ -827,6 +834,7 @@ public class OpdsController : BaseApiController
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Search should allow Chapters/Files and more
|
||||||
|
|
||||||
return CreateXmlResult(SerializeXml(feed));
|
return CreateXmlResult(SerializeXml(feed));
|
||||||
}
|
}
|
||||||
|
@ -73,7 +73,7 @@ public class PersonController : BaseApiController
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="dto"></param>
|
/// <param name="dto"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
[Authorize("AdminRequired")]
|
[Authorize("RequireAdminRole")]
|
||||||
[HttpPost("update")]
|
[HttpPost("update")]
|
||||||
public async Task<ActionResult<PersonDto>> UpdatePerson(UpdatePersonDto dto)
|
public async Task<ActionResult<PersonDto>> UpdatePerson(UpdatePersonDto dto)
|
||||||
{
|
{
|
||||||
@ -135,7 +135,11 @@ public class PersonController : BaseApiController
|
|||||||
|
|
||||||
var personImage = await _coverDbService.DownloadPersonImageAsync(person, settings.EncodeMediaAs);
|
var personImage = await _coverDbService.DownloadPersonImageAsync(person, settings.EncodeMediaAs);
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(personImage)) return BadRequest(await _localizationService.Translate(User.GetUserId(), "person-image-doesnt-exist"));
|
if (string.IsNullOrEmpty(personImage))
|
||||||
|
{
|
||||||
|
|
||||||
|
return BadRequest(await _localizationService.Translate(User.GetUserId(), "person-image-doesnt-exist"));
|
||||||
|
}
|
||||||
|
|
||||||
person.CoverImage = personImage;
|
person.CoverImage = personImage;
|
||||||
_imageService.UpdateColorScape(person);
|
_imageService.UpdateColorScape(person);
|
||||||
|
@ -5,6 +5,7 @@ using System.Threading.Tasks;
|
|||||||
using API.Data;
|
using API.Data;
|
||||||
using API.Data.Repositories;
|
using API.Data.Repositories;
|
||||||
using API.DTOs.Account;
|
using API.DTOs.Account;
|
||||||
|
using API.DTOs.KavitaPlus.Account;
|
||||||
using API.DTOs.Scrobbling;
|
using API.DTOs.Scrobbling;
|
||||||
using API.Entities.Scrobble;
|
using API.Entities.Scrobble;
|
||||||
using API.Extensions;
|
using API.Extensions;
|
||||||
@ -73,9 +74,9 @@ public class ScrobblingController : BaseApiController
|
|||||||
/// Update the current user's AniList token
|
/// Update the current user's AniList token
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="dto"></param>
|
/// <param name="dto"></param>
|
||||||
/// <returns></returns>
|
/// <returns>True if the token was new or not</returns>
|
||||||
[HttpPost("update-anilist-token")]
|
[HttpPost("update-anilist-token")]
|
||||||
public async Task<ActionResult> UpdateAniListToken(AniListUpdateDto dto)
|
public async Task<ActionResult<bool>> UpdateAniListToken(AniListUpdateDto dto)
|
||||||
{
|
{
|
||||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
||||||
if (user == null) return Unauthorized();
|
if (user == null) return Unauthorized();
|
||||||
@ -85,31 +86,39 @@ public class ScrobblingController : BaseApiController
|
|||||||
_unitOfWork.UserRepository.Update(user);
|
_unitOfWork.UserRepository.Update(user);
|
||||||
await _unitOfWork.CommitAsync();
|
await _unitOfWork.CommitAsync();
|
||||||
|
|
||||||
if (isNewToken)
|
return Ok(isNewToken);
|
||||||
{
|
|
||||||
BackgroundJob.Enqueue(() => _scrobblingService.CreateEventsFromExistingHistory(user.Id));
|
|
||||||
}
|
|
||||||
|
|
||||||
return Ok();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Update the current user's MAL token (Client ID) and Username
|
/// Update the current user's MAL token (Client ID) and Username
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="dto"></param>
|
/// <param name="dto"></param>
|
||||||
/// <returns></returns>
|
/// <returns>True if the token was new or not</returns>
|
||||||
[HttpPost("update-mal-token")]
|
[HttpPost("update-mal-token")]
|
||||||
public async Task<ActionResult> UpdateMalToken(MalUserInfoDto dto)
|
public async Task<ActionResult<bool>> UpdateMalToken(MalUserInfoDto dto)
|
||||||
{
|
{
|
||||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
||||||
if (user == null) return Unauthorized();
|
if (user == null) return Unauthorized();
|
||||||
|
|
||||||
|
var isNewToken = string.IsNullOrEmpty(user.MalAccessToken);
|
||||||
user.MalAccessToken = dto.AccessToken;
|
user.MalAccessToken = dto.AccessToken;
|
||||||
user.MalUserName = dto.Username;
|
user.MalUserName = dto.Username;
|
||||||
|
|
||||||
_unitOfWork.UserRepository.Update(user);
|
_unitOfWork.UserRepository.Update(user);
|
||||||
await _unitOfWork.CommitAsync();
|
await _unitOfWork.CommitAsync();
|
||||||
|
|
||||||
|
return Ok(isNewToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// When a user request to generate scrobble events from history. Should only be ran once per user.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
[HttpPost("generate-scrobble-events")]
|
||||||
|
public ActionResult GenerateScrobbleEvents()
|
||||||
|
{
|
||||||
|
BackgroundJob.Enqueue(() => _scrobblingService.CreateEventsFromExistingHistory(User.GetUserId()));
|
||||||
|
|
||||||
return Ok();
|
return Ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -9,6 +9,7 @@ using API.DTOs.Dashboard;
|
|||||||
using API.DTOs.Filtering;
|
using API.DTOs.Filtering;
|
||||||
using API.DTOs.Filtering.v2;
|
using API.DTOs.Filtering.v2;
|
||||||
using API.DTOs.Metadata;
|
using API.DTOs.Metadata;
|
||||||
|
using API.DTOs.Metadata.Matching;
|
||||||
using API.DTOs.Recommendation;
|
using API.DTOs.Recommendation;
|
||||||
using API.DTOs.SeriesDetail;
|
using API.DTOs.SeriesDetail;
|
||||||
using API.Entities;
|
using API.Entities;
|
||||||
@ -616,4 +617,42 @@ public class SeriesController : BaseApiController
|
|||||||
return Ok(await _seriesService.GetEstimatedChapterCreationDate(seriesId, userId));
|
return Ok(await _seriesService.GetEstimatedChapterCreationDate(seriesId, userId));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sends a request to Kavita+ API for all potential matches, sorted by relevance
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="dto"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
[HttpPost("match")]
|
||||||
|
public async Task<ActionResult<IList<ExternalSeriesMatchDto>>> MatchSeries(MatchSeriesDto dto)
|
||||||
|
{
|
||||||
|
return Ok(await _externalMetadataService.MatchSeries(dto));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This will perform the fix match
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="dto"></param>
|
||||||
|
/// <param name="seriesId"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
[HttpPost("update-match")]
|
||||||
|
public async Task<ActionResult> UpdateSeriesMatch(ExternalSeriesDetailDto dto, [FromQuery] int seriesId)
|
||||||
|
{
|
||||||
|
await _externalMetadataService.FixSeriesMatch(seriesId, dto);
|
||||||
|
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// When true, will not perform a match and will prevent Kavita from attempting to match/scrobble against this series
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="seriesId"></param>
|
||||||
|
/// <param name="dontMatch"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
[HttpPost("dont-match")]
|
||||||
|
public async Task<ActionResult> UpdateDontMatch([FromQuery] int seriesId, [FromQuery] bool dontMatch)
|
||||||
|
{
|
||||||
|
await _externalMetadataService.UpdateSeriesDontMatch(seriesId, dontMatch);
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -213,15 +213,16 @@ public class ServerController : BaseApiController
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Pull the Changelog for Kavita from Github and display
|
/// Pull the Changelog for Kavita from Github and display
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="count">How many releases from the latest to return</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
[AllowAnonymous]
|
[AllowAnonymous]
|
||||||
[HttpGet("changelog")]
|
[HttpGet("changelog")]
|
||||||
public async Task<ActionResult<IEnumerable<UpdateNotificationDto>>> GetChangelog()
|
public async Task<ActionResult<IEnumerable<UpdateNotificationDto>>> GetChangelog(int count = 0)
|
||||||
{
|
{
|
||||||
// Strange bug where [Authorize] doesn't work
|
// Strange bug where [Authorize] doesn't work
|
||||||
if (User.GetUserId() == 0) return Unauthorized();
|
if (User.GetUserId() == 0) return Unauthorized();
|
||||||
|
|
||||||
return Ok(await _versionUpdaterService.GetAllReleases());
|
return Ok(await _versionUpdaterService.GetAllReleases(count));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -389,7 +389,6 @@ public class SettingsController : BaseApiController
|
|||||||
{
|
{
|
||||||
if (setting.Key == ServerSettingKey.TaskBackup && updateSettingsDto.TaskBackup != setting.Value)
|
if (setting.Key == ServerSettingKey.TaskBackup && updateSettingsDto.TaskBackup != setting.Value)
|
||||||
{
|
{
|
||||||
//if (updateSettingsDto.TotalBackup)
|
|
||||||
setting.Value = updateSettingsDto.TaskBackup;
|
setting.Value = updateSettingsDto.TaskBackup;
|
||||||
_unitOfWork.SettingsRepository.Update(setting);
|
_unitOfWork.SettingsRepository.Update(setting);
|
||||||
|
|
||||||
|
@ -222,18 +222,4 @@ public class StatsController : BaseApiController
|
|||||||
return Ok(_statService.GetWordsReadCountByYear(userId));
|
return Ok(_statService.GetWordsReadCountByYear(userId));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns for Kavita+ the number of Series that have been processed, errored, and not processed
|
|
||||||
/// </summary>
|
|
||||||
/// <returns></returns>
|
|
||||||
[Authorize("RequireAdminRole")]
|
|
||||||
[HttpGet("kavitaplus-metadata-breakdown")]
|
|
||||||
[ResponseCache(CacheProfileName = "Statistics")]
|
|
||||||
public async Task<ActionResult<IEnumerable<StatCount<int>>>> GetKavitaPlusMetadataBreakdown()
|
|
||||||
{
|
|
||||||
if (!await _licenseService.HasActiveLicense())
|
|
||||||
return BadRequest("This data is not available for non-Kavita+ servers");
|
|
||||||
return Ok(await _statService.GetKavitaPlusMetadataBreakdown());
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -5,8 +5,10 @@ using API.Constants;
|
|||||||
using API.Data;
|
using API.Data;
|
||||||
using API.Data.Repositories;
|
using API.Data.Repositories;
|
||||||
using API.DTOs;
|
using API.DTOs;
|
||||||
|
using API.DTOs.KavitaPlus.Account;
|
||||||
using API.Extensions;
|
using API.Extensions;
|
||||||
using API.Services;
|
using API.Services;
|
||||||
|
using API.Services.Plus;
|
||||||
using API.SignalR;
|
using API.SignalR;
|
||||||
using AutoMapper;
|
using AutoMapper;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
@ -23,14 +25,16 @@ public class UsersController : BaseApiController
|
|||||||
private readonly IMapper _mapper;
|
private readonly IMapper _mapper;
|
||||||
private readonly IEventHub _eventHub;
|
private readonly IEventHub _eventHub;
|
||||||
private readonly ILocalizationService _localizationService;
|
private readonly ILocalizationService _localizationService;
|
||||||
|
private readonly ILicenseService _licenseService;
|
||||||
|
|
||||||
public UsersController(IUnitOfWork unitOfWork, IMapper mapper, IEventHub eventHub,
|
public UsersController(IUnitOfWork unitOfWork, IMapper mapper, IEventHub eventHub,
|
||||||
ILocalizationService localizationService)
|
ILocalizationService localizationService, ILicenseService licenseService)
|
||||||
{
|
{
|
||||||
_unitOfWork = unitOfWork;
|
_unitOfWork = unitOfWork;
|
||||||
_mapper = mapper;
|
_mapper = mapper;
|
||||||
_eventHub = eventHub;
|
_eventHub = eventHub;
|
||||||
_localizationService = localizationService;
|
_localizationService = localizationService;
|
||||||
|
_licenseService = licenseService;
|
||||||
}
|
}
|
||||||
|
|
||||||
[Authorize(Policy = "RequireAdminRole")]
|
[Authorize(Policy = "RequireAdminRole")]
|
||||||
@ -173,4 +177,18 @@ public class UsersController : BaseApiController
|
|||||||
{
|
{
|
||||||
return Ok((await _unitOfWork.UserRepository.GetAllUsersAsync()).Select(u => u.UserName));
|
return Ok((await _unitOfWork.UserRepository.GetAllUsersAsync()).Select(u => u.UserName));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns all users with tokens registered and their token information. Does not send the tokens.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>Kavita+ only</remarks>
|
||||||
|
/// <returns></returns>
|
||||||
|
[Authorize(Policy = "RequireAdminRole")]
|
||||||
|
[HttpGet("tokens")]
|
||||||
|
public async Task<ActionResult<IEnumerable<UserTokenInfo>>> GetUserTokens()
|
||||||
|
{
|
||||||
|
if (!await _licenseService.HasActiveLicense()) return BadRequest(_localizationService.Translate(User.GetUserId(), "kavitaplus-restricted"));
|
||||||
|
|
||||||
|
return Ok((await _unitOfWork.UserRepository.GetUserTokenInfo()));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
14
API/DTOs/Email/EmailHistoryDto.cs
Normal file
14
API/DTOs/Email/EmailHistoryDto.cs
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace API.DTOs.Email;
|
||||||
|
|
||||||
|
public class EmailHistoryDto
|
||||||
|
{
|
||||||
|
public long Id { get; set; }
|
||||||
|
public bool Sent { get; set; }
|
||||||
|
public DateTime SendDate { get; set; } = DateTime.UtcNow;
|
||||||
|
public string EmailTemplate { get; set; }
|
||||||
|
public string ErrorMessage { get; set; }
|
||||||
|
public string ToUserName { get; set; }
|
||||||
|
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
namespace API.DTOs.Account;
|
namespace API.DTOs.KavitaPlus.Account;
|
||||||
|
|
||||||
public class AniListUpdateDto
|
public class AniListUpdateDto
|
||||||
{
|
{
|
16
API/DTOs/KavitaPlus/Account/UserTokenInfo.cs
Normal file
16
API/DTOs/KavitaPlus/Account/UserTokenInfo.cs
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace API.DTOs.KavitaPlus.Account;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents information around a user's tokens and their status
|
||||||
|
/// </summary>
|
||||||
|
public class UserTokenInfo
|
||||||
|
{
|
||||||
|
public int UserId { get; set; }
|
||||||
|
public string Username { get; set; }
|
||||||
|
public bool IsAniListTokenSet { get; set; }
|
||||||
|
public bool IsAniListTokenValid { get; set; }
|
||||||
|
public DateTime AniListValidUntilUtc { get; set; }
|
||||||
|
public bool IsMalTokenSet { get; set; }
|
||||||
|
}
|
@ -0,0 +1,16 @@
|
|||||||
|
using API.DTOs.Scrobbling;
|
||||||
|
|
||||||
|
namespace API.DTOs.KavitaPlus.ExternalMetadata;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Used for matching and fetching metadata on a series
|
||||||
|
/// </summary>
|
||||||
|
internal class ExternalMetadataIdsDto
|
||||||
|
{
|
||||||
|
public long? MalId { get; set; }
|
||||||
|
public int? AniListId { get; set; }
|
||||||
|
|
||||||
|
public string? SeriesName { get; set; }
|
||||||
|
public string? LocalizedSeriesName { get; set; }
|
||||||
|
public PlusMediaFormat? PlusMediaFormat { get; set; } = DTOs.Scrobbling.PlusMediaFormat.Unknown;
|
||||||
|
}
|
@ -0,0 +1,16 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using API.DTOs.Scrobbling;
|
||||||
|
|
||||||
|
namespace API.DTOs.KavitaPlus.ExternalMetadata;
|
||||||
|
|
||||||
|
internal class MatchSeriesRequestDto
|
||||||
|
{
|
||||||
|
public string SeriesName { get; set; }
|
||||||
|
public ICollection<string> AlternativeNames { get; set; }
|
||||||
|
public int Year { get; set; } = 0;
|
||||||
|
public string Query { get; set; }
|
||||||
|
public int? AniListId { get; set; }
|
||||||
|
public long? MalId { get; set; }
|
||||||
|
public string? HardcoverId { get; set; }
|
||||||
|
public PlusMediaFormat Format { get; set; }
|
||||||
|
}
|
@ -0,0 +1,14 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using API.DTOs.Scrobbling;
|
||||||
|
using API.DTOs.SeriesDetail;
|
||||||
|
|
||||||
|
namespace API.DTOs.KavitaPlus.ExternalMetadata;
|
||||||
|
|
||||||
|
internal class SeriesDetailPlusApiDto
|
||||||
|
{
|
||||||
|
public IEnumerable<MediaRecommendationDto> Recommendations { get; set; }
|
||||||
|
public IEnumerable<UserReviewDto> Reviews { get; set; }
|
||||||
|
public IEnumerable<RatingDto> Ratings { get; set; }
|
||||||
|
public int? AniListId { get; set; }
|
||||||
|
public long? MalId { get; set; }
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
namespace API.DTOs.License;
|
namespace API.DTOs.KavitaPlus.License;
|
||||||
|
|
||||||
public class EncryptLicenseDto
|
public class EncryptLicenseDto
|
||||||
{
|
{
|
35
API/DTOs/KavitaPlus/License/LicenseInfoDto.cs
Normal file
35
API/DTOs/KavitaPlus/License/LicenseInfoDto.cs
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace API.DTOs.KavitaPlus.License;
|
||||||
|
|
||||||
|
public class LicenseInfoDto
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// If cancelled, will represent cancellation date. If not, will represent repayment date
|
||||||
|
/// </summary>
|
||||||
|
public DateTime ExpirationDate { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// If cancelled or not
|
||||||
|
/// </summary>
|
||||||
|
public bool IsActive { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// If will be or is cancelled
|
||||||
|
/// </summary>
|
||||||
|
public bool IsCancelled { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Is the installed version valid for Kavita+ (aka within 3 releases)
|
||||||
|
/// </summary>
|
||||||
|
public bool IsValidVersion { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// The email on file
|
||||||
|
/// </summary>
|
||||||
|
public string RegisteredEmail { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Number of months user has been subscribed
|
||||||
|
/// </summary>
|
||||||
|
public int TotalMonthsSubbed { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// A license is stored within Kavita
|
||||||
|
/// </summary>
|
||||||
|
public bool HasLicense { get; set; }
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
namespace API.DTOs.Account;
|
namespace API.DTOs.KavitaPlus.License;
|
||||||
|
|
||||||
public class LicenseValidDto
|
public class LicenseValidDto
|
||||||
{
|
{
|
@ -1,4 +1,4 @@
|
|||||||
namespace API.DTOs.License;
|
namespace API.DTOs.KavitaPlus.License;
|
||||||
|
|
||||||
public class ResetLicenseDto
|
public class ResetLicenseDto
|
||||||
{
|
{
|
@ -1,4 +1,4 @@
|
|||||||
namespace API.DTOs.License;
|
namespace API.DTOs.KavitaPlus.License;
|
||||||
|
|
||||||
public class UpdateLicenseDto
|
public class UpdateLicenseDto
|
||||||
{
|
{
|
19
API/DTOs/KavitaPlus/Manage/ManageMatchFilterDto.cs
Normal file
19
API/DTOs/KavitaPlus/Manage/ManageMatchFilterDto.cs
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
namespace API.DTOs.KavitaPlus.Manage;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents an option in the UI layer for Filtering
|
||||||
|
/// </summary>
|
||||||
|
public enum MatchStateOption
|
||||||
|
{
|
||||||
|
All = 0,
|
||||||
|
Matched = 1,
|
||||||
|
NotMatched = 2,
|
||||||
|
Error = 3,
|
||||||
|
DontMatch = 4
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ManageMatchFilterDto
|
||||||
|
{
|
||||||
|
public MatchStateOption MatchStateOption { get; set; } = MatchStateOption.All;
|
||||||
|
public string SearchTerm { get; set; } = string.Empty;
|
||||||
|
}
|
10
API/DTOs/KavitaPlus/Manage/ManageMatchSeriesDto.cs
Normal file
10
API/DTOs/KavitaPlus/Manage/ManageMatchSeriesDto.cs
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace API.DTOs.KavitaPlus.Manage;
|
||||||
|
|
||||||
|
public class ManageMatchSeriesDto
|
||||||
|
{
|
||||||
|
public SeriesDto Series { get; set; }
|
||||||
|
public bool IsMatched { get; set; }
|
||||||
|
public DateTime ValidUntilUtc { get; set; }
|
||||||
|
}
|
9
API/DTOs/Metadata/Matching/ExternalSeriesMatchDto.cs
Normal file
9
API/DTOs/Metadata/Matching/ExternalSeriesMatchDto.cs
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
using API.DTOs.Recommendation;
|
||||||
|
|
||||||
|
namespace API.DTOs.Metadata.Matching;
|
||||||
|
|
||||||
|
public class ExternalSeriesMatchDto
|
||||||
|
{
|
||||||
|
public ExternalSeriesDetailDto Series { get; set; }
|
||||||
|
public float MatchRating { get; set; }
|
||||||
|
}
|
20
API/DTOs/Metadata/Matching/MatchSeriesDto.cs
Normal file
20
API/DTOs/Metadata/Matching/MatchSeriesDto.cs
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
namespace API.DTOs.Metadata.Matching;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Used for matching a series with Kavita+ for metadata and scrobbling
|
||||||
|
/// </summary>
|
||||||
|
public class MatchSeriesDto
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// When set, Kavita will stop attempting to match this series and will not perform any scrobbling
|
||||||
|
/// </summary>
|
||||||
|
public bool DontMatch { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Series Id to pull internal metadata from to improve matching
|
||||||
|
/// </summary>
|
||||||
|
public int SeriesId { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Free form text to query for. Can be a url and ids will be parsed from it
|
||||||
|
/// </summary>
|
||||||
|
public string Query { get; set; }
|
||||||
|
}
|
@ -11,7 +11,7 @@ public class ExternalSeriesDetailDto
|
|||||||
public int? AniListId { get; set; }
|
public int? AniListId { get; set; }
|
||||||
public long? MALId { get; set; }
|
public long? MALId { get; set; }
|
||||||
public IList<string> Synonyms { get; set; }
|
public IList<string> Synonyms { get; set; }
|
||||||
public MediaFormat PlusMediaFormat { get; set; }
|
public PlusMediaFormat PlusMediaFormat { get; set; }
|
||||||
public string? SiteUrl { get; set; }
|
public string? SiteUrl { get; set; }
|
||||||
public string? CoverUrl { get; set; }
|
public string? CoverUrl { get; set; }
|
||||||
public IList<string> Genres { get; set; }
|
public IList<string> Genres { get; set; }
|
||||||
|
@ -12,4 +12,6 @@ public class ExternalSeriesDto
|
|||||||
public int? AniListId { get; set; }
|
public int? AniListId { get; set; }
|
||||||
public long? MalId { get; set; }
|
public long? MalId { get; set; }
|
||||||
public ScrobbleProvider Provider { get; set; } = ScrobbleProvider.AniList;
|
public ScrobbleProvider Provider { get; set; } = ScrobbleProvider.AniList;
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,7 @@ public record PlusSeriesDto
|
|||||||
public string? MangaDexId { get; set; }
|
public string? MangaDexId { get; set; }
|
||||||
public string SeriesName { get; set; }
|
public string SeriesName { get; set; }
|
||||||
public string? AltSeriesName { get; set; }
|
public string? AltSeriesName { get; set; }
|
||||||
public MediaFormat MediaFormat { get; set; }
|
public PlusMediaFormat MediaFormat { get; set; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Optional but can help with matching
|
/// Optional but can help with matching
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -22,7 +22,7 @@ public enum ScrobbleEventType
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents PlusMediaFormat
|
/// Represents PlusMediaFormat
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public enum MediaFormat
|
public enum PlusMediaFormat
|
||||||
{
|
{
|
||||||
[Description("Manga")]
|
[Description("Manga")]
|
||||||
Manga = 1,
|
Manga = 1,
|
||||||
@ -44,7 +44,7 @@ public class ScrobbleDto
|
|||||||
public string AniListToken { get; set; }
|
public string AniListToken { get; set; }
|
||||||
public string SeriesName { get; set; }
|
public string SeriesName { get; set; }
|
||||||
public string LocalizedSeriesName { get; set; }
|
public string LocalizedSeriesName { get; set; }
|
||||||
public MediaFormat Format { get; set; }
|
public PlusMediaFormat Format { get; set; }
|
||||||
public int? Year { get; set; }
|
public int? Year { get; set; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Optional AniListId if present on Kavita's WebLinks
|
/// Optional AniListId if present on Kavita's WebLinks
|
||||||
|
@ -67,6 +67,16 @@ public class SeriesDto : IHasReadTimeEstimate, IHasCoverImage
|
|||||||
/// The last time the folder for this series was scanned
|
/// The last time the folder for this series was scanned
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public DateTime LastFolderScanned { get; set; }
|
public DateTime LastFolderScanned { get; set; }
|
||||||
|
#region KavitaPlus
|
||||||
|
/// <summary>
|
||||||
|
/// Do not match the series with any external Metadata service. This will automatically opt it out of scrobbling.
|
||||||
|
/// </summary>
|
||||||
|
public bool DontMatch { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// If the series was unable to match, it will be blacklisted until a manual metadata match overrides it
|
||||||
|
/// </summary>
|
||||||
|
public bool IsBlacklisted { get; set; }
|
||||||
|
#endregion
|
||||||
|
|
||||||
public string? CoverImage { get; set; }
|
public string? CoverImage { get; set; }
|
||||||
public string PrimaryColor { get; set; }
|
public string PrimaryColor { get; set; }
|
||||||
|
@ -1,4 +1,7 @@
|
|||||||
namespace API.DTOs.Update;
|
using System.Collections.Generic;
|
||||||
|
using System.Runtime.InteropServices.JavaScript;
|
||||||
|
|
||||||
|
namespace API.DTOs.Update;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Update Notification denoting a new release available for user to update to
|
/// Update Notification denoting a new release available for user to update to
|
||||||
@ -21,11 +24,11 @@ public class UpdateNotificationDto
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Title of the release
|
/// Title of the release
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public required string UpdateTitle { get; init; }
|
public required string UpdateTitle { get; set; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Github Url
|
/// Github Url
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public required string UpdateUrl { get; init; }
|
public required string UpdateUrl { get; set; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// If this install is within Docker
|
/// If this install is within Docker
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -37,7 +40,8 @@ public class UpdateNotificationDto
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Date of the publish
|
/// Date of the publish
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public required string PublishDate { get; init; }
|
public required string PublishDate { get; set
|
||||||
|
; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Is the server on a nightly within this release
|
/// Is the server on a nightly within this release
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -50,4 +54,16 @@ public class UpdateNotificationDto
|
|||||||
/// Is the server on this version
|
/// Is the server on this version
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool IsReleaseEqual { get; set; }
|
public bool IsReleaseEqual { get; set; }
|
||||||
|
|
||||||
|
public IList<string> Added { get; set; }
|
||||||
|
public IList<string> Removed { get; set; }
|
||||||
|
public IList<string> Changed { get; set; }
|
||||||
|
public IList<string> Fixed { get; set; }
|
||||||
|
public IList<string> Theme { get; set; }
|
||||||
|
public IList<string> Developer { get; set; }
|
||||||
|
public IList<string> Api { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// The part above the changelog part
|
||||||
|
/// </summary>
|
||||||
|
public string BlogPart { get; set; }
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,7 @@ using System.Threading.Tasks;
|
|||||||
using API.Entities;
|
using API.Entities;
|
||||||
using API.Entities.Enums;
|
using API.Entities.Enums;
|
||||||
using API.Entities.Enums.UserPreferences;
|
using API.Entities.Enums.UserPreferences;
|
||||||
|
using API.Entities.History;
|
||||||
using API.Entities.Interfaces;
|
using API.Entities.Interfaces;
|
||||||
using API.Entities.Metadata;
|
using API.Entities.Metadata;
|
||||||
using API.Entities.Scrobble;
|
using API.Entities.Scrobble;
|
||||||
@ -68,6 +69,7 @@ public sealed class DataContext : IdentityDbContext<AppUser, AppRole, int,
|
|||||||
public DbSet<AppUserCollection> AppUserCollection { get; set; } = null!;
|
public DbSet<AppUserCollection> AppUserCollection { get; set; } = null!;
|
||||||
public DbSet<ChapterPeople> ChapterPeople { get; set; } = null!;
|
public DbSet<ChapterPeople> ChapterPeople { get; set; } = null!;
|
||||||
public DbSet<SeriesMetadataPeople> SeriesMetadataPeople { get; set; } = null!;
|
public DbSet<SeriesMetadataPeople> SeriesMetadataPeople { get; set; } = null!;
|
||||||
|
public DbSet<EmailHistory> EmailHistory { get; set; } = null!;
|
||||||
|
|
||||||
|
|
||||||
protected override void OnModelCreating(ModelBuilder builder)
|
protected override void OnModelCreating(ModelBuilder builder)
|
||||||
|
@ -0,0 +1,56 @@
|
|||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using API.Entities;
|
||||||
|
using API.Entities.History;
|
||||||
|
using API.Entities.Metadata;
|
||||||
|
using Kavita.Common.EnvironmentInfo;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace API.Data.ManualMigrations;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// v0.8.5 - Migrating Kavita+ BlacklistedSeries table to Series entity to streamline implementation and generate a "Needs Manual Match" entry for the Series
|
||||||
|
/// </summary>
|
||||||
|
public static class ManualMigrateBlacklistTableToSeries
|
||||||
|
{
|
||||||
|
public static async Task Migrate(DataContext context, ILogger<Program> logger)
|
||||||
|
{
|
||||||
|
if (await context.ManualMigrationHistory.AnyAsync(m => m.Name == "ManualMigrateBlacklistTableToSeries"))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.LogCritical("Running ManualMigrateBlacklistTableToSeries migration - Please be patient, this may take some time. This is not an error");
|
||||||
|
|
||||||
|
// Get all series in the Blacklist table and set their IsBlacklist = true
|
||||||
|
var blacklistedSeries = await context.SeriesBlacklist
|
||||||
|
.Include(s => s.Series.ExternalSeriesMetadata)
|
||||||
|
.Select(s => s.Series)
|
||||||
|
.ToListAsync();
|
||||||
|
foreach (var series in blacklistedSeries)
|
||||||
|
{
|
||||||
|
series.IsBlacklisted = true;
|
||||||
|
series.ExternalSeriesMetadata ??= new ExternalSeriesMetadata() { SeriesId = series.Id };
|
||||||
|
context.Series.Entry(series).State = EntityState.Modified;
|
||||||
|
}
|
||||||
|
// Remove everything in SeriesBlacklist (it will be removed in another migration)
|
||||||
|
context.SeriesBlacklist.RemoveRange(context.SeriesBlacklist);
|
||||||
|
|
||||||
|
if (context.ChangeTracker.HasChanges())
|
||||||
|
{
|
||||||
|
await context.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
await context.ManualMigrationHistory.AddAsync(new ManualMigrationHistory()
|
||||||
|
{
|
||||||
|
Name = "ManualMigrateBlacklistTableToSeries",
|
||||||
|
ProductVersion = BuildInfo.Version.ToString(),
|
||||||
|
RanAt = DateTime.UtcNow
|
||||||
|
});
|
||||||
|
await context.SaveChangesAsync();
|
||||||
|
|
||||||
|
logger.LogCritical("Running ManualMigrateBlacklistTableToSeries migration - Completed. This is not an error");
|
||||||
|
}
|
||||||
|
}
|
@ -4,6 +4,7 @@ using System.Linq;
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using API.Entities;
|
using API.Entities;
|
||||||
using API.Entities.Enums;
|
using API.Entities.Enums;
|
||||||
|
using API.Entities.History;
|
||||||
using Flurl.Util;
|
using Flurl.Util;
|
||||||
using Kavita.Common.EnvironmentInfo;
|
using Kavita.Common.EnvironmentInfo;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
@ -3,6 +3,7 @@ using System.IO;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using API.Entities;
|
using API.Entities;
|
||||||
|
using API.Entities.History;
|
||||||
using API.Extensions;
|
using API.Extensions;
|
||||||
using API.Helpers.Builders;
|
using API.Helpers.Builders;
|
||||||
using API.Services;
|
using API.Services;
|
||||||
|
@ -3,6 +3,7 @@ using System.IO;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using API.Entities;
|
using API.Entities;
|
||||||
|
using API.Entities.History;
|
||||||
using API.Extensions;
|
using API.Extensions;
|
||||||
using API.Helpers.Builders;
|
using API.Helpers.Builders;
|
||||||
using API.Services;
|
using API.Services;
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using API.Entities;
|
using API.Entities;
|
||||||
|
using API.Entities.History;
|
||||||
using Kavita.Common.EnvironmentInfo;
|
using Kavita.Common.EnvironmentInfo;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using API.Entities;
|
using API.Entities;
|
||||||
|
using API.Entities.History;
|
||||||
using Kavita.Common.EnvironmentInfo;
|
using Kavita.Common.EnvironmentInfo;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using API.Entities;
|
using API.Entities;
|
||||||
|
using API.Entities.History;
|
||||||
using Kavita.Common.EnvironmentInfo;
|
using Kavita.Common.EnvironmentInfo;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
@ -3,6 +3,7 @@ using System.Linq;
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using API.Entities;
|
using API.Entities;
|
||||||
using API.Entities.Enums;
|
using API.Entities.Enums;
|
||||||
|
using API.Entities.History;
|
||||||
using Kavita.Common.EnvironmentInfo;
|
using Kavita.Common.EnvironmentInfo;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
@ -3,6 +3,7 @@ using System.IO;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using API.Entities;
|
using API.Entities;
|
||||||
|
using API.Entities.History;
|
||||||
using API.Services.Tasks.Scanner.Parser;
|
using API.Services.Tasks.Scanner.Parser;
|
||||||
using Kavita.Common.EnvironmentInfo;
|
using Kavita.Common.EnvironmentInfo;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using API.Entities;
|
using API.Entities;
|
||||||
|
using API.Entities.History;
|
||||||
using API.Services.Tasks.Scanner.Parser;
|
using API.Services.Tasks.Scanner.Parser;
|
||||||
using Kavita.Common.EnvironmentInfo;
|
using Kavita.Common.EnvironmentInfo;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using API.Entities;
|
using API.Entities;
|
||||||
|
using API.Entities.History;
|
||||||
using API.Extensions;
|
using API.Extensions;
|
||||||
using API.Helpers.Builders;
|
using API.Helpers.Builders;
|
||||||
using API.Services.Tasks.Scanner.Parser;
|
using API.Services.Tasks.Scanner.Parser;
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using API.Entities;
|
using API.Entities;
|
||||||
|
using API.Entities.History;
|
||||||
using Kavita.Common.EnvironmentInfo;
|
using Kavita.Common.EnvironmentInfo;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
@ -4,6 +4,7 @@ using System.Threading.Tasks;
|
|||||||
using API.Data.Repositories;
|
using API.Data.Repositories;
|
||||||
using API.Entities;
|
using API.Entities;
|
||||||
using API.Entities.Enums;
|
using API.Entities.Enums;
|
||||||
|
using API.Entities.History;
|
||||||
using API.Extensions.QueryExtensions;
|
using API.Extensions.QueryExtensions;
|
||||||
using Kavita.Common.EnvironmentInfo;
|
using Kavita.Common.EnvironmentInfo;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using API.Entities;
|
using API.Entities;
|
||||||
|
using API.Entities.History;
|
||||||
using API.Services.Tasks.Scanner.Parser;
|
using API.Services.Tasks.Scanner.Parser;
|
||||||
using Kavita.Common.EnvironmentInfo;
|
using Kavita.Common.EnvironmentInfo;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
@ -4,6 +4,7 @@ using System.Linq;
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using API.Entities;
|
using API.Entities;
|
||||||
using API.Entities.Enums;
|
using API.Entities.Enums;
|
||||||
|
using API.Entities.History;
|
||||||
using API.Services;
|
using API.Services;
|
||||||
using Kavita.Common.EnvironmentInfo;
|
using Kavita.Common.EnvironmentInfo;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using API.Entities;
|
using API.Entities;
|
||||||
|
using API.Entities.History;
|
||||||
using API.Services.Tasks.Scanner.Parser;
|
using API.Services.Tasks.Scanner.Parser;
|
||||||
using Kavita.Common.EnvironmentInfo;
|
using Kavita.Common.EnvironmentInfo;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using API.Entities;
|
using API.Entities;
|
||||||
|
using API.Entities.History;
|
||||||
using API.Services.Tasks.Scanner.Parser;
|
using API.Services.Tasks.Scanner.Parser;
|
||||||
using Kavita.Common.EnvironmentInfo;
|
using Kavita.Common.EnvironmentInfo;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using API.Entities;
|
using API.Entities;
|
||||||
|
using API.Entities.History;
|
||||||
using API.Services.Tasks.Scanner.Parser;
|
using API.Services.Tasks.Scanner.Parser;
|
||||||
using Kavita.Common.EnvironmentInfo;
|
using Kavita.Common.EnvironmentInfo;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using API.Entities;
|
using API.Entities;
|
||||||
|
using API.Entities.History;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
@ -4,6 +4,7 @@ using System.IO;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using API.Entities;
|
using API.Entities;
|
||||||
|
using API.Entities.History;
|
||||||
using API.Services;
|
using API.Services;
|
||||||
using CsvHelper;
|
using CsvHelper;
|
||||||
using CsvHelper.Configuration.Attributes;
|
using CsvHelper.Configuration.Attributes;
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using API.Entities;
|
using API.Entities;
|
||||||
|
using API.Entities.History;
|
||||||
using API.Services;
|
using API.Services;
|
||||||
using API.Services.Tasks.Scanner.Parser;
|
using API.Services.Tasks.Scanner.Parser;
|
||||||
using Kavita.Common.EnvironmentInfo;
|
using Kavita.Common.EnvironmentInfo;
|
||||||
|
@ -4,6 +4,7 @@ using System.Text.RegularExpressions;
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using API.DTOs.Filtering.v2;
|
using API.DTOs.Filtering.v2;
|
||||||
using API.Entities;
|
using API.Entities;
|
||||||
|
using API.Entities.History;
|
||||||
using API.Helpers;
|
using API.Helpers;
|
||||||
using Kavita.Common.EnvironmentInfo;
|
using Kavita.Common.EnvironmentInfo;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using API.Entities;
|
using API.Entities;
|
||||||
|
using API.Entities.History;
|
||||||
using Kavita.Common.EnvironmentInfo;
|
using Kavita.Common.EnvironmentInfo;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
3203
API/Data/Migrations/20250105180131_SeriesDontMatchAndBlacklist.Designer.cs
generated
Normal file
3203
API/Data/Migrations/20250105180131_SeriesDontMatchAndBlacklist.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,40 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace API.Data.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class SeriesDontMatchAndBlacklist : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<bool>(
|
||||||
|
name: "DontMatch",
|
||||||
|
table: "Series",
|
||||||
|
type: "INTEGER",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: false);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<bool>(
|
||||||
|
name: "IsBlacklisted",
|
||||||
|
table: "Series",
|
||||||
|
type: "INTEGER",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "DontMatch",
|
||||||
|
table: "Series");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "IsBlacklisted",
|
||||||
|
table: "Series");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
3265
API/Data/Migrations/20250109173537_EmailHistory.Designer.cs
generated
Normal file
3265
API/Data/Migrations/20250109173537_EmailHistory.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
62
API/Data/Migrations/20250109173537_EmailHistory.cs
Normal file
62
API/Data/Migrations/20250109173537_EmailHistory.cs
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace API.Data.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class EmailHistory : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "EmailHistory",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<long>(type: "INTEGER", nullable: false)
|
||||||
|
.Annotation("Sqlite:Autoincrement", true),
|
||||||
|
Sent = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||||
|
SendDate = table.Column<DateTime>(type: "TEXT", nullable: false),
|
||||||
|
EmailTemplate = table.Column<string>(type: "TEXT", nullable: true),
|
||||||
|
Subject = table.Column<string>(type: "TEXT", nullable: true),
|
||||||
|
Body = table.Column<string>(type: "TEXT", nullable: true),
|
||||||
|
DeliveryStatus = table.Column<string>(type: "TEXT", nullable: true),
|
||||||
|
ErrorMessage = table.Column<string>(type: "TEXT", nullable: true),
|
||||||
|
AppUserId = table.Column<int>(type: "INTEGER", nullable: false),
|
||||||
|
Created = table.Column<DateTime>(type: "TEXT", nullable: false),
|
||||||
|
CreatedUtc = table.Column<DateTime>(type: "TEXT", nullable: false),
|
||||||
|
LastModified = table.Column<DateTime>(type: "TEXT", nullable: false),
|
||||||
|
LastModifiedUtc = table.Column<DateTime>(type: "TEXT", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_EmailHistory", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_EmailHistory_AspNetUsers_AppUserId",
|
||||||
|
column: x => x.AppUserId,
|
||||||
|
principalTable: "AspNetUsers",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_EmailHistory_AppUserId",
|
||||||
|
table: "EmailHistory",
|
||||||
|
column: "AppUserId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_EmailHistory_Sent_AppUserId_EmailTemplate_SendDate",
|
||||||
|
table: "EmailHistory",
|
||||||
|
columns: new[] { "Sent", "AppUserId", "EmailTemplate", "SendDate" });
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "EmailHistory");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -15,7 +15,7 @@ namespace API.Data.Migrations
|
|||||||
protected override void BuildModel(ModelBuilder modelBuilder)
|
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
#pragma warning disable 612, 618
|
#pragma warning disable 612, 618
|
||||||
modelBuilder.HasAnnotation("ProductVersion", "8.0.8");
|
modelBuilder.HasAnnotation("ProductVersion", "9.0.0");
|
||||||
|
|
||||||
modelBuilder.Entity("API.Entities.AppRole", b =>
|
modelBuilder.Entity("API.Entities.AppRole", b =>
|
||||||
{
|
{
|
||||||
@ -1000,6 +1000,57 @@ namespace API.Data.Migrations
|
|||||||
b.ToTable("Device");
|
b.ToTable("Device");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.EmailHistory", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("AppUserId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Body")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime>("Created")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedUtc")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("DeliveryStatus")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("EmailTemplate")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("ErrorMessage")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime>("LastModified")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime>("LastModifiedUtc")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime>("SendDate")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("Sent")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Subject")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("AppUserId");
|
||||||
|
|
||||||
|
b.HasIndex("Sent", "AppUserId", "EmailTemplate", "SendDate");
|
||||||
|
|
||||||
|
b.ToTable("EmailHistory");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("API.Entities.FolderPath", b =>
|
modelBuilder.Entity("API.Entities.FolderPath", b =>
|
||||||
{
|
{
|
||||||
b.Property<int>("Id")
|
b.Property<int>("Id")
|
||||||
@ -1866,12 +1917,18 @@ namespace API.Data.Migrations
|
|||||||
b.Property<DateTime>("CreatedUtc")
|
b.Property<DateTime>("CreatedUtc")
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("DontMatch")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
b.Property<string>("FolderPath")
|
b.Property<string>("FolderPath")
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
b.Property<int>("Format")
|
b.Property<int>("Format")
|
||||||
.HasColumnType("INTEGER");
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<bool>("IsBlacklisted")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
b.Property<DateTime>("LastChapterAdded")
|
b.Property<DateTime>("LastChapterAdded")
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
@ -2660,6 +2717,17 @@ namespace API.Data.Migrations
|
|||||||
b.Navigation("AppUser");
|
b.Navigation("AppUser");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.EmailHistory", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("API.Entities.AppUser", "AppUser")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("AppUserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("AppUser");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("API.Entities.FolderPath", b =>
|
modelBuilder.Entity("API.Entities.FolderPath", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("API.Entities.Library", "Library")
|
b.HasOne("API.Entities.Library", "Library")
|
||||||
|
37
API/Data/Repositories/EmailHistoryRepository.cs
Normal file
37
API/Data/Repositories/EmailHistoryRepository.cs
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using API.DTOs.Email;
|
||||||
|
using API.Entities;
|
||||||
|
using API.Helpers;
|
||||||
|
using AutoMapper;
|
||||||
|
using AutoMapper.QueryableExtensions;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace API.Data.Repositories;
|
||||||
|
|
||||||
|
public interface IEmailHistoryRepository
|
||||||
|
{
|
||||||
|
Task<IList<EmailHistoryDto>> GetEmailDtos(UserParams userParams);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class EmailHistoryRepository : IEmailHistoryRepository
|
||||||
|
{
|
||||||
|
private readonly DataContext _context;
|
||||||
|
private readonly IMapper _mapper;
|
||||||
|
|
||||||
|
public EmailHistoryRepository(DataContext context, IMapper mapper)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
_mapper = mapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public async Task<IList<EmailHistoryDto>> GetEmailDtos(UserParams userParams)
|
||||||
|
{
|
||||||
|
return await _context.EmailHistory
|
||||||
|
.OrderByDescending(h => h.SendDate)
|
||||||
|
.ProjectTo<EmailHistoryDto>(_mapper.ConfigurationProvider)
|
||||||
|
.ToListAsync();
|
||||||
|
}
|
||||||
|
}
|
@ -4,6 +4,7 @@ using System.Linq;
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using API.Constants;
|
using API.Constants;
|
||||||
using API.DTOs;
|
using API.DTOs;
|
||||||
|
using API.DTOs.KavitaPlus.Manage;
|
||||||
using API.DTOs.Recommendation;
|
using API.DTOs.Recommendation;
|
||||||
using API.DTOs.Scrobbling;
|
using API.DTOs.Scrobbling;
|
||||||
using API.DTOs.SeriesDetail;
|
using API.DTOs.SeriesDetail;
|
||||||
@ -31,14 +32,12 @@ public interface IExternalSeriesMetadataRepository
|
|||||||
void Remove(IEnumerable<ExternalRecommendation>? recommendations);
|
void Remove(IEnumerable<ExternalRecommendation>? recommendations);
|
||||||
void Remove(ExternalSeriesMetadata metadata);
|
void Remove(ExternalSeriesMetadata metadata);
|
||||||
Task<ExternalSeriesMetadata?> GetExternalSeriesMetadata(int seriesId);
|
Task<ExternalSeriesMetadata?> GetExternalSeriesMetadata(int seriesId);
|
||||||
Task<bool> ExternalSeriesMetadataNeedsRefresh(int seriesId);
|
Task<bool> NeedsDataRefresh(int seriesId);
|
||||||
Task<SeriesDetailPlusDto> GetSeriesDetailPlusDto(int seriesId);
|
Task<SeriesDetailPlusDto?> GetSeriesDetailPlusDto(int seriesId);
|
||||||
Task LinkRecommendationsToSeries(Series series);
|
Task LinkRecommendationsToSeries(Series series);
|
||||||
Task LinkRecommendationsToSeries(int seriesId);
|
|
||||||
Task<bool> IsBlacklistedSeries(int seriesId);
|
Task<bool> IsBlacklistedSeries(int seriesId);
|
||||||
Task CreateBlacklistedSeries(int seriesId, bool saveChanges = true);
|
|
||||||
Task RemoveFromBlacklist(int seriesId);
|
|
||||||
Task<IList<int>> GetAllSeriesIdsWithoutMetadata(int limit);
|
Task<IList<int>> GetAllSeriesIdsWithoutMetadata(int limit);
|
||||||
|
Task<IList<ManageMatchSeriesDto>> GetAllSeries(ManageMatchFilterDto filter);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class ExternalSeriesMetadataRepository : IExternalSeriesMetadataRepository
|
public class ExternalSeriesMetadataRepository : IExternalSeriesMetadataRepository
|
||||||
@ -107,7 +106,7 @@ public class ExternalSeriesMetadataRepository : IExternalSeriesMetadataRepositor
|
|||||||
.FirstOrDefaultAsync();
|
.FirstOrDefaultAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<bool> ExternalSeriesMetadataNeedsRefresh(int seriesId)
|
public async Task<bool> NeedsDataRefresh(int seriesId)
|
||||||
{
|
{
|
||||||
var row = await _context.ExternalSeriesMetadata
|
var row = await _context.ExternalSeriesMetadata
|
||||||
.Where(s => s.SeriesId == seriesId)
|
.Where(s => s.SeriesId == seriesId)
|
||||||
@ -115,7 +114,7 @@ public class ExternalSeriesMetadataRepository : IExternalSeriesMetadataRepositor
|
|||||||
return row == null || row.ValidUntilUtc <= DateTime.UtcNow;
|
return row == null || row.ValidUntilUtc <= DateTime.UtcNow;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<SeriesDetailPlusDto> GetSeriesDetailPlusDto(int seriesId)
|
public async Task<SeriesDetailPlusDto?> GetSeriesDetailPlusDto(int seriesId)
|
||||||
{
|
{
|
||||||
var seriesDetailDto = await _context.ExternalSeriesMetadata
|
var seriesDetailDto = await _context.ExternalSeriesMetadata
|
||||||
.Where(m => m.SeriesId == seriesId)
|
.Where(m => m.SeriesId == seriesId)
|
||||||
@ -180,13 +179,6 @@ public class ExternalSeriesMetadataRepository : IExternalSeriesMetadataRepositor
|
|||||||
return seriesDetailPlusDto;
|
return seriesDetailPlusDto;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task LinkRecommendationsToSeries(int seriesId)
|
|
||||||
{
|
|
||||||
var series = await _context.Series.Where(s => s.Id == seriesId).AsNoTracking().SingleOrDefaultAsync();
|
|
||||||
if (series == null) return;
|
|
||||||
await LinkRecommendationsToSeries(series);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Searches Recommendations without a SeriesId on record and attempts to link based on Series Name/Localized Name
|
/// Searches Recommendations without a SeriesId on record and attempts to link based on Series Name/Localized Name
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -210,45 +202,12 @@ public class ExternalSeriesMetadataRepository : IExternalSeriesMetadataRepositor
|
|||||||
|
|
||||||
public Task<bool> IsBlacklistedSeries(int seriesId)
|
public Task<bool> IsBlacklistedSeries(int seriesId)
|
||||||
{
|
{
|
||||||
return _context.SeriesBlacklist.AnyAsync(s => s.SeriesId == seriesId);
|
return _context.Series
|
||||||
|
.Where(s => s.Id == seriesId)
|
||||||
|
.Select(s => s.IsBlacklisted)
|
||||||
|
.FirstOrDefaultAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Creates a new instance against SeriesId and Saves to the DB
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="seriesId"></param>
|
|
||||||
/// <param name="saveChanges"></param>
|
|
||||||
public async Task CreateBlacklistedSeries(int seriesId, bool saveChanges = true)
|
|
||||||
{
|
|
||||||
if (seriesId <= 0 || await _context.SeriesBlacklist.AnyAsync(s => s.SeriesId == seriesId)) return;
|
|
||||||
|
|
||||||
await _context.SeriesBlacklist.AddAsync(new SeriesBlacklist()
|
|
||||||
{
|
|
||||||
SeriesId = seriesId
|
|
||||||
});
|
|
||||||
if (saveChanges)
|
|
||||||
{
|
|
||||||
await _context.SaveChangesAsync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Removes the Series from Blacklist and Saves to the DB
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="seriesId"></param>
|
|
||||||
public async Task RemoveFromBlacklist(int seriesId)
|
|
||||||
{
|
|
||||||
var seriesBlacklist = await _context.SeriesBlacklist.FirstOrDefaultAsync(sb => sb.SeriesId == seriesId);
|
|
||||||
|
|
||||||
if (seriesBlacklist != null)
|
|
||||||
{
|
|
||||||
// Remove the SeriesBlacklist entity from the context
|
|
||||||
_context.SeriesBlacklist.Remove(seriesBlacklist);
|
|
||||||
|
|
||||||
// Save the changes to the database
|
|
||||||
await _context.SaveChangesAsync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<IList<int>> GetAllSeriesIdsWithoutMetadata(int limit)
|
public async Task<IList<int>> GetAllSeriesIdsWithoutMetadata(int limit)
|
||||||
{
|
{
|
||||||
@ -261,4 +220,14 @@ public class ExternalSeriesMetadataRepository : IExternalSeriesMetadataRepositor
|
|||||||
.Take(limit)
|
.Take(limit)
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<IList<ManageMatchSeriesDto>> GetAllSeries(ManageMatchFilterDto filter)
|
||||||
|
{
|
||||||
|
return await _context.Series
|
||||||
|
.Where(s => !ExternalMetadataService.NonEligibleLibraryTypes.Contains(s.Library.Type))
|
||||||
|
.FilterMatchState(filter.MatchStateOption)
|
||||||
|
.OrderBy(s => s.NormalizedName)
|
||||||
|
.ProjectTo<ManageMatchSeriesDto>(_mapper.ConfigurationProvider)
|
||||||
|
.ToListAsync();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -29,6 +29,7 @@ public interface IScrobbleRepository
|
|||||||
Task<ScrobbleEvent?> GetEvent(int userId, int seriesId, ScrobbleEventType eventType);
|
Task<ScrobbleEvent?> GetEvent(int userId, int seriesId, ScrobbleEventType eventType);
|
||||||
Task<IEnumerable<ScrobbleEvent>> GetUserEventsForSeries(int userId, int seriesId);
|
Task<IEnumerable<ScrobbleEvent>> GetUserEventsForSeries(int userId, int seriesId);
|
||||||
Task<PagedList<ScrobbleEventDto>> GetUserEvents(int userId, ScrobbleEventFilter filter, UserParams pagination);
|
Task<PagedList<ScrobbleEventDto>> GetUserEvents(int userId, ScrobbleEventFilter filter, UserParams pagination);
|
||||||
|
Task<IList<ScrobbleEvent>> GetAllEventsForSeries(int seriesId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -153,4 +154,10 @@ public class ScrobbleRepository : IScrobbleRepository
|
|||||||
|
|
||||||
return await PagedList<ScrobbleEventDto>.CreateAsync(query, pagination.PageNumber, pagination.PageSize);
|
return await PagedList<ScrobbleEventDto>.CreateAsync(query, pagination.PageNumber, pagination.PageSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<IList<ScrobbleEvent>> GetAllEventsForSeries(int seriesId)
|
||||||
|
{
|
||||||
|
return await _context.ScrobbleEvent.Where(e => e.SeriesId == seriesId)
|
||||||
|
.ToListAsync();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,6 +15,7 @@ using API.DTOs.Filtering;
|
|||||||
using API.DTOs.Filtering.v2;
|
using API.DTOs.Filtering.v2;
|
||||||
using API.DTOs.Metadata;
|
using API.DTOs.Metadata;
|
||||||
using API.DTOs.ReadingLists;
|
using API.DTOs.ReadingLists;
|
||||||
|
using API.DTOs.Recommendation;
|
||||||
using API.DTOs.Scrobbling;
|
using API.DTOs.Scrobbling;
|
||||||
using API.DTOs.Search;
|
using API.DTOs.Search;
|
||||||
using API.DTOs.SeriesDetail;
|
using API.DTOs.SeriesDetail;
|
||||||
@ -165,6 +166,7 @@ public interface ISeriesRepository
|
|||||||
Task<PagedList<SeriesDto>> GetSeriesDtoForLibraryIdV2Async(int userId, UserParams userParams, FilterV2Dto filterDto, QueryContext queryContext = QueryContext.None);
|
Task<PagedList<SeriesDto>> GetSeriesDtoForLibraryIdV2Async(int userId, UserParams userParams, FilterV2Dto filterDto, QueryContext queryContext = QueryContext.None);
|
||||||
Task<PlusSeriesDto?> GetPlusSeriesDto(int seriesId);
|
Task<PlusSeriesDto?> GetPlusSeriesDto(int seriesId);
|
||||||
Task<int> GetCountAsync();
|
Task<int> GetCountAsync();
|
||||||
|
Task<Series?> MatchSeries(ExternalSeriesDetailDto externalSeries);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class SeriesRepository : ISeriesRepository
|
public class SeriesRepository : ISeriesRepository
|
||||||
@ -709,7 +711,7 @@ public class SeriesRepository : ISeriesRepository
|
|||||||
.Where(s => s.Id == seriesId)
|
.Where(s => s.Id == seriesId)
|
||||||
.Select(series => new PlusSeriesDto()
|
.Select(series => new PlusSeriesDto()
|
||||||
{
|
{
|
||||||
MediaFormat = LibraryTypeHelper.GetFormat(series.Library.Type),
|
MediaFormat = series.Library.Type.ConvertToPlusMediaFormat(series.Format),
|
||||||
SeriesName = series.Name,
|
SeriesName = series.Name,
|
||||||
AltSeriesName = series.LocalizedName,
|
AltSeriesName = series.LocalizedName,
|
||||||
AniListId = ScrobblingService.ExtractId<int?>(series.Metadata.WebLinks,
|
AniListId = ScrobblingService.ExtractId<int?>(series.Metadata.WebLinks,
|
||||||
@ -2037,9 +2039,6 @@ public class SeriesRepository : ISeriesRepository
|
|||||||
/// Uses multiple names to find a match against a series. If not, returns null.
|
/// Uses multiple names to find a match against a series. If not, returns null.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>This does not restrict to the user at all. That is handled at the API level.</remarks>
|
/// <remarks>This does not restrict to the user at all. That is handled at the API level.</remarks>
|
||||||
/// <param name="userId"></param>
|
|
||||||
/// <param name="names"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public async Task<SeriesDto?> GetSeriesDtoByNamesAndMetadataIds(IEnumerable<string> names, LibraryType libraryType, string aniListUrl, string malUrl)
|
public async Task<SeriesDto?> GetSeriesDtoByNamesAndMetadataIds(IEnumerable<string> names, LibraryType libraryType, string aniListUrl, string malUrl)
|
||||||
{
|
{
|
||||||
var libraryIds = await _context.Library
|
var libraryIds = await _context.Library
|
||||||
@ -2073,6 +2072,47 @@ public class SeriesRepository : ISeriesRepository
|
|||||||
.FirstOrDefaultAsync(); // Some users may have improperly configured libraries
|
.FirstOrDefaultAsync(); // Some users may have improperly configured libraries
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<Series?> MatchSeries(ExternalSeriesDetailDto externalSeries)
|
||||||
|
{
|
||||||
|
var libraryIds = await _context.Library
|
||||||
|
.Where(lib => externalSeries.PlusMediaFormat.ConvertToLibraryTypes().Contains(lib.Type))
|
||||||
|
.Select(l => l.Id)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
var normalizedNames = (externalSeries.Synonyms ?? Enumerable.Empty<string>())
|
||||||
|
.Prepend(externalSeries.Name)
|
||||||
|
.Select(n => n.ToNormalized())
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var aniListWebLink =
|
||||||
|
ScrobblingService.CreateUrl(ScrobblingService.AniListWeblinkWebsite, externalSeries.AniListId);
|
||||||
|
var malWebLink =
|
||||||
|
ScrobblingService.CreateUrl(ScrobblingService.MalWeblinkWebsite, externalSeries.MALId);
|
||||||
|
|
||||||
|
Series? result = null;
|
||||||
|
if (!string.IsNullOrEmpty(aniListWebLink) || !string.IsNullOrEmpty(malWebLink))
|
||||||
|
{
|
||||||
|
result = await _context.Series
|
||||||
|
.Where(s => !string.IsNullOrEmpty(s.Metadata.WebLinks))
|
||||||
|
.Where(s => libraryIds.Contains(s.Library.Id))
|
||||||
|
.WhereIf(!string.IsNullOrEmpty(aniListWebLink), s => s.Metadata.WebLinks.Contains(aniListWebLink))
|
||||||
|
.WhereIf(!string.IsNullOrEmpty(malWebLink), s => s.Metadata.WebLinks.Contains(malWebLink))
|
||||||
|
.Include(s => s.Metadata)
|
||||||
|
.AsSplitQuery()
|
||||||
|
.FirstOrDefaultAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result != null) return result;
|
||||||
|
|
||||||
|
return await _context.Series
|
||||||
|
.Where(s => normalizedNames.Contains(s.NormalizedName) ||
|
||||||
|
normalizedNames.Contains(s.NormalizedLocalizedName))
|
||||||
|
.Where(s => libraryIds.Contains(s.Library.Id))
|
||||||
|
.AsSplitQuery()
|
||||||
|
.Include(s => s.Metadata)
|
||||||
|
.FirstOrDefaultAsync(); // Some users may have improperly configured libraries
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns the Average rating for all users within Kavita instance
|
/// Returns the Average rating for all users within Kavita instance
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -7,6 +7,7 @@ using API.DTOs;
|
|||||||
using API.DTOs.Account;
|
using API.DTOs.Account;
|
||||||
using API.DTOs.Dashboard;
|
using API.DTOs.Dashboard;
|
||||||
using API.DTOs.Filtering.v2;
|
using API.DTOs.Filtering.v2;
|
||||||
|
using API.DTOs.KavitaPlus.Account;
|
||||||
using API.DTOs.Reader;
|
using API.DTOs.Reader;
|
||||||
using API.DTOs.Scrobbling;
|
using API.DTOs.Scrobbling;
|
||||||
using API.DTOs.SeriesDetail;
|
using API.DTOs.SeriesDetail;
|
||||||
@ -15,6 +16,7 @@ using API.Entities;
|
|||||||
using API.Extensions;
|
using API.Extensions;
|
||||||
using API.Extensions.QueryExtensions;
|
using API.Extensions.QueryExtensions;
|
||||||
using API.Extensions.QueryExtensions.Filtering;
|
using API.Extensions.QueryExtensions.Filtering;
|
||||||
|
using API.Helpers;
|
||||||
using AutoMapper;
|
using AutoMapper;
|
||||||
using AutoMapper.QueryableExtensions;
|
using AutoMapper.QueryableExtensions;
|
||||||
using Microsoft.AspNetCore.Identity;
|
using Microsoft.AspNetCore.Identity;
|
||||||
@ -96,6 +98,8 @@ public interface IUserRepository
|
|||||||
Task<IList<AppUserSideNavStream>> GetSideNavStreamsByLibraryId(int libraryId);
|
Task<IList<AppUserSideNavStream>> GetSideNavStreamsByLibraryId(int libraryId);
|
||||||
Task<IList<AppUserSideNavStream>> GetSideNavStreamWithExternalSource(int externalSourceId);
|
Task<IList<AppUserSideNavStream>> GetSideNavStreamWithExternalSource(int externalSourceId);
|
||||||
Task<IList<AppUserSideNavStream>> GetDashboardStreamsByIds(IList<int> streamIds);
|
Task<IList<AppUserSideNavStream>> GetDashboardStreamsByIds(IList<int> streamIds);
|
||||||
|
Task<IEnumerable<UserTokenInfo>> GetUserTokenInfo();
|
||||||
|
Task<AppUser?> GetUserByDeviceEmail(string deviceEmail);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class UserRepository : IUserRepository
|
public class UserRepository : IUserRepository
|
||||||
@ -490,6 +494,43 @@ public class UserRepository : IUserRepository
|
|||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<UserTokenInfo>> GetUserTokenInfo()
|
||||||
|
{
|
||||||
|
var users = await _context.AppUser
|
||||||
|
.Select(u => new
|
||||||
|
{
|
||||||
|
u.Id,
|
||||||
|
u.UserName,
|
||||||
|
u.AniListAccessToken, // JWT Token
|
||||||
|
u.MalAccessToken // JWT Token
|
||||||
|
})
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
var userTokenInfos = users.Select(user => new UserTokenInfo
|
||||||
|
{
|
||||||
|
UserId = user.Id,
|
||||||
|
Username = user.UserName,
|
||||||
|
IsAniListTokenSet = !string.IsNullOrEmpty(user.AniListAccessToken),
|
||||||
|
AniListValidUntilUtc = JwtHelper.GetTokenExpiry(user.AniListAccessToken),
|
||||||
|
IsAniListTokenValid = JwtHelper.IsTokenValid(user.AniListAccessToken),
|
||||||
|
IsMalTokenSet = !string.IsNullOrEmpty(user.MalAccessToken),
|
||||||
|
});
|
||||||
|
|
||||||
|
return userTokenInfos;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the first user with a device email matching
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="deviceEmail"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public async Task<AppUser> GetUserByDeviceEmail(string deviceEmail)
|
||||||
|
{
|
||||||
|
return await _context.AppUser
|
||||||
|
.Where(u => u.Devices.Any(d => d.EmailAddress == deviceEmail))
|
||||||
|
.FirstOrDefaultAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public async Task<IEnumerable<AppUser>> GetAdminUsersAsync()
|
public async Task<IEnumerable<AppUser>> GetAdminUsersAsync()
|
||||||
{
|
{
|
||||||
|
@ -32,6 +32,7 @@ public interface IUnitOfWork
|
|||||||
IAppUserSmartFilterRepository AppUserSmartFilterRepository { get; }
|
IAppUserSmartFilterRepository AppUserSmartFilterRepository { get; }
|
||||||
IAppUserExternalSourceRepository AppUserExternalSourceRepository { get; }
|
IAppUserExternalSourceRepository AppUserExternalSourceRepository { get; }
|
||||||
IExternalSeriesMetadataRepository ExternalSeriesMetadataRepository { get; }
|
IExternalSeriesMetadataRepository ExternalSeriesMetadataRepository { get; }
|
||||||
|
IEmailHistoryRepository EmailHistoryRepository { get; }
|
||||||
bool Commit();
|
bool Commit();
|
||||||
Task<bool> CommitAsync();
|
Task<bool> CommitAsync();
|
||||||
bool HasChanges();
|
bool HasChanges();
|
||||||
@ -72,6 +73,7 @@ public class UnitOfWork : IUnitOfWork
|
|||||||
AppUserSmartFilterRepository = new AppUserSmartFilterRepository(_context, _mapper);
|
AppUserSmartFilterRepository = new AppUserSmartFilterRepository(_context, _mapper);
|
||||||
AppUserExternalSourceRepository = new AppUserExternalSourceRepository(_context, _mapper);
|
AppUserExternalSourceRepository = new AppUserExternalSourceRepository(_context, _mapper);
|
||||||
ExternalSeriesMetadataRepository = new ExternalSeriesMetadataRepository(_context, _mapper);
|
ExternalSeriesMetadataRepository = new ExternalSeriesMetadataRepository(_context, _mapper);
|
||||||
|
EmailHistoryRepository = new EmailHistoryRepository(_context, _mapper);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -100,6 +102,7 @@ public class UnitOfWork : IUnitOfWork
|
|||||||
public IAppUserSmartFilterRepository AppUserSmartFilterRepository { get; }
|
public IAppUserSmartFilterRepository AppUserSmartFilterRepository { get; }
|
||||||
public IAppUserExternalSourceRepository AppUserExternalSourceRepository { get; }
|
public IAppUserExternalSourceRepository AppUserExternalSourceRepository { get; }
|
||||||
public IExternalSeriesMetadataRepository ExternalSeriesMetadataRepository { get; }
|
public IExternalSeriesMetadataRepository ExternalSeriesMetadataRepository { get; }
|
||||||
|
public IEmailHistoryRepository EmailHistoryRepository { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Commits changes to the DB. Completes the open transaction.
|
/// Commits changes to the DB. Completes the open transaction.
|
||||||
|
28
API/EmailTemplates/TokenExpiration.html
Normal file
28
API/EmailTemplates/TokenExpiration.html
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
<tr>
|
||||||
|
<td valign="top" style="text-align: center; padding: 20px 0 10px 20px;">
|
||||||
|
<h1 style="margin: 0; font-family: 'Montserrat', sans-serif; font-size: 30px; line-height: 36px; font-weight: bold;">Your {{Provider}} Token is Expired!</h1> </td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td valign="top" style="text-align: center; padding: 10px 20px 15px 20px; font-family: sans-serif; font-size: 18px; line-height: 20px;">
|
||||||
|
<p style="margin: 0;">Kavita will stop syncing with {{Provider}} until you renew your token.</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td valign="top" align="center" style="text-align: center; padding: 15px 0px 20px 0px;">
|
||||||
|
<!-- Button : BEGIN -->
|
||||||
|
<center>
|
||||||
|
<table role="presentation" align="center" cellspacing="0" cellpadding="0" border="0" class="center-on-narrow" style="text-align: center;">
|
||||||
|
<tr>
|
||||||
|
<td style="border-radius: 50px; background: #153643; text-align: center;" class="button-td">
|
||||||
|
<a rel="noopener noreferrer" href="{{Link}}" style="background: #153643; border: 15px solid #153643; font-family: 'Montserrat', sans-serif; font-size: 14px; line-height: 1.1; text-align: center; text-decoration: none; display: block; border-radius: 50px; font-weight: bold;" class="button-a">
|
||||||
|
<span style="color:#ffffff;" class="button-link"> RENEW </span> </a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</center>
|
||||||
|
<!-- Button : END -->
|
||||||
|
<tr>
|
||||||
|
<td valign="top" style="text-align: center; padding: 10px 20px 15px 20px; font-family: sans-serif; font-size: 12px; line-height: 20px;">
|
||||||
|
<p style="margin: 0;">If the button above does not work, please find the link here: <a style="color:inherit;margin: 0;width: 100%;word-break: break-all;" rel="noopener noreferrer" href="{{Link}}">{{Link}}</a></p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
28
API/EmailTemplates/TokenExpiringSoon.html
Normal file
28
API/EmailTemplates/TokenExpiringSoon.html
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
<tr>
|
||||||
|
<td valign="top" style="text-align: center; padding: 20px 0 10px 20px;">
|
||||||
|
<h1 style="margin: 0; font-family: 'Montserrat', sans-serif; font-size: 30px; line-height: 36px; font-weight: bold;">Your {{Provider}} Token will Expire soon!</h1> </td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td valign="top" style="text-align: center; padding: 10px 20px 15px 20px; font-family: sans-serif; font-size: 18px; line-height: 20px;">
|
||||||
|
<p style="margin: 0;">Kavita will stop syncing with {{Provider}} until you renew your token.</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td valign="top" align="center" style="text-align: center; padding: 15px 0px 20px 0px;">
|
||||||
|
<!-- Button : BEGIN -->
|
||||||
|
<center>
|
||||||
|
<table role="presentation" align="center" cellspacing="0" cellpadding="0" border="0" class="center-on-narrow" style="text-align: center;">
|
||||||
|
<tr>
|
||||||
|
<td style="border-radius: 50px; background: #153643; text-align: center;" class="button-td">
|
||||||
|
<a rel="noopener noreferrer" href="{{Link}}" style="background: #153643; border: 15px solid #153643; font-family: 'Montserrat', sans-serif; font-size: 14px; line-height: 1.1; text-align: center; text-decoration: none; display: block; border-radius: 50px; font-weight: bold;" class="button-a">
|
||||||
|
<span style="color:#ffffff;" class="button-link"> RENEW </span> </a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</center>
|
||||||
|
<!-- Button : END -->
|
||||||
|
<tr>
|
||||||
|
<td valign="top" style="text-align: center; padding: 10px 20px 15px 20px; font-family: sans-serif; font-size: 12px; line-height: 20px;">
|
||||||
|
<p style="margin: 0;">If the button above does not work, please find the link here: <a style="color:inherit;margin: 0;width: 100%;word-break: break-all;" rel="noopener noreferrer" href="{{Link}}">{{Link}}</a></p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
@ -76,6 +76,8 @@ public class AppUser : IdentityUser<int>, IHasConcurrencyToken
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public string? MalAccessToken { get; set; }
|
public string? MalAccessToken { get; set; }
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A list of Series the user doesn't want scrobbling for
|
/// A list of Series the user doesn't want scrobbling for
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
31
API/Entities/EmailHistory.cs
Normal file
31
API/Entities/EmailHistory.cs
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
using System;
|
||||||
|
using API.Entities.Interfaces;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace API.Entities;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Records all emails that are sent from Kavita
|
||||||
|
/// </summary>
|
||||||
|
[Index("Sent", "AppUserId", "EmailTemplate", "SendDate")]
|
||||||
|
public class EmailHistory : IEntityDate
|
||||||
|
{
|
||||||
|
public long Id { get; set; }
|
||||||
|
public bool Sent { get; set; }
|
||||||
|
public DateTime SendDate { get; set; } = DateTime.UtcNow;
|
||||||
|
public string EmailTemplate { get; set; }
|
||||||
|
public string Subject { get; set; }
|
||||||
|
public string Body { get; set; }
|
||||||
|
|
||||||
|
public string DeliveryStatus { get; set; }
|
||||||
|
public string ErrorMessage { get; set; }
|
||||||
|
|
||||||
|
public int AppUserId { get; set; }
|
||||||
|
public virtual AppUser AppUser { get; set; }
|
||||||
|
|
||||||
|
|
||||||
|
public DateTime Created { get; set; }
|
||||||
|
public DateTime CreatedUtc { get; set; }
|
||||||
|
public DateTime LastModified { get; set; }
|
||||||
|
public DateTime LastModifiedUtc { get; set; }
|
||||||
|
}
|
9
API/Entities/History/KavitaPlusHistory.cs
Normal file
9
API/Entities/History/KavitaPlusHistory.cs
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
namespace API.Entities.History;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Records history of actions Kavita+ takes
|
||||||
|
/// </summary>
|
||||||
|
// public class KavitaPlusHistory
|
||||||
|
// {
|
||||||
|
//
|
||||||
|
// }
|
@ -1,6 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
|
|
||||||
namespace API.Entities;
|
namespace API.Entities.History;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// This will track manual migrations so that I can use simple selects to check if a Manual Migration is needed
|
/// This will track manual migrations so that I can use simple selects to check if a Manual Migration is needed
|
@ -23,7 +23,7 @@ public class ExternalSeriesMetadata
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Average External Rating. -1 means not set, 0 - 100
|
/// Average External Rating. -1 means not set, 0 - 100
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int AverageExternalRating { get; set; } = 0;
|
public int AverageExternalRating { get; set; } = -1;
|
||||||
|
|
||||||
public int AniListId { get; set; }
|
public int AniListId { get; set; }
|
||||||
public long MalId { get; set; }
|
public long MalId { get; set; }
|
||||||
|
@ -5,10 +5,12 @@ namespace API.Entities.Metadata;
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// A blacklist of Series for Kavita+
|
/// A blacklist of Series for Kavita+
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[Obsolete("Kavita v0.8.5 moved the implementation to Series.IsBlacklisted")]
|
||||||
public class SeriesBlacklist
|
public class SeriesBlacklist
|
||||||
{
|
{
|
||||||
public int Id { get; set; }
|
public int Id { get; set; }
|
||||||
|
public DateTime LastChecked { get; set; } = DateTime.UtcNow;
|
||||||
|
|
||||||
public int SeriesId { get; set; }
|
public int SeriesId { get; set; }
|
||||||
public Series Series { get; set; }
|
public Series Series { get; set; }
|
||||||
public DateTime LastChecked { get; set; } = DateTime.UtcNow;
|
|
||||||
}
|
}
|
||||||
|
@ -28,7 +28,7 @@ public class ScrobbleEvent : IEntityDate
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public string? ReviewBody { get; set; }
|
public string? ReviewBody { get; set; }
|
||||||
public string? ReviewTitle { get; set; }
|
public string? ReviewTitle { get; set; }
|
||||||
public required MediaFormat Format { get; set; }
|
public required PlusMediaFormat Format { get; set; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Depends on the ScrobbleEvent if filled in
|
/// Depends on the ScrobbleEvent if filled in
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -103,6 +103,17 @@ public class Series : IEntityDate, IHasReadTimeEstimate, IHasCoverImage
|
|||||||
public int MaxHoursToRead { get; set; }
|
public int MaxHoursToRead { get; set; }
|
||||||
public float AvgHoursToRead { get; set; }
|
public float AvgHoursToRead { get; set; }
|
||||||
|
|
||||||
|
#region KavitaPlus
|
||||||
|
/// <summary>
|
||||||
|
/// Do not match the series with any external Metadata service. This will automatically opt it out of scrobbling.
|
||||||
|
/// </summary>
|
||||||
|
public bool DontMatch { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// If the series was unable to match, it will be blacklisted until a manual metadata match overrides it
|
||||||
|
/// </summary>
|
||||||
|
public bool IsBlacklisted { get; set; }
|
||||||
|
#endregion
|
||||||
|
|
||||||
public SeriesMetadata Metadata { get; set; } = null!;
|
public SeriesMetadata Metadata { get; set; } = null!;
|
||||||
public ExternalSeriesMetadata ExternalSeriesMetadata { get; set; } = null!;
|
public ExternalSeriesMetadata ExternalSeriesMetadata { get; set; } = null!;
|
||||||
|
|
||||||
@ -151,4 +162,14 @@ public class Series : IEntityDate, IHasReadTimeEstimate, IHasCoverImage
|
|||||||
PrimaryColor = string.Empty;
|
PrimaryColor = string.Empty;
|
||||||
SecondaryColor = string.Empty;
|
SecondaryColor = string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Is this Series capable of Scrobbling
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>This includes if there is no Match/Manual Match needed, the series is blacklisted, or has a NoMatch</remarks>
|
||||||
|
/// <returns></returns>
|
||||||
|
public bool WillScrobble()
|
||||||
|
{
|
||||||
|
return !IsBlacklisted && !DontMatch;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -24,8 +24,6 @@ public static class ApplicationServiceExtensions
|
|||||||
{
|
{
|
||||||
services.AddAutoMapper(typeof(AutoMapperProfiles).Assembly);
|
services.AddAutoMapper(typeof(AutoMapperProfiles).Assembly);
|
||||||
|
|
||||||
//services.AddScoped<DataContext>();
|
|
||||||
|
|
||||||
services.AddScoped<IUnitOfWork, UnitOfWork>();
|
services.AddScoped<IUnitOfWork, UnitOfWork>();
|
||||||
services.AddScoped<ITokenService, TokenService>();
|
services.AddScoped<ITokenService, TokenService>();
|
||||||
services.AddScoped<IFileService, FileService>();
|
services.AddScoped<IFileService, FileService>();
|
||||||
@ -52,7 +50,6 @@ public static class ApplicationServiceExtensions
|
|||||||
services.AddScoped<IStatisticService, StatisticService>();
|
services.AddScoped<IStatisticService, StatisticService>();
|
||||||
services.AddScoped<IMediaErrorService, MediaErrorService>();
|
services.AddScoped<IMediaErrorService, MediaErrorService>();
|
||||||
services.AddScoped<IMediaConversionService, MediaConversionService>();
|
services.AddScoped<IMediaConversionService, MediaConversionService>();
|
||||||
services.AddScoped<IRecommendationService, RecommendationService>();
|
|
||||||
services.AddScoped<IStreamService, StreamService>();
|
services.AddScoped<IStreamService, StreamService>();
|
||||||
|
|
||||||
services.AddScoped<IScannerService, ScannerService>();
|
services.AddScoped<IScannerService, ScannerService>();
|
||||||
@ -77,6 +74,7 @@ public static class ApplicationServiceExtensions
|
|||||||
services.AddScoped<ILicenseService, LicenseService>();
|
services.AddScoped<ILicenseService, LicenseService>();
|
||||||
services.AddScoped<IExternalMetadataService, ExternalMetadataService>();
|
services.AddScoped<IExternalMetadataService, ExternalMetadataService>();
|
||||||
services.AddScoped<ISmartCollectionSyncService, SmartCollectionSyncService>();
|
services.AddScoped<ISmartCollectionSyncService, SmartCollectionSyncService>();
|
||||||
|
services.AddScoped<IWantToReadSyncService, WantToReadSyncService>();
|
||||||
|
|
||||||
services.AddSqLite();
|
services.AddSqLite();
|
||||||
services.AddSignalR(opt => opt.EnableDetailedErrors = true);
|
services.AddSignalR(opt => opt.EnableDetailedErrors = true);
|
||||||
@ -84,12 +82,13 @@ public static class ApplicationServiceExtensions
|
|||||||
services.AddEasyCaching(options =>
|
services.AddEasyCaching(options =>
|
||||||
{
|
{
|
||||||
options.UseInMemory(EasyCacheProfiles.Favicon);
|
options.UseInMemory(EasyCacheProfiles.Favicon);
|
||||||
options.UseInMemory(EasyCacheProfiles.License);
|
|
||||||
options.UseInMemory(EasyCacheProfiles.Library);
|
options.UseInMemory(EasyCacheProfiles.Library);
|
||||||
options.UseInMemory(EasyCacheProfiles.RevokedJwt);
|
options.UseInMemory(EasyCacheProfiles.RevokedJwt);
|
||||||
|
|
||||||
// KavitaPlus stuff
|
// KavitaPlus stuff
|
||||||
options.UseInMemory(EasyCacheProfiles.KavitaPlusExternalSeries);
|
options.UseInMemory(EasyCacheProfiles.KavitaPlusExternalSeries);
|
||||||
|
options.UseInMemory(EasyCacheProfiles.License);
|
||||||
|
options.UseInMemory(EasyCacheProfiles.LicenseInfo);
|
||||||
});
|
});
|
||||||
|
|
||||||
services.AddMemoryCache(options =>
|
services.AddMemoryCache(options =>
|
||||||
|
21
API/Extensions/FlurlExtensions.cs
Normal file
21
API/Extensions/FlurlExtensions.cs
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
using System;
|
||||||
|
using Flurl.Http;
|
||||||
|
using Kavita.Common;
|
||||||
|
using Kavita.Common.EnvironmentInfo;
|
||||||
|
|
||||||
|
namespace API.Extensions;
|
||||||
|
|
||||||
|
public static class FlurlExtensions
|
||||||
|
{
|
||||||
|
public static IFlurlRequest WithKavitaPlusHeaders(this string request, string license)
|
||||||
|
{
|
||||||
|
return request
|
||||||
|
.WithHeader("Accept", "application/json")
|
||||||
|
.WithHeader("User-Agent", "Kavita")
|
||||||
|
.WithHeader("x-license-key", license)
|
||||||
|
.WithHeader("x-installId", HashUtil.ServerToken())
|
||||||
|
.WithHeader("x-kavita-version", BuildInfo.Version)
|
||||||
|
.WithHeader("Content-Type", "application/json")
|
||||||
|
.WithTimeout(TimeSpan.FromSeconds(Configuration.DefaultTimeOutSecs));
|
||||||
|
}
|
||||||
|
}
|
50
API/Extensions/PlusMediaFormatExtensions.cs
Normal file
50
API/Extensions/PlusMediaFormatExtensions.cs
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using API.DTOs.Scrobbling;
|
||||||
|
using API.Entities.Enums;
|
||||||
|
|
||||||
|
namespace API.Extensions;
|
||||||
|
|
||||||
|
public static class PlusMediaFormatExtensions
|
||||||
|
{
|
||||||
|
public static PlusMediaFormat ConvertToPlusMediaFormat(this LibraryType libraryType, MangaFormat? seriesFormat = null)
|
||||||
|
{
|
||||||
|
|
||||||
|
return libraryType switch
|
||||||
|
{
|
||||||
|
LibraryType.Manga => seriesFormat is MangaFormat.Epub ? PlusMediaFormat.LightNovel : PlusMediaFormat.Manga,
|
||||||
|
LibraryType.Comic => PlusMediaFormat.Comic,
|
||||||
|
LibraryType.LightNovel => PlusMediaFormat.LightNovel,
|
||||||
|
LibraryType.Book => PlusMediaFormat.LightNovel,
|
||||||
|
LibraryType.Image => PlusMediaFormat.Manga,
|
||||||
|
LibraryType.ComicVine => PlusMediaFormat.Comic,
|
||||||
|
_ => throw new ArgumentOutOfRangeException(nameof(libraryType), libraryType, null)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IEnumerable<LibraryType> ConvertToLibraryTypes(this PlusMediaFormat plusMediaFormat)
|
||||||
|
{
|
||||||
|
return plusMediaFormat switch
|
||||||
|
{
|
||||||
|
PlusMediaFormat.Manga => new[] { LibraryType.Manga, LibraryType.Image },
|
||||||
|
PlusMediaFormat.Comic => new[] { LibraryType.Comic, LibraryType.ComicVine },
|
||||||
|
PlusMediaFormat.LightNovel => new[] { LibraryType.LightNovel, LibraryType.Book, LibraryType.Manga },
|
||||||
|
_ => throw new ArgumentOutOfRangeException(nameof(plusMediaFormat), plusMediaFormat, null)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static IList<MangaFormat> GetMangaFormats(this PlusMediaFormat? mediaFormat)
|
||||||
|
{
|
||||||
|
if (mediaFormat == null) return [MangaFormat.Archive];
|
||||||
|
return mediaFormat switch
|
||||||
|
{
|
||||||
|
PlusMediaFormat.Manga => [MangaFormat.Archive, MangaFormat.Image],
|
||||||
|
PlusMediaFormat.Comic => [MangaFormat.Archive],
|
||||||
|
PlusMediaFormat.LightNovel => [MangaFormat.Epub, MangaFormat.Pdf],
|
||||||
|
PlusMediaFormat.Book => [MangaFormat.Epub, MangaFormat.Pdf],
|
||||||
|
PlusMediaFormat.Unknown => [MangaFormat.Archive],
|
||||||
|
_ => [MangaFormat.Archive]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
@ -6,6 +6,7 @@ using System.Threading.Tasks;
|
|||||||
using API.Data.Misc;
|
using API.Data.Misc;
|
||||||
using API.Data.Repositories;
|
using API.Data.Repositories;
|
||||||
using API.DTOs.Filtering;
|
using API.DTOs.Filtering;
|
||||||
|
using API.DTOs.KavitaPlus.Manage;
|
||||||
using API.Entities;
|
using API.Entities;
|
||||||
using API.Entities.Enums;
|
using API.Entities.Enums;
|
||||||
using API.Entities.Scrobble;
|
using API.Entities.Scrobble;
|
||||||
@ -281,4 +282,17 @@ public static class QueryableExtensions
|
|||||||
{
|
{
|
||||||
return sortOptions.IsAscending ? query.OrderBy(keySelector) : query.OrderByDescending(keySelector);
|
return sortOptions.IsAscending ? query.OrderBy(keySelector) : query.OrderByDescending(keySelector);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static IQueryable<Series> FilterMatchState(this IQueryable<Series> query, MatchStateOption stateOption)
|
||||||
|
{
|
||||||
|
return stateOption switch
|
||||||
|
{
|
||||||
|
MatchStateOption.All => query,
|
||||||
|
MatchStateOption.Matched => query.Where(s => s.ExternalSeriesMetadata != null && s.ExternalSeriesMetadata.ValidUntilUtc > DateTime.MinValue && !s.IsBlacklisted),
|
||||||
|
MatchStateOption.NotMatched => query.Where(s => (s.ExternalSeriesMetadata == null || s.ExternalSeriesMetadata.ValidUntilUtc == DateTime.MinValue) && !s.IsBlacklisted),
|
||||||
|
MatchStateOption.Error => query.Where(s => s.IsBlacklisted),
|
||||||
|
MatchStateOption.DontMatch => query.Where(s => s.DontMatch),
|
||||||
|
_ => throw new ArgumentOutOfRangeException(nameof(stateOption), stateOption, null)
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
31
API/Extensions/VersionExtensions.cs
Normal file
31
API/Extensions/VersionExtensions.cs
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace API.Extensions;
|
||||||
|
|
||||||
|
public static class VersionExtensions
|
||||||
|
{
|
||||||
|
public static bool CompareWithoutRevision(this Version v1, Version v2)
|
||||||
|
{
|
||||||
|
if (v1.Major != v2.Major)
|
||||||
|
return v1.Major == v2.Major;
|
||||||
|
if (v1.Minor != v2.Minor)
|
||||||
|
return v1.Minor == v2.Minor;
|
||||||
|
if (v1.Build != v2.Build)
|
||||||
|
return v1.Build == v2.Build;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// v0.8.2.3 is within v0.8.2 (v1). Essentially checks if this is a Nightly of a stable release
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="v1"></param>
|
||||||
|
/// <param name="v2"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static bool IsWithinStableRelease(this Version v1, Version v2)
|
||||||
|
{
|
||||||
|
return v1.Major == v2.Major && v1.Minor != v2.Minor && v1.Build != v2.Build;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
@ -8,8 +8,10 @@ using API.DTOs.Collection;
|
|||||||
using API.DTOs.CollectionTags;
|
using API.DTOs.CollectionTags;
|
||||||
using API.DTOs.Dashboard;
|
using API.DTOs.Dashboard;
|
||||||
using API.DTOs.Device;
|
using API.DTOs.Device;
|
||||||
|
using API.DTOs.Email;
|
||||||
using API.DTOs.Filtering;
|
using API.DTOs.Filtering;
|
||||||
using API.DTOs.Filtering.v2;
|
using API.DTOs.Filtering.v2;
|
||||||
|
using API.DTOs.KavitaPlus.Manage;
|
||||||
using API.DTOs.MediaErrors;
|
using API.DTOs.MediaErrors;
|
||||||
using API.DTOs.Metadata;
|
using API.DTOs.Metadata;
|
||||||
using API.DTOs.Progress;
|
using API.DTOs.Progress;
|
||||||
@ -32,6 +34,8 @@ using API.Helpers.Converters;
|
|||||||
using API.Services;
|
using API.Services;
|
||||||
using AutoMapper;
|
using AutoMapper;
|
||||||
using CollectionTag = API.Entities.CollectionTag;
|
using CollectionTag = API.Entities.CollectionTag;
|
||||||
|
using EmailHistory = API.Entities.EmailHistory;
|
||||||
|
using ExternalSeriesMetadata = API.Entities.Metadata.ExternalSeriesMetadata;
|
||||||
using MediaError = API.Entities.MediaError;
|
using MediaError = API.Entities.MediaError;
|
||||||
using PublicationStatus = API.Entities.Enums.PublicationStatus;
|
using PublicationStatus = API.Entities.Enums.PublicationStatus;
|
||||||
using SiteTheme = API.Entities.SiteTheme;
|
using SiteTheme = API.Entities.SiteTheme;
|
||||||
@ -334,9 +338,21 @@ public class AutoMapperProfiles : Profile
|
|||||||
opt.MapFrom(src => ReviewService.GetCharacters(src.Body)));
|
opt.MapFrom(src => ReviewService.GetCharacters(src.Body)));
|
||||||
|
|
||||||
CreateMap<ExternalRecommendation, ExternalSeriesDto>();
|
CreateMap<ExternalRecommendation, ExternalSeriesDto>();
|
||||||
|
CreateMap<Series, ManageMatchSeriesDto>()
|
||||||
|
.ForMember(dest => dest.Series,
|
||||||
|
opt =>
|
||||||
|
opt.MapFrom(src => src))
|
||||||
|
.ForMember(dest => dest.IsMatched,
|
||||||
|
opt =>
|
||||||
|
opt.MapFrom(src => src.ExternalSeriesMetadata != null && src.ExternalSeriesMetadata.AniListId != 0 && src.ExternalSeriesMetadata.ValidUntilUtc > DateTime.MinValue))
|
||||||
|
.ForMember(dest => dest.ValidUntilUtc,
|
||||||
|
opt =>
|
||||||
|
opt.MapFrom(src => src.ExternalSeriesMetadata.ValidUntilUtc));
|
||||||
|
|
||||||
|
|
||||||
CreateMap<MangaFile, FileExtensionExportDto>();
|
CreateMap<MangaFile, FileExtensionExportDto>();
|
||||||
|
CreateMap<EmailHistory, EmailHistoryDto>()
|
||||||
|
.ForMember(dest => dest.ToUserName, opt => opt.MapFrom(src => src.AppUser.UserName));
|
||||||
|
|
||||||
CreateMap<Chapter, StandaloneChapterDto>()
|
CreateMap<Chapter, StandaloneChapterDto>()
|
||||||
.ForMember(dest => dest.SeriesId, opt => opt.MapFrom(src => src.Volume.SeriesId))
|
.ForMember(dest => dest.SeriesId, opt => opt.MapFrom(src => src.Volume.SeriesId))
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
using API.DTOs;
|
using API.DTOs;
|
||||||
using API.DTOs.Scrobbling;
|
using API.DTOs.Scrobbling;
|
||||||
using API.Entities;
|
using API.Entities;
|
||||||
|
using API.Extensions;
|
||||||
using API.Services.Plus;
|
using API.Services.Plus;
|
||||||
|
|
||||||
namespace API.Helpers.Builders;
|
namespace API.Helpers.Builders;
|
||||||
@ -19,7 +20,7 @@ public class PlusSeriesDtoBuilder : IEntityBuilder<PlusSeriesDto>
|
|||||||
{
|
{
|
||||||
_seriesDto = new PlusSeriesDto()
|
_seriesDto = new PlusSeriesDto()
|
||||||
{
|
{
|
||||||
MediaFormat = LibraryTypeHelper.GetFormat(series.Library.Type),
|
MediaFormat = series.Library.Type.ConvertToPlusMediaFormat(series.Format),
|
||||||
SeriesName = series.Name,
|
SeriesName = series.Name,
|
||||||
AltSeriesName = series.LocalizedName,
|
AltSeriesName = series.LocalizedName,
|
||||||
AniListId = ScrobblingService.ExtractId<int?>(series.Metadata.WebLinks,
|
AniListId = ScrobblingService.ExtractId<int?>(series.Metadata.WebLinks,
|
||||||
|
18
API/Helpers/DayOfWeekHelper.cs
Normal file
18
API/Helpers/DayOfWeekHelper.cs
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace API.Extensions;
|
||||||
|
|
||||||
|
public static class DayOfWeekHelper
|
||||||
|
{
|
||||||
|
private static readonly Random Rnd = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns a random DayOfWeek value.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>A randomly selected DayOfWeek.</returns>
|
||||||
|
public static DayOfWeek Random()
|
||||||
|
{
|
||||||
|
var values = Enum.GetValues<DayOfWeek>();
|
||||||
|
return (DayOfWeek)values.GetValue(Rnd.Next(values.Length))!;
|
||||||
|
}
|
||||||
|
}
|
@ -12,8 +12,10 @@ using Microsoft.EntityFrameworkCore;
|
|||||||
namespace API.Helpers;
|
namespace API.Helpers;
|
||||||
#nullable enable
|
#nullable enable
|
||||||
|
|
||||||
|
|
||||||
public static class GenreHelper
|
public static class GenreHelper
|
||||||
{
|
{
|
||||||
|
|
||||||
public static async Task UpdateChapterGenres(Chapter chapter, IEnumerable<string> genreNames, IUnitOfWork unitOfWork)
|
public static async Task UpdateChapterGenres(Chapter chapter, IEnumerable<string> genreNames, IUnitOfWork unitOfWork)
|
||||||
{
|
{
|
||||||
// Normalize genre names once and store them in a hash set for quick lookups
|
// Normalize genre names once and store them in a hash set for quick lookups
|
||||||
|
40
API/Helpers/JwtHelper.cs
Normal file
40
API/Helpers/JwtHelper.cs
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
using System;
|
||||||
|
using System.IdentityModel.Tokens.Jwt;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace API.Helpers;
|
||||||
|
|
||||||
|
public static class JwtHelper
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Extracts the expiration date from a JWT token.
|
||||||
|
/// </summary>
|
||||||
|
public static DateTime GetTokenExpiry(string jwtToken)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(jwtToken))
|
||||||
|
return DateTime.MinValue;
|
||||||
|
|
||||||
|
// Parse the JWT and extract the expiry claim
|
||||||
|
var jwtHandler = new JwtSecurityTokenHandler();
|
||||||
|
var token = jwtHandler.ReadJwtToken(jwtToken);
|
||||||
|
var exp = token.Claims.FirstOrDefault(c => c.Type == "exp")?.Value;
|
||||||
|
|
||||||
|
if (long.TryParse(exp, out var expSeconds))
|
||||||
|
{
|
||||||
|
return DateTimeOffset.FromUnixTimeSeconds(expSeconds).UtcDateTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
return DateTime.MinValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks if a JWT token is valid based on its expiry date.
|
||||||
|
/// </summary>
|
||||||
|
public static bool IsTokenValid(string jwtToken)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(jwtToken)) return false;
|
||||||
|
|
||||||
|
var expiry = GetTokenExpiry(jwtToken);
|
||||||
|
return expiry > DateTime.UtcNow;
|
||||||
|
}
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user