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
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: 8.0.x
|
||||
dotnet-version: 9.0.x
|
||||
|
||||
- name: Install Swashbuckle CLI
|
||||
shell: powershell
|
||||
|
10
.github/workflows/canary-workflow.yml
vendored
10
.github/workflows/canary-workflow.yml
vendored
@ -9,7 +9,7 @@ on:
|
||||
jobs:
|
||||
build:
|
||||
name: Upload Kavita.Common for Version Bump
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Checkout Repo
|
||||
uses: actions/checkout@v4
|
||||
@ -24,7 +24,7 @@ jobs:
|
||||
version:
|
||||
name: Bump version
|
||||
needs: [ build ]
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
@ -33,7 +33,7 @@ jobs:
|
||||
- name: Setup .NET Core
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: 8.0.x
|
||||
dotnet-version: 9.0.x
|
||||
|
||||
- name: Bump versions
|
||||
uses: SiqiLu/dotnet-bump-version@2.0.0
|
||||
@ -45,7 +45,7 @@ jobs:
|
||||
canary:
|
||||
name: Build Canary Docker
|
||||
needs: [ build, version ]
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
packages: write
|
||||
contents: read
|
||||
@ -98,7 +98,7 @@ jobs:
|
||||
- name: Compile dotnet app
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: 8.0.x
|
||||
dotnet-version: 9.0.x
|
||||
|
||||
- name: Install Swashbuckle 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:
|
||||
debug:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Debug Info
|
||||
run: |
|
||||
@ -17,7 +17,7 @@ jobs:
|
||||
echo "Matches Develop: ${{ github.ref == 'refs/heads/develop' }}"
|
||||
build:
|
||||
name: Upload Kavita.Common for Version Bump
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
if: github.ref == 'refs/heads/develop'
|
||||
steps:
|
||||
- name: Checkout Repo
|
||||
@ -33,7 +33,7 @@ jobs:
|
||||
version:
|
||||
name: Bump version
|
||||
needs: [ build ]
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
if: github.ref == 'refs/heads/develop'
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@ -43,7 +43,7 @@ jobs:
|
||||
- name: Setup .NET Core
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: 8.0.x
|
||||
dotnet-version: 9.0.x
|
||||
|
||||
- name: Bump versions
|
||||
uses: majora2007/dotnet-bump-version@v0.0.10
|
||||
@ -55,7 +55,7 @@ jobs:
|
||||
develop:
|
||||
name: Build Nightly Docker
|
||||
needs: [ build, version ]
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
if: github.ref == 'refs/heads/develop'
|
||||
permissions:
|
||||
packages: write
|
||||
@ -128,7 +128,7 @@ jobs:
|
||||
- name: Compile dotnet app
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: 8.0.x
|
||||
dotnet-version: 9.0.x
|
||||
|
||||
- name: Install Swashbuckle 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:
|
||||
check_pr:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Extract branch name
|
||||
shell: bash
|
||||
|
10
.github/workflows/release-workflow.yml
vendored
10
.github/workflows/release-workflow.yml
vendored
@ -10,7 +10,7 @@ on:
|
||||
|
||||
jobs:
|
||||
debug:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Debug Info
|
||||
run: |
|
||||
@ -20,13 +20,13 @@ jobs:
|
||||
echo "Matches Develop: ${{ github.ref == 'refs/heads/develop' }}"
|
||||
if_merged:
|
||||
if: github.event.pull_request.merged == true && contains(github.head_ref, 'release')
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- run: |
|
||||
echo The PR was merged
|
||||
build:
|
||||
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')
|
||||
steps:
|
||||
- name: Checkout Repo
|
||||
@ -43,7 +43,7 @@ jobs:
|
||||
name: Build Stable and Nightly Docker if Release
|
||||
needs: [ build ]
|
||||
if: github.event.pull_request.merged == true && contains(github.head_ref, 'release')
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
packages: write
|
||||
contents: read
|
||||
@ -106,7 +106,7 @@ jobs:
|
||||
- name: Compile dotnet app
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: 8.0.x
|
||||
dotnet-version: 9.0.x
|
||||
- name: Install Swashbuckle 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/bookmarks/
|
||||
/API/config/favicons/
|
||||
/API/config/cache-long/
|
||||
/API/config/kavita.db
|
||||
/API/config/kavita.db-shm
|
||||
/API/config/kavita.db-wal
|
||||
|
@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<OutputType>Exe</OutputType>
|
||||
</PropertyGroup>
|
||||
|
||||
|
@ -1,22 +1,22 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="8.0.10" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="9.0.1" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
||||
<PackageReference Include="NSubstitute" Version="5.3.0" />
|
||||
<PackageReference Include="System.IO.Abstractions.TestingHelpers" Version="21.1.3" />
|
||||
<PackageReference Include="TestableIO.System.IO.Abstractions.Wrappers" Version="21.1.3" />
|
||||
<PackageReference Include="xunit" Version="2.9.2" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
|
||||
<PackageReference Include="System.IO.Abstractions.TestingHelpers" Version="21.2.1" />
|
||||
<PackageReference Include="TestableIO.System.IO.Abstractions.Wrappers" Version="21.2.1" />
|
||||
<PackageReference Include="xunit" Version="2.9.3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.0.1">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</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>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
<PropertyGroup>
|
||||
<AnalysisMode>Default</AnalysisMode>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
|
||||
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
|
||||
<TieredPGO>true</TieredPGO>
|
||||
@ -12,10 +12,10 @@
|
||||
<LangVersion>latestmajor</LangVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- <Target Name="PostBuild" AfterTargets="Build" Condition=" '$(Configuration)' == 'Debug' ">-->
|
||||
<!-- <Delete Files="../openapi.json" />-->
|
||||
<!-- <Exec Command="swagger tofile --output ../openapi.json bin/$(Configuration)/$(TargetFramework)/$(AssemblyName).dll v1" />-->
|
||||
<!-- </Target>-->
|
||||
<Target Name="PostBuild" AfterTargets="Build" Condition=" '$(Configuration)' == 'Debug' ">
|
||||
<Delete Files="../openapi.json" />
|
||||
<Exec Command="swagger tofile --output ../openapi.json bin/$(Configuration)/$(TargetFramework)/$(AssemblyName).dll v1" />
|
||||
</Target>
|
||||
|
||||
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
|
||||
<DebugSymbols>false</DebugSymbols>
|
||||
@ -55,8 +55,8 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CsvHelper" Version="33.0.1" />
|
||||
<PackageReference Include="MailKit" Version="4.8.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.10">
|
||||
<PackageReference Include="MailKit" Version="4.9.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.1">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
@ -64,49 +64,49 @@
|
||||
<PackageReference Include="Docnet.Core" Version="2.6.0" />
|
||||
<PackageReference Include="EasyCaching.InMemory" Version="1.9.2" />
|
||||
<PackageReference Include="ExCSS" Version="4.3.0" />
|
||||
<PackageReference Include="Flurl" Version="3.0.7" />
|
||||
<PackageReference Include="Flurl.Http" Version="3.2.4" />
|
||||
<PackageReference Include="Hangfire" Version="1.8.15" />
|
||||
<PackageReference Include="Flurl" Version="4.0.0" />
|
||||
<PackageReference Include="Flurl.Http" Version="4.0.2" />
|
||||
<PackageReference Include="Hangfire" Version="1.8.17" />
|
||||
<PackageReference Include="Hangfire.InMemory" Version="1.0.0" />
|
||||
<PackageReference Include="Hangfire.MaximumConcurrentExecutions" Version="1.1.0" />
|
||||
<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="Hangfire.AspNetCore" Version="1.8.15" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.SignalR" Version="1.1.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.10" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="8.0.10" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.10" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.10" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.1" />
|
||||
<PackageReference Include="Hangfire.AspNetCore" Version="1.8.17" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.SignalR" Version="1.2.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.1" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="9.0.1" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="9.0.1" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.1" />
|
||||
<PackageReference Include="Microsoft.IO.RecyclableMemoryStream" Version="3.0.1" />
|
||||
<PackageReference Include="MimeTypeMapOfficial" Version="1.0.17" />
|
||||
<PackageReference Include="Nager.ArticleNumber" Version="1.0.7" />
|
||||
<PackageReference Include="NetVips" Version="3.0.0" />
|
||||
<PackageReference Include="NetVips.Native" Version="8.16.0" />
|
||||
<PackageReference Include="NReco.Logging.File" Version="1.2.1" />
|
||||
<PackageReference Include="Serilog" Version="4.1.0" />
|
||||
<PackageReference Include="Serilog.AspNetCore" Version="8.0.3" />
|
||||
<PackageReference Include="NReco.Logging.File" Version="1.2.2" />
|
||||
<PackageReference Include="Serilog" Version="4.2.0" />
|
||||
<PackageReference Include="Serilog.AspNetCore" Version="9.0.0" />
|
||||
<PackageReference Include="Serilog.Enrichers.Thread" Version="4.0.0" />
|
||||
<PackageReference Include="Serilog.Extensions.Hosting" Version="8.0.0" />
|
||||
<PackageReference Include="Serilog.Settings.Configuration" Version="8.0.4" />
|
||||
<PackageReference Include="Serilog.Extensions.Hosting" Version="9.0.0" />
|
||||
<PackageReference Include="Serilog.Settings.Configuration" Version="9.0.0" />
|
||||
<PackageReference Include="Serilog.Sinks.AspNetCore.SignalR" Version="0.4.0" />
|
||||
<PackageReference Include="Serilog.Sinks.Console" 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="SharpCompress" Version="0.38.0" />
|
||||
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.5" />
|
||||
<PackageReference Include="SonarAnalyzer.CSharp" Version="10.3.0.106239">
|
||||
<PackageReference Include="SharpCompress" Version="0.39.0" />
|
||||
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.6" />
|
||||
<PackageReference Include="SonarAnalyzer.CSharp" Version="10.5.0.109200">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</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="System.IdentityModel.Tokens.Jwt" Version="8.2.0" />
|
||||
<PackageReference Include="System.IO.Abstractions" Version="21.1.3" />
|
||||
<PackageReference Include="System.Drawing.Common" Version="8.0.10" />
|
||||
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.3.0" />
|
||||
<PackageReference Include="System.IO.Abstractions" Version="21.2.1" />
|
||||
<PackageReference Include="System.Drawing.Common" Version="9.0.1" />
|
||||
<PackageReference Include="VersOne.Epub" Version="3.3.2" />
|
||||
<PackageReference Include="YamlDotNet" Version="16.1.3" />
|
||||
<PackageReference Include="YamlDotNet" Version="16.3.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@ -192,6 +192,7 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="config\cache-long\" />
|
||||
<Folder Include="config\themes" />
|
||||
<Content Include="EmailTemplates\**">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
|
@ -12,6 +12,10 @@ public static class EasyCacheProfiles
|
||||
/// </summary>
|
||||
public const string License = "license";
|
||||
/// <summary>
|
||||
/// License Information
|
||||
/// </summary>
|
||||
public const string LicenseInfo = "licenseInfo";
|
||||
/// <summary>
|
||||
/// Cache the libraries on the server
|
||||
/// </summary>
|
||||
public const string Library = "library";
|
||||
|
@ -15,6 +15,7 @@ using API.Errors;
|
||||
using API.Extensions;
|
||||
using API.Helpers.Builders;
|
||||
using API.Services;
|
||||
using API.Services.Plus;
|
||||
using API.SignalR;
|
||||
using AutoMapper;
|
||||
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.DTOs;
|
||||
using API.DTOs.Progress;
|
||||
using API.Entities;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
@ -13,10 +16,12 @@ namespace API.Controllers;
|
||||
public class AdminController : BaseApiController
|
||||
{
|
||||
private readonly UserManager<AppUser> _userManager;
|
||||
private readonly IUnitOfWork _unitOfWork;
|
||||
|
||||
public AdminController(UserManager<AppUser> userManager)
|
||||
public AdminController(UserManager<AppUser> userManager, IUnitOfWork unitOfWork)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_unitOfWork = unitOfWork;
|
||||
}
|
||||
|
||||
/// <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 API.Constants;
|
||||
using API.Data;
|
||||
using API.DTOs.License;
|
||||
using API.DTOs.KavitaPlus.License;
|
||||
using API.Entities.Enums;
|
||||
using API.Extensions;
|
||||
using API.Services;
|
||||
using API.Services.Plus;
|
||||
using Hangfire;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@ -41,7 +42,7 @@ public class LicenseController(
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Has any license
|
||||
/// Has any license registered with the instance. Does not check Kavita+ API
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[Authorize("RequireAdminRole")]
|
||||
@ -53,6 +54,19 @@ public class LicenseController(
|
||||
(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")]
|
||||
[HttpDelete]
|
||||
[ResponseCache(CacheProfileName = ResponseCacheProfiles.LicenseCache)]
|
||||
@ -67,6 +81,7 @@ public class LicenseController(
|
||||
return Ok();
|
||||
}
|
||||
|
||||
|
||||
[Authorize("RequireAdminRole")]
|
||||
[HttpPost("reset")]
|
||||
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
|
||||
/// </summary>
|
||||
/// <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>
|
||||
[HttpGet("genres")]
|
||||
[ResponseCache(CacheProfileName = ResponseCacheProfiles.Instant, VaryByQueryKeys = ["libraryIds", "context"])]
|
||||
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));
|
||||
}
|
||||
@ -189,12 +192,12 @@ public class MetadataController(IUnitOfWork unitOfWork, ILocalizationService loc
|
||||
/// </summary>
|
||||
/// <param name="seriesId"></param>
|
||||
/// <returns></returns>
|
||||
[HttpPost("force-refresh")]
|
||||
public async Task<ActionResult> ForceRefresh(int seriesId)
|
||||
{
|
||||
await metadataService.ForceKavitaPlusRefresh(seriesId);
|
||||
return Ok();
|
||||
}
|
||||
// [HttpPost("force-refresh")]
|
||||
// public async Task<ActionResult> ForceRefresh(int seriesId)
|
||||
// {
|
||||
// await metadataService.ForceKavitaPlusRefresh(seriesId);
|
||||
// return Ok();
|
||||
// }
|
||||
|
||||
/// <summary>
|
||||
/// Fetches the details needed from Kavita+ for Series Detail page
|
||||
|
@ -764,6 +764,12 @@ public class OpdsController : BaseApiController
|
||||
return CreateXmlResult(SerializeXml(feed));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// OPDS Search endpoint
|
||||
/// </summary>
|
||||
/// <param name="apiKey"></param>
|
||||
/// <param name="query"></param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("{apiKey}/series")]
|
||||
[Produces("application/xml")]
|
||||
public async Task<IActionResult> SearchSeries(string apiKey, [FromQuery] string query)
|
||||
@ -781,20 +787,21 @@ public class OpdsController : BaseApiController
|
||||
query = query.Replace(@"%", string.Empty);
|
||||
// Get libraries user has access to
|
||||
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 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);
|
||||
SetFeedId(feed, "search-series");
|
||||
foreach (var seriesDto in series.Series)
|
||||
foreach (var seriesDto in searchResults.Series)
|
||||
{
|
||||
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()
|
||||
{
|
||||
@ -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()
|
||||
{
|
||||
@ -827,6 +834,7 @@ public class OpdsController : BaseApiController
|
||||
});
|
||||
}
|
||||
|
||||
// TODO: Search should allow Chapters/Files and more
|
||||
|
||||
return CreateXmlResult(SerializeXml(feed));
|
||||
}
|
||||
|
@ -73,7 +73,7 @@ public class PersonController : BaseApiController
|
||||
/// </summary>
|
||||
/// <param name="dto"></param>
|
||||
/// <returns></returns>
|
||||
[Authorize("AdminRequired")]
|
||||
[Authorize("RequireAdminRole")]
|
||||
[HttpPost("update")]
|
||||
public async Task<ActionResult<PersonDto>> UpdatePerson(UpdatePersonDto dto)
|
||||
{
|
||||
@ -135,7 +135,11 @@ public class PersonController : BaseApiController
|
||||
|
||||
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;
|
||||
_imageService.UpdateColorScape(person);
|
||||
|
@ -5,6 +5,7 @@ using System.Threading.Tasks;
|
||||
using API.Data;
|
||||
using API.Data.Repositories;
|
||||
using API.DTOs.Account;
|
||||
using API.DTOs.KavitaPlus.Account;
|
||||
using API.DTOs.Scrobbling;
|
||||
using API.Entities.Scrobble;
|
||||
using API.Extensions;
|
||||
@ -73,9 +74,9 @@ public class ScrobblingController : BaseApiController
|
||||
/// Update the current user's AniList token
|
||||
/// </summary>
|
||||
/// <param name="dto"></param>
|
||||
/// <returns></returns>
|
||||
/// <returns>True if the token was new or not</returns>
|
||||
[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());
|
||||
if (user == null) return Unauthorized();
|
||||
@ -85,31 +86,39 @@ public class ScrobblingController : BaseApiController
|
||||
_unitOfWork.UserRepository.Update(user);
|
||||
await _unitOfWork.CommitAsync();
|
||||
|
||||
if (isNewToken)
|
||||
{
|
||||
BackgroundJob.Enqueue(() => _scrobblingService.CreateEventsFromExistingHistory(user.Id));
|
||||
}
|
||||
|
||||
return Ok();
|
||||
return Ok(isNewToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update the current user's MAL token (Client ID) and Username
|
||||
/// </summary>
|
||||
/// <param name="dto"></param>
|
||||
/// <returns></returns>
|
||||
/// <returns>True if the token was new or not</returns>
|
||||
[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());
|
||||
if (user == null) return Unauthorized();
|
||||
|
||||
var isNewToken = string.IsNullOrEmpty(user.MalAccessToken);
|
||||
user.MalAccessToken = dto.AccessToken;
|
||||
user.MalUserName = dto.Username;
|
||||
|
||||
_unitOfWork.UserRepository.Update(user);
|
||||
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();
|
||||
}
|
||||
|
||||
|
@ -9,6 +9,7 @@ using API.DTOs.Dashboard;
|
||||
using API.DTOs.Filtering;
|
||||
using API.DTOs.Filtering.v2;
|
||||
using API.DTOs.Metadata;
|
||||
using API.DTOs.Metadata.Matching;
|
||||
using API.DTOs.Recommendation;
|
||||
using API.DTOs.SeriesDetail;
|
||||
using API.Entities;
|
||||
@ -616,4 +617,42 @@ public class SeriesController : BaseApiController
|
||||
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>
|
||||
/// Pull the Changelog for Kavita from Github and display
|
||||
/// </summary>
|
||||
/// <param name="count">How many releases from the latest to return</param>
|
||||
/// <returns></returns>
|
||||
[AllowAnonymous]
|
||||
[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
|
||||
if (User.GetUserId() == 0) return Unauthorized();
|
||||
|
||||
return Ok(await _versionUpdaterService.GetAllReleases());
|
||||
return Ok(await _versionUpdaterService.GetAllReleases(count));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -389,7 +389,6 @@ public class SettingsController : BaseApiController
|
||||
{
|
||||
if (setting.Key == ServerSettingKey.TaskBackup && updateSettingsDto.TaskBackup != setting.Value)
|
||||
{
|
||||
//if (updateSettingsDto.TotalBackup)
|
||||
setting.Value = updateSettingsDto.TaskBackup;
|
||||
_unitOfWork.SettingsRepository.Update(setting);
|
||||
|
||||
|
@ -222,18 +222,4 @@ public class StatsController : BaseApiController
|
||||
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.Repositories;
|
||||
using API.DTOs;
|
||||
using API.DTOs.KavitaPlus.Account;
|
||||
using API.Extensions;
|
||||
using API.Services;
|
||||
using API.Services.Plus;
|
||||
using API.SignalR;
|
||||
using AutoMapper;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
@ -23,14 +25,16 @@ public class UsersController : BaseApiController
|
||||
private readonly IMapper _mapper;
|
||||
private readonly IEventHub _eventHub;
|
||||
private readonly ILocalizationService _localizationService;
|
||||
private readonly ILicenseService _licenseService;
|
||||
|
||||
public UsersController(IUnitOfWork unitOfWork, IMapper mapper, IEventHub eventHub,
|
||||
ILocalizationService localizationService)
|
||||
ILocalizationService localizationService, ILicenseService licenseService)
|
||||
{
|
||||
_unitOfWork = unitOfWork;
|
||||
_mapper = mapper;
|
||||
_eventHub = eventHub;
|
||||
_localizationService = localizationService;
|
||||
_licenseService = licenseService;
|
||||
}
|
||||
|
||||
[Authorize(Policy = "RequireAdminRole")]
|
||||
@ -173,4 +177,18 @@ public class UsersController : BaseApiController
|
||||
{
|
||||
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
|
||||
{
|
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
|
||||
{
|
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
|
||||
{
|
@ -1,4 +1,4 @@
|
||||
namespace API.DTOs.License;
|
||||
namespace API.DTOs.KavitaPlus.License;
|
||||
|
||||
public class ResetLicenseDto
|
||||
{
|
@ -1,4 +1,4 @@
|
||||
namespace API.DTOs.License;
|
||||
namespace API.DTOs.KavitaPlus.License;
|
||||
|
||||
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 long? MALId { 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? CoverUrl { get; set; }
|
||||
public IList<string> Genres { get; set; }
|
||||
|
@ -12,4 +12,6 @@ public class ExternalSeriesDto
|
||||
public int? AniListId { get; set; }
|
||||
public long? MalId { get; set; }
|
||||
public ScrobbleProvider Provider { get; set; } = ScrobbleProvider.AniList;
|
||||
|
||||
|
||||
}
|
||||
|
@ -8,7 +8,7 @@ public record PlusSeriesDto
|
||||
public string? MangaDexId { get; set; }
|
||||
public string SeriesName { get; set; }
|
||||
public string? AltSeriesName { get; set; }
|
||||
public MediaFormat MediaFormat { get; set; }
|
||||
public PlusMediaFormat MediaFormat { get; set; }
|
||||
/// <summary>
|
||||
/// Optional but can help with matching
|
||||
/// </summary>
|
||||
|
@ -22,7 +22,7 @@ public enum ScrobbleEventType
|
||||
/// <summary>
|
||||
/// Represents PlusMediaFormat
|
||||
/// </summary>
|
||||
public enum MediaFormat
|
||||
public enum PlusMediaFormat
|
||||
{
|
||||
[Description("Manga")]
|
||||
Manga = 1,
|
||||
@ -44,7 +44,7 @@ public class ScrobbleDto
|
||||
public string AniListToken { get; set; }
|
||||
public string SeriesName { get; set; }
|
||||
public string LocalizedSeriesName { get; set; }
|
||||
public MediaFormat Format { get; set; }
|
||||
public PlusMediaFormat Format { get; set; }
|
||||
public int? Year { get; set; }
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// </summary>
|
||||
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 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>
|
||||
/// Update Notification denoting a new release available for user to update to
|
||||
@ -21,11 +24,11 @@ public class UpdateNotificationDto
|
||||
/// <summary>
|
||||
/// Title of the release
|
||||
/// </summary>
|
||||
public required string UpdateTitle { get; init; }
|
||||
public required string UpdateTitle { get; set; }
|
||||
/// <summary>
|
||||
/// Github Url
|
||||
/// </summary>
|
||||
public required string UpdateUrl { get; init; }
|
||||
public required string UpdateUrl { get; set; }
|
||||
/// <summary>
|
||||
/// If this install is within Docker
|
||||
/// </summary>
|
||||
@ -37,7 +40,8 @@ public class UpdateNotificationDto
|
||||
/// <summary>
|
||||
/// Date of the publish
|
||||
/// </summary>
|
||||
public required string PublishDate { get; init; }
|
||||
public required string PublishDate { get; set
|
||||
; }
|
||||
/// <summary>
|
||||
/// Is the server on a nightly within this release
|
||||
/// </summary>
|
||||
@ -50,4 +54,16 @@ public class UpdateNotificationDto
|
||||
/// Is the server on this version
|
||||
/// </summary>
|
||||
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.Enums;
|
||||
using API.Entities.Enums.UserPreferences;
|
||||
using API.Entities.History;
|
||||
using API.Entities.Interfaces;
|
||||
using API.Entities.Metadata;
|
||||
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<ChapterPeople> ChapterPeople { get; set; } = null!;
|
||||
public DbSet<SeriesMetadataPeople> SeriesMetadataPeople { get; set; } = null!;
|
||||
public DbSet<EmailHistory> EmailHistory { get; set; } = null!;
|
||||
|
||||
|
||||
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 API.Entities;
|
||||
using API.Entities.Enums;
|
||||
using API.Entities.History;
|
||||
using Flurl.Util;
|
||||
using Kavita.Common.EnvironmentInfo;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
@ -3,6 +3,7 @@ using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using API.Entities;
|
||||
using API.Entities.History;
|
||||
using API.Extensions;
|
||||
using API.Helpers.Builders;
|
||||
using API.Services;
|
||||
|
@ -3,6 +3,7 @@ using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using API.Entities;
|
||||
using API.Entities.History;
|
||||
using API.Extensions;
|
||||
using API.Helpers.Builders;
|
||||
using API.Services;
|
||||
|
@ -2,6 +2,7 @@
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using API.Entities;
|
||||
using API.Entities.History;
|
||||
using Kavita.Common.EnvironmentInfo;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using API.Entities;
|
||||
using API.Entities.History;
|
||||
using Kavita.Common.EnvironmentInfo;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
@ -2,6 +2,7 @@
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using API.Entities;
|
||||
using API.Entities.History;
|
||||
using Kavita.Common.EnvironmentInfo;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
@ -3,6 +3,7 @@ using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using API.Entities;
|
||||
using API.Entities.Enums;
|
||||
using API.Entities.History;
|
||||
using Kavita.Common.EnvironmentInfo;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
@ -3,6 +3,7 @@ using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using API.Entities;
|
||||
using API.Entities.History;
|
||||
using API.Services.Tasks.Scanner.Parser;
|
||||
using Kavita.Common.EnvironmentInfo;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using API.Entities;
|
||||
using API.Entities.History;
|
||||
using API.Services.Tasks.Scanner.Parser;
|
||||
using Kavita.Common.EnvironmentInfo;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
@ -2,6 +2,7 @@
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using API.Entities;
|
||||
using API.Entities.History;
|
||||
using API.Extensions;
|
||||
using API.Helpers.Builders;
|
||||
using API.Services.Tasks.Scanner.Parser;
|
||||
|
@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using API.Entities;
|
||||
using API.Entities.History;
|
||||
using Kavita.Common.EnvironmentInfo;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
@ -4,6 +4,7 @@ using System.Threading.Tasks;
|
||||
using API.Data.Repositories;
|
||||
using API.Entities;
|
||||
using API.Entities.Enums;
|
||||
using API.Entities.History;
|
||||
using API.Extensions.QueryExtensions;
|
||||
using Kavita.Common.EnvironmentInfo;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
@ -2,6 +2,7 @@
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using API.Entities;
|
||||
using API.Entities.History;
|
||||
using API.Services.Tasks.Scanner.Parser;
|
||||
using Kavita.Common.EnvironmentInfo;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
@ -4,6 +4,7 @@ using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using API.Entities;
|
||||
using API.Entities.Enums;
|
||||
using API.Entities.History;
|
||||
using API.Services;
|
||||
using Kavita.Common.EnvironmentInfo;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
@ -2,6 +2,7 @@
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using API.Entities;
|
||||
using API.Entities.History;
|
||||
using API.Services.Tasks.Scanner.Parser;
|
||||
using Kavita.Common.EnvironmentInfo;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
@ -2,6 +2,7 @@
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using API.Entities;
|
||||
using API.Entities.History;
|
||||
using API.Services.Tasks.Scanner.Parser;
|
||||
using Kavita.Common.EnvironmentInfo;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using API.Entities;
|
||||
using API.Entities.History;
|
||||
using API.Services.Tasks.Scanner.Parser;
|
||||
using Kavita.Common.EnvironmentInfo;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using API.Entities;
|
||||
using API.Entities.History;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
|
@ -4,6 +4,7 @@ using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using API.Entities;
|
||||
using API.Entities.History;
|
||||
using API.Services;
|
||||
using CsvHelper;
|
||||
using CsvHelper.Configuration.Attributes;
|
||||
|
@ -2,6 +2,7 @@
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using API.Entities;
|
||||
using API.Entities.History;
|
||||
using API.Services;
|
||||
using API.Services.Tasks.Scanner.Parser;
|
||||
using Kavita.Common.EnvironmentInfo;
|
||||
|
@ -4,6 +4,7 @@ using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using API.DTOs.Filtering.v2;
|
||||
using API.Entities;
|
||||
using API.Entities.History;
|
||||
using API.Helpers;
|
||||
using Kavita.Common.EnvironmentInfo;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
@ -2,6 +2,7 @@
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using API.Entities;
|
||||
using API.Entities.History;
|
||||
using Kavita.Common.EnvironmentInfo;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
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)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "8.0.8");
|
||||
modelBuilder.HasAnnotation("ProductVersion", "9.0.0");
|
||||
|
||||
modelBuilder.Entity("API.Entities.AppRole", b =>
|
||||
{
|
||||
@ -1000,6 +1000,57 @@ namespace API.Data.Migrations
|
||||
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 =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
@ -1866,12 +1917,18 @@ namespace API.Data.Migrations
|
||||
b.Property<DateTime>("CreatedUtc")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("DontMatch")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("FolderPath")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("Format")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("IsBlacklisted")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("LastChapterAdded")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
@ -2660,6 +2717,17 @@ namespace API.Data.Migrations
|
||||
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 =>
|
||||
{
|
||||
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 API.Constants;
|
||||
using API.DTOs;
|
||||
using API.DTOs.KavitaPlus.Manage;
|
||||
using API.DTOs.Recommendation;
|
||||
using API.DTOs.Scrobbling;
|
||||
using API.DTOs.SeriesDetail;
|
||||
@ -31,14 +32,12 @@ public interface IExternalSeriesMetadataRepository
|
||||
void Remove(IEnumerable<ExternalRecommendation>? recommendations);
|
||||
void Remove(ExternalSeriesMetadata metadata);
|
||||
Task<ExternalSeriesMetadata?> GetExternalSeriesMetadata(int seriesId);
|
||||
Task<bool> ExternalSeriesMetadataNeedsRefresh(int seriesId);
|
||||
Task<SeriesDetailPlusDto> GetSeriesDetailPlusDto(int seriesId);
|
||||
Task<bool> NeedsDataRefresh(int seriesId);
|
||||
Task<SeriesDetailPlusDto?> GetSeriesDetailPlusDto(int seriesId);
|
||||
Task LinkRecommendationsToSeries(Series series);
|
||||
Task LinkRecommendationsToSeries(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<ManageMatchSeriesDto>> GetAllSeries(ManageMatchFilterDto filter);
|
||||
}
|
||||
|
||||
public class ExternalSeriesMetadataRepository : IExternalSeriesMetadataRepository
|
||||
@ -107,7 +106,7 @@ public class ExternalSeriesMetadataRepository : IExternalSeriesMetadataRepositor
|
||||
.FirstOrDefaultAsync();
|
||||
}
|
||||
|
||||
public async Task<bool> ExternalSeriesMetadataNeedsRefresh(int seriesId)
|
||||
public async Task<bool> NeedsDataRefresh(int seriesId)
|
||||
{
|
||||
var row = await _context.ExternalSeriesMetadata
|
||||
.Where(s => s.SeriesId == seriesId)
|
||||
@ -115,7 +114,7 @@ public class ExternalSeriesMetadataRepository : IExternalSeriesMetadataRepositor
|
||||
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
|
||||
.Where(m => m.SeriesId == seriesId)
|
||||
@ -180,13 +179,6 @@ public class ExternalSeriesMetadataRepository : IExternalSeriesMetadataRepositor
|
||||
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>
|
||||
/// Searches Recommendations without a SeriesId on record and attempts to link based on Series Name/Localized Name
|
||||
/// </summary>
|
||||
@ -210,45 +202,12 @@ public class ExternalSeriesMetadataRepository : IExternalSeriesMetadataRepositor
|
||||
|
||||
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)
|
||||
{
|
||||
@ -261,4 +220,14 @@ public class ExternalSeriesMetadataRepository : IExternalSeriesMetadataRepositor
|
||||
.Take(limit)
|
||||
.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<IEnumerable<ScrobbleEvent>> GetUserEventsForSeries(int userId, int seriesId);
|
||||
Task<PagedList<ScrobbleEventDto>> GetUserEvents(int userId, ScrobbleEventFilter filter, UserParams pagination);
|
||||
Task<IList<ScrobbleEvent>> GetAllEventsForSeries(int seriesId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -153,4 +154,10 @@ public class ScrobbleRepository : IScrobbleRepository
|
||||
|
||||
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.Metadata;
|
||||
using API.DTOs.ReadingLists;
|
||||
using API.DTOs.Recommendation;
|
||||
using API.DTOs.Scrobbling;
|
||||
using API.DTOs.Search;
|
||||
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<PlusSeriesDto?> GetPlusSeriesDto(int seriesId);
|
||||
Task<int> GetCountAsync();
|
||||
Task<Series?> MatchSeries(ExternalSeriesDetailDto externalSeries);
|
||||
}
|
||||
|
||||
public class SeriesRepository : ISeriesRepository
|
||||
@ -709,7 +711,7 @@ public class SeriesRepository : ISeriesRepository
|
||||
.Where(s => s.Id == seriesId)
|
||||
.Select(series => new PlusSeriesDto()
|
||||
{
|
||||
MediaFormat = LibraryTypeHelper.GetFormat(series.Library.Type),
|
||||
MediaFormat = series.Library.Type.ConvertToPlusMediaFormat(series.Format),
|
||||
SeriesName = series.Name,
|
||||
AltSeriesName = series.LocalizedName,
|
||||
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.
|
||||
/// </summary>
|
||||
/// <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)
|
||||
{
|
||||
var libraryIds = await _context.Library
|
||||
@ -2073,6 +2072,47 @@ public class SeriesRepository : ISeriesRepository
|
||||
.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>
|
||||
/// Returns the Average rating for all users within Kavita instance
|
||||
/// </summary>
|
||||
|
@ -7,6 +7,7 @@ using API.DTOs;
|
||||
using API.DTOs.Account;
|
||||
using API.DTOs.Dashboard;
|
||||
using API.DTOs.Filtering.v2;
|
||||
using API.DTOs.KavitaPlus.Account;
|
||||
using API.DTOs.Reader;
|
||||
using API.DTOs.Scrobbling;
|
||||
using API.DTOs.SeriesDetail;
|
||||
@ -15,6 +16,7 @@ using API.Entities;
|
||||
using API.Extensions;
|
||||
using API.Extensions.QueryExtensions;
|
||||
using API.Extensions.QueryExtensions.Filtering;
|
||||
using API.Helpers;
|
||||
using AutoMapper;
|
||||
using AutoMapper.QueryableExtensions;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
@ -96,6 +98,8 @@ public interface IUserRepository
|
||||
Task<IList<AppUserSideNavStream>> GetSideNavStreamsByLibraryId(int libraryId);
|
||||
Task<IList<AppUserSideNavStream>> GetSideNavStreamWithExternalSource(int externalSourceId);
|
||||
Task<IList<AppUserSideNavStream>> GetDashboardStreamsByIds(IList<int> streamIds);
|
||||
Task<IEnumerable<UserTokenInfo>> GetUserTokenInfo();
|
||||
Task<AppUser?> GetUserByDeviceEmail(string deviceEmail);
|
||||
}
|
||||
|
||||
public class UserRepository : IUserRepository
|
||||
@ -490,6 +494,43 @@ public class UserRepository : IUserRepository
|
||||
.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()
|
||||
{
|
||||
|
@ -32,6 +32,7 @@ public interface IUnitOfWork
|
||||
IAppUserSmartFilterRepository AppUserSmartFilterRepository { get; }
|
||||
IAppUserExternalSourceRepository AppUserExternalSourceRepository { get; }
|
||||
IExternalSeriesMetadataRepository ExternalSeriesMetadataRepository { get; }
|
||||
IEmailHistoryRepository EmailHistoryRepository { get; }
|
||||
bool Commit();
|
||||
Task<bool> CommitAsync();
|
||||
bool HasChanges();
|
||||
@ -72,6 +73,7 @@ public class UnitOfWork : IUnitOfWork
|
||||
AppUserSmartFilterRepository = new AppUserSmartFilterRepository(_context, _mapper);
|
||||
AppUserExternalSourceRepository = new AppUserExternalSourceRepository(_context, _mapper);
|
||||
ExternalSeriesMetadataRepository = new ExternalSeriesMetadataRepository(_context, _mapper);
|
||||
EmailHistoryRepository = new EmailHistoryRepository(_context, _mapper);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -100,6 +102,7 @@ public class UnitOfWork : IUnitOfWork
|
||||
public IAppUserSmartFilterRepository AppUserSmartFilterRepository { get; }
|
||||
public IAppUserExternalSourceRepository AppUserExternalSourceRepository { get; }
|
||||
public IExternalSeriesMetadataRepository ExternalSeriesMetadataRepository { get; }
|
||||
public IEmailHistoryRepository EmailHistoryRepository { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 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>
|
||||
public string? MalAccessToken { get; set; }
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// A list of Series the user doesn't want scrobbling for
|
||||
/// </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;
|
||||
|
||||
namespace API.Entities;
|
||||
namespace API.Entities.History;
|
||||
|
||||
/// <summary>
|
||||
/// 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>
|
||||
/// Average External Rating. -1 means not set, 0 - 100
|
||||
/// </summary>
|
||||
public int AverageExternalRating { get; set; } = 0;
|
||||
public int AverageExternalRating { get; set; } = -1;
|
||||
|
||||
public int AniListId { get; set; }
|
||||
public long MalId { get; set; }
|
||||
|
@ -5,10 +5,12 @@ namespace API.Entities.Metadata;
|
||||
/// <summary>
|
||||
/// A blacklist of Series for Kavita+
|
||||
/// </summary>
|
||||
[Obsolete("Kavita v0.8.5 moved the implementation to Series.IsBlacklisted")]
|
||||
public class SeriesBlacklist
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public DateTime LastChecked { get; set; } = DateTime.UtcNow;
|
||||
|
||||
public int SeriesId { get; set; }
|
||||
public Series Series { get; set; }
|
||||
public DateTime LastChecked { get; set; } = DateTime.UtcNow;
|
||||
}
|
||||
|
@ -28,7 +28,7 @@ public class ScrobbleEvent : IEntityDate
|
||||
/// </summary>
|
||||
public string? ReviewBody { get; set; }
|
||||
public string? ReviewTitle { get; set; }
|
||||
public required MediaFormat Format { get; set; }
|
||||
public required PlusMediaFormat Format { get; set; }
|
||||
/// <summary>
|
||||
/// Depends on the ScrobbleEvent if filled in
|
||||
/// </summary>
|
||||
|
@ -103,6 +103,17 @@ public class Series : IEntityDate, IHasReadTimeEstimate, IHasCoverImage
|
||||
public int MaxHoursToRead { 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 ExternalSeriesMetadata ExternalSeriesMetadata { get; set; } = null!;
|
||||
|
||||
@ -151,4 +162,14 @@ public class Series : IEntityDate, IHasReadTimeEstimate, IHasCoverImage
|
||||
PrimaryColor = 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.AddScoped<DataContext>();
|
||||
|
||||
services.AddScoped<IUnitOfWork, UnitOfWork>();
|
||||
services.AddScoped<ITokenService, TokenService>();
|
||||
services.AddScoped<IFileService, FileService>();
|
||||
@ -52,7 +50,6 @@ public static class ApplicationServiceExtensions
|
||||
services.AddScoped<IStatisticService, StatisticService>();
|
||||
services.AddScoped<IMediaErrorService, MediaErrorService>();
|
||||
services.AddScoped<IMediaConversionService, MediaConversionService>();
|
||||
services.AddScoped<IRecommendationService, RecommendationService>();
|
||||
services.AddScoped<IStreamService, StreamService>();
|
||||
|
||||
services.AddScoped<IScannerService, ScannerService>();
|
||||
@ -77,6 +74,7 @@ public static class ApplicationServiceExtensions
|
||||
services.AddScoped<ILicenseService, LicenseService>();
|
||||
services.AddScoped<IExternalMetadataService, ExternalMetadataService>();
|
||||
services.AddScoped<ISmartCollectionSyncService, SmartCollectionSyncService>();
|
||||
services.AddScoped<IWantToReadSyncService, WantToReadSyncService>();
|
||||
|
||||
services.AddSqLite();
|
||||
services.AddSignalR(opt => opt.EnableDetailedErrors = true);
|
||||
@ -84,12 +82,13 @@ public static class ApplicationServiceExtensions
|
||||
services.AddEasyCaching(options =>
|
||||
{
|
||||
options.UseInMemory(EasyCacheProfiles.Favicon);
|
||||
options.UseInMemory(EasyCacheProfiles.License);
|
||||
options.UseInMemory(EasyCacheProfiles.Library);
|
||||
options.UseInMemory(EasyCacheProfiles.RevokedJwt);
|
||||
|
||||
// KavitaPlus stuff
|
||||
options.UseInMemory(EasyCacheProfiles.KavitaPlusExternalSeries);
|
||||
options.UseInMemory(EasyCacheProfiles.License);
|
||||
options.UseInMemory(EasyCacheProfiles.LicenseInfo);
|
||||
});
|
||||
|
||||
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.Repositories;
|
||||
using API.DTOs.Filtering;
|
||||
using API.DTOs.KavitaPlus.Manage;
|
||||
using API.Entities;
|
||||
using API.Entities.Enums;
|
||||
using API.Entities.Scrobble;
|
||||
@ -281,4 +282,17 @@ public static class QueryableExtensions
|
||||
{
|
||||
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.Dashboard;
|
||||
using API.DTOs.Device;
|
||||
using API.DTOs.Email;
|
||||
using API.DTOs.Filtering;
|
||||
using API.DTOs.Filtering.v2;
|
||||
using API.DTOs.KavitaPlus.Manage;
|
||||
using API.DTOs.MediaErrors;
|
||||
using API.DTOs.Metadata;
|
||||
using API.DTOs.Progress;
|
||||
@ -32,6 +34,8 @@ using API.Helpers.Converters;
|
||||
using API.Services;
|
||||
using AutoMapper;
|
||||
using CollectionTag = API.Entities.CollectionTag;
|
||||
using EmailHistory = API.Entities.EmailHistory;
|
||||
using ExternalSeriesMetadata = API.Entities.Metadata.ExternalSeriesMetadata;
|
||||
using MediaError = API.Entities.MediaError;
|
||||
using PublicationStatus = API.Entities.Enums.PublicationStatus;
|
||||
using SiteTheme = API.Entities.SiteTheme;
|
||||
@ -334,9 +338,21 @@ public class AutoMapperProfiles : Profile
|
||||
opt.MapFrom(src => ReviewService.GetCharacters(src.Body)));
|
||||
|
||||
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<EmailHistory, EmailHistoryDto>()
|
||||
.ForMember(dest => dest.ToUserName, opt => opt.MapFrom(src => src.AppUser.UserName));
|
||||
|
||||
CreateMap<Chapter, StandaloneChapterDto>()
|
||||
.ForMember(dest => dest.SeriesId, opt => opt.MapFrom(src => src.Volume.SeriesId))
|
||||
|
@ -2,6 +2,7 @@
|
||||
using API.DTOs;
|
||||
using API.DTOs.Scrobbling;
|
||||
using API.Entities;
|
||||
using API.Extensions;
|
||||
using API.Services.Plus;
|
||||
|
||||
namespace API.Helpers.Builders;
|
||||
@ -19,7 +20,7 @@ public class PlusSeriesDtoBuilder : IEntityBuilder<PlusSeriesDto>
|
||||
{
|
||||
_seriesDto = new PlusSeriesDto()
|
||||
{
|
||||
MediaFormat = LibraryTypeHelper.GetFormat(series.Library.Type),
|
||||
MediaFormat = series.Library.Type.ConvertToPlusMediaFormat(series.Format),
|
||||
SeriesName = series.Name,
|
||||
AltSeriesName = series.LocalizedName,
|
||||
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;
|
||||
#nullable enable
|
||||
|
||||
|
||||
public static class GenreHelper
|
||||
{
|
||||
|
||||
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
|
||||
|
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